Hey, Scripting Guy! Volume 1

The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.


How Can I Verify that ADAM is Installed?

Hey, Scripting Guy! How can I verify that ADAM is installed?

-- CW

Hey, CW. Although former President Ronald Reagan is usually given credit for the phrase “Trust but verify,” we’ve always thought that sounded more like the sort of thing a system administrator would say: “Of course I trust you when you say you installed ADAM on that computer. I just need to verify that it was installed. That’s a whole different thing.”

So how can you verify that ADAM (Active Directory Application Mode) is installed on a computer? Here’s one way, a scripting solution that is also usually credited to former President Reagan:

strComputer = "."

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

Set colServices = objWMIService.ExecQuery _
    ("Select * from Win32_Service where Name Like '%ADAM_%'")

If colServices.Count = 0 Then
    Wscript.Echo "ADAM is not installed."
Else
    For Each objService in colServices
        Wscript.Echo objService.Name & " -- " & objService.State
    Next
End If

This script relies on the fact that ADAM runs as a service: if the script uncovers an instance of the ADAM service then you know that ADAM is installed. Admittedly, there’s a catch here: each instance of ADAM runs as a separate service with a separate name. Fortunately, there’s also an easy way to determine whether any instance of ADAM is installed. And that’s exactly what we’re going to talk about next.

The script begins by connecting to the WMI service on the local computer; as usual, you can modify this script to work against a remote machine simply by assigning the computer name to the variable strComputer. We then encounter this line of code:

Set colServices = objWMIService.ExecQuery _
    ("Select * from Win32_Service where Name Like '%ADAM_%'")

What we’re doing here is using the Like operator to return all services where the service name includes the characters ADAM_. When using WMI’s LIKE operator the two percentage signs stand for “anything.” That means our query reads, “Get me all the services that have the characters ADAM_ somewhere in the Name property. I don’t care what comes before or after the ADAM_; I just care that the string ADAM_ appears somewhere in the Name.”

Why do we do this? Well, as we noted, each instance of ADAM runs as a separate service; however, each service name will begin with the characters ADAM_. For example, if you have an instance of ADAM named Fabrikam the service name will be ADAM_Fabrikam; if you have an instance of ADAM named Contoso the service name will be ADAM_Contoso. We’ve constructed our query to ensure that either of those two instances – or any other services with the characters ADAM_ in the Name – will be returned as part of the services collection.

Note. Of course, you might be thinking, “Wait a second, Scripting Guys: isn’t the LIKE operator supported only on Windows XP and Windows Server 2003? What if I’m running ADAM on Windows 2000?” Well, you are correct: the LIKE operator is supported only on Windows XP and Windows Server 2003. However, that shouldn’t pose much of a problem here because ADAM itself is supported only on Windows XP and Windows Server 2003. Because you can’t run ADAM on Windows 2000 you don’t have to worry about whether ADAM is installed on a Windows 2000 machine; it isn’t. Granted, you will have to run this script on a Windows XP or Windows Server 2003 machine. But if you’re using ADAM we’re assuming you’ll be able to find one of those computers without too much trouble.

After retrieving the collection of services we run into this If Then Else block:

If colServices.Count = 0 Then
    Wscript.Echo "ADAM is not installed."
Else
    For Each objService in colServices
        Wscript.Echo objService.Name & " -- " & objService.State
    Next
End If

What we’re doing here is checking to see if the Count property of the returned collection is equal to 0. The Count tells us how many items are in the collection: if the Count is equal to 0 then there are no items in the collection. In turn, that means no instances of ADAM are installed on the computer and we echo back that very message.

If the Count is greater than 0 that means at least one instance of ADAM was found. In that case, we set up a simple For Each loop to loop through all the instances of ADAM and, for each one, report back the instance Name and State (that is running, stopped, paused, etc.).

That’s all you have to do; just run this script and – what’s that? What do you mean you don’t trust us? Oh, we see: you do trust us, you just want to verify that the script actually works. We understand: that’s a whole different thing. Give it a try and see what happens.


How Can I Count the Number of Computers in a Domain?

Hey, Scripting Guy! How can I count the number of computers in a domain?

-- TN

Hey, TN. Here’s a true story for you. About 5 years ago, when one of the Scripting Guys first started at Microsoft, his group here (which might not be the most technically-savvy group at the company) needed to get a count of all our computers. How did they do that? Why, exactly the way you’d think they would: they distributed floppy disks to everyone. On that floppy disk was a batch file that would grab the computer name and write it to the disk. Everyone was instructed to place the disk in the floppy drive of each of their machines and then run the batch file. Later that week, IT types came around, collected all the disks, and – presumably – collated all the information to get their computer count.

Like we said, ours might not be the most technically-savvy group here. Or anywhere, for that matter.

Admittedly, that’s a tough act to follow. And while we can’t claim to have come up with anything as creative as distributing floppy disks to all your users, the following script – as humble and unimaginative as it might seem – will nonetheless give you a count of all the computers (or, at least, all the computer accounts) in your domain:

On
Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='computer'"
Set objRecordSet = objCommand.Execute

Wscript.Echo objRecordSet.RecordCount

What’s that? Why, yes, this is a script that does a search of Active Directory. And, yes, that does mean that we won’t explain the details of the script today; instead we’ll refer you to the two-part Tales from the Script series Dude, Where’s My Printer? if you need some background information on searching Active Directory. And, yes, that also means that – say, wait a second: who’s writing this column anyway?

Note. To tell you the truth, we’d be happy to let you write it. But if we didn’t do this column Microsoft would have to find something else for us to do. And we do not want to walk around from office-to-office collecting floppy disks!

As you were probably about to point out, while this is an Active Directory search script it’s actually a bit simpler than most such scripts. For example, our SQL command is pretty bare-bones: we simply write a query that returns a collection of all the objects that have an objectCategory equal to computer:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='computer'"

After defining the query we call the Execute method, which returns a recordset consisting of all the computer accounts found in the domain. At this point we would normally set up a Do Loop to walk through all the items in the collection (that is, all the computers in the domain) and echo back whatever properties we specified in our query. For this script, however, we don’t have to bother with any of that. Why not? Well, all we wanted was a count of the number of computers. As it turns out, ADO (ActiveX Data Objects) recordsets all have a property named RecordCount that tells you how many items are in the collection. Want to know how many records are in the recordset (which – by extension – tells you how many computers are in the domain)? All you have to do is echo back the value of the RecordCount property:

Wscript.Echo objRecordSet.RecordCount

That’s it. Like we said, it’s a bit on the boring side. But it does save you a lot of money on – no, not car insurance. On floppy disks. Sheesh.


How Can I Disable a Computer Account?

Hey, Scripting Guy! How can I disable a computer account?

-- MD

Hey, MD. You know, our first thought when we read your question was to ask our very own Peter Costantini, a scripting guru who’s done a lot of work with Active Directory. For better or worse, however, Peter’s originally from New Jersey and is a big fan of The Sopranos. Consequently, when we asked him how to disable a computer account he told us, “Well, foist youse takes a baseball bat and cracks ‘em good about the kneecaps ….”

You know, come to think of it, that’s Peter’s solution to every question we pose to him.

Admittedly, there have been plenty of times when we’ve wanted to take a baseball bat to our computer; however, we weren’t exactly sure how to find the computer’s kneecaps. Therefore, we decided to disable a computer account using a script like this instead:

Set objComputer = GetObject("LDAP://cn=atl-ws-01,cn=computers,dc=fabrikam,dc=com")

objComputer.AccountDisabled = True
objComputer.SetInfo

Maybe not as satisfying as whacking your computer with a baseball bat, but quicker, easier, and a lot cheaper to boot. We begin by binding to the computer account we want to disable; in this case that’s a computer named atl-ws-01, which is located in the Computers container in fabrikam.com:

Set objComputer = GetObject("LDAP://cn=atl-ws-01,cn=computers,dc=fabrikam,dc=com")

Notice the syntax we used: cn=computers. It’s important to remember that Users and Computers are not organizational units; that means the syntax ou=Computers will fail. Consequently, we use the cn= syntax instead. If our computer was in an OU (say, the Finance OU) then we’d use the ou= syntax, like so:

Set objComputer = GetObject("LDAP://cn=atl-ws-01,ou=finance,dc=fabrikam,dc=com")

After making the connection all we do is set the AccountDisabled attribute to True. (Quick quiz: How could we enable a disabled account? That’ right: set AccountDisabled to False.) We then call the SetInfo method to write the changes back to Active Directory and we’re done.

Important clarification. Peter has pointed out that he doesn’t actually say things like “foist” and “youse,” and said he is tired of people from Washington state making fun of people from New Jersey. With that in mind, he politely asked us to apologize for our disrespect; otherwise, he’d take a baseball bat to our kneecaps. Peter, we apologize.


How Can I Find and Move an Active Directory Computer Account?

Hey, Scripting Guy! I need to move a computer account from one OU to another; however, I don’t know which OU the computer account is currently in. Any advice?

-- AA

Hey, AA. You know, one thing that marks all the great advice columns – Dear Abby; Ann Landers; Hey, Scripting Guy! – is the fact that, over time, these columns tend to give the same advice over and over. No matter how many times someone writes to Dear Abby complaining about problems with a neighbor, you know she’s never going to suggest that you burn down the guy’s house. The same is true with Ann Landers, and the same is true with Hey, Scripting Guy!

Well, OK: the same is true with Ann Landers.

The point is, we often give the same advice in response to a reader’s question, and today is no exception. So here goes: this is really a two-part problem, so let’s analyze the two parts separately. Oh, and this: one of the parts involves searching Active Directory. Yes, we know: how many times have you heard that? But it seems to be what works.

As you probably figured out, if one of the parts involves searching Active Directory, the other part must involve moving a computer account from one OU to another. Because this is so easy, let’s go ahead and address it first. Here’s a sample script that moves the computer atl-ws-01 from the Finance OU to the Research OU:

Set objOU = GetObject("LDAP://OU=Research,DC=fabrikam,DC=com")
intReturn = objOU.MoveHere _
    ("LDAP://CN=atl-ws-01,OU=Finance,DC=fabrikam,DC=com", vbNullString)

That’s right: just two lines of code. We bind to the new OU (the one we want to move the computer to). After binding to the OU, we call the MoveHere method, passing it two parameters: the ADsPath of the computer we want to move; and vbNullString, a VBScript constant equal to Null. Passing a Null as the second parameter tells the MoveHere method that we want the object to keep its current CN (atl-ws-01). Had we passed a different CN, not only would the computer have been moved, but it would have been renamed as well.

By the way, you don’t actually have to include the Null parameter; if there’s no second parameter MoveHere will assume the second parameter is Null. We show it here just so you know that MoveHere accepts two parameters.

Special bonus script: Suppose you do want to rename an Active Directory account. Well, then bind to the OU where the account currently lives, call the MoveHere method, and give the account a different CN. For example, this script renames the computer atl-ws-01, giving it the new name finance-ws-01:

Set objOU = GetObject("LDAP://OU=Finance,DC=fabrikam,DC=com")
intReturn = objOU.MoveHere _
    ("LDAP://CN=atl-ws-01,OU=Finance,DC=fabrikam,DC=com", “cn=finance-ws-01”)

You’re right: we should charge extra for these bonus scripts, shouldn’t we?

Now that we know how to move a computer account all we have to do is figure out how to find that computer account. Here’s where we’ll use our Active Directory search script. This script searches Active Directory for the computer named atl-ws-01:

On
Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='computer' " & _
"AND name='atl-ws-01'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Wscript.Echo objRecordSet.Fields("ADsPath").Value
objRecordSet.MoveNext
Loop

The preceding script merely echoes the ADsPath of the computer. That’s nice, but as long as we have the AdsPath we can go ahead and move the computer account to a different OU. (Remember, unless we’re renaming the computer, AdsPath is the only parameter we need to pass to the MoveHere method.) So here’s a revised script that tracks down the computer atl-ws-01 and then moves it from the Finance OU to the Research OU:

On
Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='computer' " & _
"AND name='atl-ws-01'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
strADsPath = objRecordSet.Fields("ADsPath").Value
Set objOU =
GetObject("LDAP://OU=Research,DC=fabrikam,DC=com")
intReturn = objOU.MoveHere(strADsPath, vbNullString)
objRecordSet.MoveNext
Loop

Just that easy, just that quick.

Incidentally, we know we didn’t explain how the Active Directory searching part works; for more information about using scripts to search Active Directory, check out this Tales From the Script column.

Tomorrow: Burning down the neighbor’s house.

No, wait, don’t burn down anyone’s house: we’re just kidding about that. Maybe we’ll just stick to scripting advice from now on ….


How Can I Get a List of All My Windows Server 2003 Computers?

Hey, Scripting Guy! How can I gather the names of all the computers in my domain that are running Windows Server 2003?

-- AS

Hey, AS. When we choose questions to answer for this column, we try to take questions from different areas of scripting. That does two things for us: it keeps us from being the answers-only-questions-about-say-scripting-Active-Directory column, and it ensures that we don’t sound like we’re providing the same answer to the same question day after day. Of course, on the other hand, we get lazy every now and then, and when we do, we usually fall back on one of our old standbys: search Active Directory.

As you might have guessed, we’re feeling lazy today. How can you gather the names of all the computers in your domain that are running Windows Server 2003? Search Active Directory.

Here’s a script that echoes back the names of all the computers in the fabrikam.com domain that are running Windows Server 2003:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://DC=fabrikam,DC=com' WHERE objectClass='computer' " & _
        "and operatingSystemVersion = '5.2 (3790)'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    Wscript.Echo "Computer Name: " & objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

As with all Active Directory search scripts, the key to this script lies in the SQL query. In this query, we’re looking for all the computers (objectClass='computer’) where the operating system version happens to be Windows Server 2003 (operatingSystemVersion = '5.2 (3790)'). We’re simply returning the names of all computers where the operatingSystemVersion attribute has a value of 5.2 (3790); that happens to be the way the version number for Windows Server 2003 is stored in Active Directory.

So how did we know that the version number for Windows Server 2003 is stored in Active Directory as 5.2 (3790)? Hey, come on, we’re the Scripting Guys; we know everything!

Well, OK: we opened up Active Directory Users and Computers and found a computer that we knew was running Windows Server 2003. We right-clicked the computer name, clicked Properties, and then looked on the Operating System tab to find the operating system version.

We can also use a script to grab all the operating systems and their version numbers from Active Directory. Unfortunately, Active Directory doesn’t support SQL’s SELECT DISTINCT query type, so we can’t pull out just the 2 or 3 or 4 operating systems in use; instead, we have to grab all the computers and their operating systems, and then look through the list to pull out the unique versions. But this might be a good exercise for you: see if you can find a way to put some sort of filtering into this script so it shows only the unique (distinct) operating system versions. (One hint: You might consider creating a Dictionary object and storing the versions there. That way, you could easily check to see if any one version is already in the Dictionary. If it is, then don’t add a duplicate entry.)

Oh, right; here’s the script:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = _
    "Select operatingSystem, operatingSystemVersion from " & _
        "'LDAP://DC=fabrikam,DC=com' where objectClass='computer' ORDER BY operatingSystem"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("operatingSystem").Value, _
        objRecordSet.Fields("operatingSystemVersion").Value
    objRecordSet.MoveNext
Loop


How Can I Get a List of All the Domain Controllers in My Domain?

Hey, Scripting Guy! Is it possible to get a list of all the domain controllers in my domain?

-- KT

Hey, KT. As a matter of fact, there are at least two ways to get a list of all domain controllers in your domain. The first way is pretty easy, but not guaranteed; depending on how you’ve set up Active Directory, you could miss a few of your domain controllers. The second way is a little bit more complicated, but barring any unforeseen circumstances, should always return a list of all your domain controllers.

Let’s take a look at the easy way first. By default, the Active Directory computer account for a domain controller is stored in the Domain Controllers OU. If that’s where all your domain controllers accounts are stored, then you can retrieve your list just by enumerating the computer accounts in that OU:

Set objOU = GetObject(“LDAP://ou=Domain Controllers, dc=fabrikam, dc=com”)
objOU.Filter = Array(“Computer”)
For Each objComputer in objOU
    Wscript.Echo objComputer.CN
Next

The preceding script binds to the Domain Controllers OU and applies a filter to ensure that only computer accounts are returned. A simple For Each loop then walks us through the collection of returned computer accounts, echoing the CN (Common Name) for each one.

So what’s wrong with this script? Well, maybe nothing. However, it’s possible that you have domain controller accounts located elsewhere in Active Directory; if so, this script won’t do you much good. Likewise, it’s possible that you might have other computer accounts (such as those for member servers) in the Domain Controllers; if so, this script will mistakenly identify those computers as domain controllers. That’s because the script is just looking for computer accounts, period.

So what’s a more sure-fire way to get a list of all your domain controllers? Well, if you’re a regular reader of Hey, Scripting Guy! then you probably already guessed the answer: search Active Directory.

We know, sometimes it sounds like “Search Active Directory” is our standard response to any question. But, hey, Active Directory is a veritable storehouse of information, and it only makes sense to tap into that storehouse any chance you get. You want a list of all the domain controllers in a domain? Then run this script:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objRootDSE = GetObject("LDAP://RootDSE")
strConfigurationNC = objRootDSE.Get("configurationNamingContext")

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://" & strConfigurationNC & "' WHERE objectClass='nTDSDSA'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Set objParent = GetObject(GetObject(objRecordset.Fields("ADsPath")).Parent)
    WScript.Echo objParent.CN
    objRecordSet.MoveNext
Loop

Ok, so it is a little more complicated, but it works and it will find all the domain controllers regardless of the location of their Active Directory accounts. The script starts out by binding to RootDSE and then connecting to the configuration naming context for the domain. (The configuration naming context – or configuration directory partition – holds information of global interest; for example, you’ll find things like the default configuration and policy information for all instances of a given service in the forest.)

From there we do a typical Active Directory search: we want to get the ADsPath for all nTDSDSA objects in the configuration naming context. For our purposes, nTDSDSA is short (in a roundabout way) for Directory System Agent, the software responsible – among other things – for providing access to the directory service. Which, of course, is exactly what a domain controller does.

Note, however, that a list of all the nTDSDSA objects is not the same thing as a list of all our domain controllers. The ADsPath to an nTDSDSA object tells us how to find that object in Active Directory; what it doesn’t tell us is the name of the computer that object is installed on (and if you have the nTDSDSA object installed, then you must be a domain controller). To determine the computer name, we have this crazy line of code:

Set objParent = GetObject(GetObject(objRecordset.Fields("ADsPath")).Parent)

What we’re doing here is first binding to an nTDSDSA object, and then immediately binding to that object’s Parent. In the case of the nTDSDSA object, the Parent object is the actual computer itself; in other words, binding to the nTDSDSA Parent binds us to the computer account (which, remember, has to be a domain controller, or it wouldn’t have the nTDSDSA object). At that point, we’ve finally reached a domain controller, and all we have to do then is echo back the CN for this computer. We then repeat the loop until we’ve handled all the nTDSDSA objects and – by extension – all the domain controllers in the domain.


How Can I List All the Attributes Used by the Computer Class in Active Directory?

Hey, Scripting Guy! How can I list all the attributes used by the Computer class in Active Directory?

-- KP

Hey, KP. Thanks for your question. In response, yes, it is true that the Kirkland Fire, the Colt League baseball team coached by one of the Scripting Guys, won the city championship this past weekend, nicely bookending the regular-season championship which the team had already clinched. And, yes, the final score of the title game really was 13-0. Anything else we can help you with?

Oh, right: your scripting question. (For those of you unfamiliar with scripting, it’s something we do to pass the time between baseball games.) Obviously one of the Scripting Guys isn’t exactly focused on doing his job today (though we don’t know why today should be an exception) but fortunately this is an easy question to answer: after all, one of the really cool things about ADSI (the scripting technology used to access Active Directory) is that you can write an ADSI script that will tell you what you can script using ADSI. For example, this script will return the names of all the attributes (properties) used by the Computer class:

Set objSchema = GetObject("LDAP://schema/computer")

Wscript.Echo "Mandatory attributes"

For Each strAttribute in objSchema.MandatoryProperties
    Wscript.Echo strAttribute
Next

Wscript.Echo

Wscript.Echo "Optional attributes"

For Each strAttribute in objSchema.OptionalProperties
    Wscript.Echo strAttribute
Next

The script begins by connecting to the schema for the local domain and then binding to the Computer class. (The schema, of course, is a master list that details all the classes and other objects that can be stored in Active Directory, along with all the properties and methods of those items.) When we do this, we get back an instance of the IADsClass object; as the name implies, IADsClass contains information about a particular Active Directory class. What if we wanted information about the User class? Then we would bind to that class instead:

Set objSchema = GetObject("LDAP://schema/user")

And if we wanted information about the organizationalUnit class we’d use this code:

Set objSchema = GetObject("LDAP://schema/organizationalUnit")

Yes, very cool.

Because IADsClass is a class, it has its own set of attributes, including these two: MandatoryProperties and OptionalProperties. MandatoryProperties represents the properties that each instance of the class (in this case, the Computer class) must have. When we run the script against Active Directory we get back the following list of mandatory properties:

cn
instanceType
nTSecurityDescriptor
objectCategory
objectClass
objectSid
sAMAccountName

Most of these properties are automatically assigned by Active Directory, but a couple of them – like the cn and the sAMAccountName are not. That means you have to assign these yourself when creating a new Computer object; if you leave out, say, the sAMAccountName, then your script will fail.

As you might expect, MandatoryProperties are stored as an array (because there can be – and will be – more than one mandatory property); therefore we use a For Each loop to cycle through all the properties and echo back each property name:

For Each strAttribute in objSchema.MandatoryProperties
    Wscript.Echo strAttribute
Next

That’s pretty much it. We do the same sort of thing for OptionalProperties (properties that a Computer object can have but doesn’t have to have) and we’re done.

Incidentally, when you look through the list of optional properties for a computer you’ll see all sorts of oddball items. Pager? Home phone number? How many computers have their own pager or home phone number? Believe it or not, that’s perfectly normal: as it turns out, the Computer class is derived from the User class. That means the Computer class has all the properties of the User class plus a few other properties – such as operating system version – specific to computers. If you see some crazy attributes associated with a computer – initials? secretary? – well, don’t worry about them. After all, as optional properties you don’t have to do anything with them if you don’t want to.

Hope that helps, KP. Now, who wants to hear more about the Kirkland Fire? Anybody? Hello? Hello?


How Can I Move a Computer From an Unknown OU into a Known OU?

Hey, Scripting Guy! How can I move a computer from an unknown OU into a known OU?

-- DB

Hey, DB. You know, a lot of people take a look at system administration scripting and think, “Well, it might be useful, but it sounds kind of boring.” After all, clearing event logs and changing user passwords are worthwhile endeavors, but they don’t compare too favorably to Halo 2 or Age of Mythology.

Ah, but the scenario you painted sounds far more interesting. You have renegade computers wandering through unknown OUs. Your mission: track down those computers and somehow bring them into a known OU. Now we’re getting somewhere: even Starcraft 2 didn’t have a level like that!

Best of all, that would allow the Scripting Guys to ditch the name Script Center and re-christen the place the System Administration Cheat Codes Center. And then instead of doling out boring old scripts we could hand out really cool cheat codes, codes that help you win the game. You know, cheat codes like this:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

objOU = GetObject("LDAP://ou=Finance,dc=fabrikam,dc=com")

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='computer' " & _
"AND Name='atl-ws-01'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
strADsPath = objRecordSet.Fields("ADsPath").Value
objOU.MoveHere strADsPath, vbNullString
objRecordSet.MoveNext
Loop

Much of this script – er, cheat code – deals with searching Active Directory. There’s a good reason for that: that’s the strategy we’re using in order to track down these computers, computers that could be in any OU. As you mentioned in your email, you know the computer name, you just don’t know the computer’s location in Active Directory. No problem: we’ll simply search for the computer with the specified name, retrieve the location (technically, the ADsPath) and go on from there.

Simple, right? But, then again, the best video games – like Centipede or the greatest video game of all, Burger Time – have always been simple.

We start out by creating a constant named ADS_SCOPE_SUBTREE and setting the value to 2; we’ll use that constant to tell the script that we want to search all of Active Directory, including all the OUs and sub-OUs. We then use this line of code to create an object reference to the Finance OU, the “known” OU where we want to move the computer:

objOU = GetObject("LDAP://ou=Finance,dc=fabrikam,dc=com")

Got that? Next we have a big block of code that configures and carries out our search. As usual, explaining all the whys and wherefores of an Active Directory search goes beyond what we can do in this little column; for more information, see our two-part Tales from the Script series Dude, Where’s My Printer? For now, we’ll just make note of the query we use in the search:

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='computer' " & _
        "AND Name='atl-ws-01'"

As you can see, all we’re doing is retrieving the ADsPath for any computer (objectCategory = 'computer') that has the Name atl-ws-01. When we call the Execute method the script will search Active Directory and return a collection consisting of all the computers that have the specified Name. (Because Names must be unique, we’ll have – at most – one item in the collection.)

Good question: why are we retrieving the ADsPath attribute? Well, the ADsPath attribute is somewhat similar to a UNC path: it provides an exact “address” we can use to locate the object in question. For example, atl-ws-01 might have an ADsPath similar to this:

LDAP://cn=atl-ws-01,ou=Research,dc=fabrikam,dc=com

Remember when we said that this computer was in an unknown location? Well, not anymore: now we know exactly where it is, and how to get to it.

Of course, that just means that we’ve conquered Level 1; to win the game we still have complete Level 2 and move the computer to the Finance OU. To do that we use this block of code:

Do Until objRecordSet.EOF
    strADsPath = objRecordSet.Fields("ADsPath").Value
    objOU.MoveHere strADsPath, vbNullString
    objRecordSet.MoveNext
Loop

What we’re doing here is setting up a Do Until loop that runs until we reach the end of the recordset (objRecordSet.EOF). Inside that loop we use this line of code to grab the ADsPath and store it in a variable named strADsPath:

strADsPath = objRecordSet.Fields("ADsPath").Value

And now we’re ready to move the computer, a task which is as simple as this:

objOU.MoveHere strADsPath, vbNullString

As you can see, we use the object reference to the Finance OU (objOU) and call the MoveHere method. MoveHere requires two parameters:

· strADsPath, the variable containing the ADsPath (location) of the computer atl-ws-01.

· vbNullString, a VBScript constant indicating a Null variable. By specifying a different CN you can use the second parameter of the MoveHere method to rename a computer as opposed to simply moving it. Because all we want to do is move the thing (retaining its old name) we simply pass a Null variable as the second parameter.

That’s basically all we have to do. We then call the MoveNext method to move to the next record in the collection. Because there won’t be a next record that means that EOF is true, and so we exit the loop and the script.

Note. By the way, don’t leave out the MoveNext method. If you do the script will lock up in an endless loop: it will try to move the first computer in the collection and then, because it hasn’t advanced to the next record, try to move the first computer in the collection a second time. And then a third time, and then ….

So there you have it, DB: a cool new game in which you hunt down lost computers and send them to a different OU. Sure, it doesn’t sound all that fun. But – nostalgia aside – it’s still way better than Space Invaders.


How Can I Prompt for a Computer Name and Then Delete That Computer Account From Active Directory?

Hey, Scripting Guy! How can I write a script that prompts the user for a computer name, then goes out and deletes that computer account from Active Directory?

-- PH

Hey, PH. As a general rule, technical writers never experience writer’s block. Why not? Well, writer’s block usually comes about because you can’t figure out how to get started; once you start writing you’re usually fine. Technical writers never experience writer’s block because technical writers never have any problem getting started. Are you writing about Active Directory? No problem; just start out by saying, “Active Directory is a technology that ….” Writing about Terminal Services? Here’s an opening sentence for you: “Terminal Services is a technology that …” Writing about Group Policy? “Group Policy is a technology – “ well, you get the idea.

For better or worse, however, the Scripting Guys aren’t like other technical writers. Instead of starting every column off by saying, “System administration scripting is a technology that …,” the Scripting Guys try to thrill and excite their readers by talking about fascinating subjects like Turducken Bowls and Scripting Dogs. That’s fine, except that, after awhile (this being, by unofficial count, the 718thHey, Scripting Guy! ever written), you run out of things to talk about.

Unless, of course, you happen to drive the Scripting Car.

OK, good point: at the moment nobody actually drives the Scripting Car; the Scripting Car just sits around like a big, dumb blob. (We’ve long heard that pets resemble their owners, but we had no idea that cars resemble their owners.) Two days ago the Scripting Car was toast. Yesterday the Scripting Car was alive and well. Today – well, you can probably guess where we stand today.

“It has to be a short somewhere,” said the mechanic after the Scripting Car was towed to the shop for the second time this week. “We just can’t figure out where. Let’s just hope it’s not in the injectors, though. You don’t want it to be in the injectors.”

He’s right: we definitely don’t want it to be in the injectors.

At the rate things are going, we’re likely to have enough Scripting Car stories to keep us in business for a long time to come. But how do you transition from a story about a broken Scripting Car to a script that can locate and delete a computer account in Active Directory? As it turns out, you don’t; you just show the script and call it good:

Const ADS_SCOPE_SUBTREE = 2

strComputer = InputBox("Please enter the computer name:", "Delete Computer Account")

If strComputer = "" Then
    Wscript.Quit
End If

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = "Select ADsPath From " & _
    "'LDAP://DC=fabrikam,DC=com' Where objectClass='computer'" & _
        " and Name = '" & strComputer & "'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    Set objComputer = GetObject(objRecordSet.Fields("ADsPath").Value)
    objComputer.DeleteObject (0)
    objRecordSet.MoveNext
Loop

Barring any more shorts (particularly one in the injectors, which we don’t want) we’ll try to explain how this script works. As PH noted, he’d like the script to prompt the user to enter the name of a computer. No problem; after defining a constant named ADS_SCOPE_SUBTREE we use this line of code to prompt the user to enter the name of a computer:

strComputer = InputBox("Please enter the computer name:", "Delete Computer Account")

As you can see, there’s nothing particularly fancy about that line of code. All we’re doing is calling the InputBox function and asking it to display an input box on the screen. This particular input box features the message Please enter the computer name: and the caption Delete Computer Account. Assuming the user types in a name and then clicks OK the typed-in name will be stored in the variable strComputer. If the user clicks Cancel strComputer will be equal to an empty string (“”); in turn, that triggers this block of code, which causes the script to terminate:

If strComputer = "" Then
    Wscript.Quit
End If

Once we know the name of the computer account to be deleted our next step is to search Active Directory and locate the account in question. As usual, we won’t discuss the ins and outs of Active Directory search scripts in any detail today; that’s what our two-part Tales from the Script series is for. However, we will take a quick peek at the SQL query used to search for this computer:

objCommand.CommandText = "Select ADsPath From " & _
    "'LDAP://DC=fabrikam,DC=com' Where objectClass='computer'" & _
        " and Name = '" & strComputer & "'"

Again, this is pretty straightforward: all we’re doing is searching the fabrikam.com domain for all items that have an objectClass equal to computer and a Name equal to the value of the variable strComputer. Because computer Names must be unique in a domain, this query lets us locate the very account that we want to delete.

Because we’re using a search script, any accounts that meet our criteria will be returned as part of a recordset; that’s true even though there can be, at most, only one such account. Because of that, we need to set up a Do Until loop that runs until we’ve looked at each and every item in the recordset. Or, as we high-tech types like to say, until the recordset’s EOF (end-of-file) property is True:

Do Until objRecordSet.EOF

So what exactly do we do inside this loop? To begin with, we take the value of the item’s ADsPath property and use it (and the GetObject method) to bind to the actual computer account in Active Directory:

Set objComputer = GetObject(objRecordSet.Fields("ADsPath").Value)

And yes, you’re right: this works because the ADsPath attribute represents the computer’s “address” within Active Directory. For example:

LDAP://CN=atl-fs-01, CN=Computers, dc=fabrikam, dc=COM

After we make the connection we then call the DeleteObject method and delete the account:

objComputer.DeleteObject (0)

Note. Why is the (0) parameter tacked on to the end of the DeleteObject method? Well, that’s a required parameter even though, at the moment at least, 0 is the only allowed value. The parameter is still required, however, because additional values might be allowed “in the future.” If you ever find yourself musing, “I wonder if the future has arrived yet,” just check to see if DeleteObject accepts any values other than 0. If it does, then the future has arrived.

One important thing to keep in mind here: the moment you call DeleteObject the computer account will be deleted. There’s no prompting on the order of “Are you sure you want to delete this account?”, although you could easily add that code in yourself if you so desire.

How could you add in that code? Well, here’s one way:

intAnswer =  Msgbox("Are you sure you want to delete this computer?", _
    vbYesNo, "Delete Computer")

If intAnswer = vbYes Then
   objComputer.DeleteObject (0)
End If

After deleting the account we then call the MoveNext method to move to the next item in the recordset. Because there won’t be a next item in the recordset that means the EOF property will be True and we’ll automatically exit the Do Until loop.

And that should do it, PH. We wish we had a snappier ending for today’s column other than, “And that should do it, PH.” Unfortunately, we’re having problems bringing things to a close lately. We seem to have no trouble with beginnings; endings are a different story.

That said, let’s just hope that this story doesn’t end with, “Bad news, Scripting Guy who writes this column: this is in the injectors. That’s something you don’t want.” No, we definitely don’t want that.


How Can I Return a List of All My Computers Except Those in a Specified OU?

Hey, Scripting Guy! I need to get back a list of all the computers in my domain, except for the computers which are in our Test Lab OU. How do I do that?

-- AM

Hey, AM. This seems to be one of the hot new trends in the IT world: in the past two weeks we’ve gotten three similar emails, all involving administrators who want to exclude a particular OU when listing all the computers in their domain. In this case, it appears you’re not interested in computers housed in your test lab; in the other two cases, we had an admin who wanted to exclude developer machines, and an admin who didn’t want domain controllers showing up in the list. Regardless, the basic idea is the same: you want a list of all your computers except for those one specific OU.

If you’re an avid reader of Hey, Scripting Guy! (and who isn’t?) then you know that 9 times out of 10 our answer to a question is, “Search Active Directory.” That’s going to be our answer today as well, although we must confess that we’re not 100% satisfied with that answer (you’ll see why in a moment). At first glance, you might think this would be an easy search to carry out in Active Directory; after all, you’d have to do nothing more than include a WHERE clause that tells the script to skip any computers found in the Test Lab OU. In other words:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE OU <> 'TestLab OU' AND " _
        & "objectCategory='computer'"

This is a great solution, except for one problem: Active Directory computer accounts don’t have a property named OU; that means you can’t use the OU in a WHERE clause. So much for Plan A.

Of course, you might then think, “Hey, what about the Distinguished Name (DN)?” After all, the DN for a computer (e.g., CN=atl-ws-01,OU=Test Lab OU,DC=fabrikam,DC=com) contains the name of the OU where the computer account resides. Couldn’t we just do a wildcard search on the Distinguished Name, something similar to this:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE distinguishedName <> "  _
        " '*OU=Finance' AND objectCategory='computer'"

Alas, this won’t work, either. As it turns out, the DN is a “constructed” attribute. The DN isn’t actually stored in Active Directory, it’s constructed on-the-fly any time you request it. Thus you can’t do a wildcard search on the distinguishedName attribute. And don’t bother asking about the ADsPath attribute, which also contains the OU name; you can’t do a wildcard search on the ADsPath, either.

In fact, as far as we know, the only way to do this is to conduct a search that returns a list of all your computers. When you get that list back, you’re probably going to do something with it (for now, we’ll assume you just want to echo the computer names to the screen). Before anything is done to a computer in the list, however, our script will check the DN to see if the target string (for example, OU=Test Lab OU) can be found. If it is, the script will simple skip that computer and go on to the machine in the list. If the target string isn’t found, then the script will echo the computer name.

Maybe this will make more sense if you see the actual script:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name, distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' " _
        & "WHERE objectCategory='computer'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    intTestOU = InStr(objRecordSet.Fields("distinguishedName").Value, "OU=Test Lab OU")
    If intTestOU = 0 Then
        Wscript.Echo objRecordSet.Fields("Name").Value
    End If
    objRecordSet.MoveNext
Loop

The lines of code we care about are these:

intTestOU =
InStr(objRecordSet.Fields("distinguishedName").Value, "OU=Test
Lab OU")
If intTestOU = 0 Then
Wscript.Echo objRecordSet.Fields("Name").Value
End If

What we’re doing here is using the InStr function to see if the string OU=TestLab OU can be found anywhere within the computer’s Distinguished Name. If the string is found, then intTestOU gets set to the character position where the string begins. For example, in the DN CN=atl-ws-01,OU=Test Lab OU,DC=fabrikam,DC=com, intTestOu would be set to 14, because our target phrase begins at the 14th letter in the string.

But what if intTestOU equals zero? That can only mean one thing: the target phrase wasn’t found. In turn, that must mean this particular computer is not in the Test Lab OU, so we go ahead and echo the computer name. Thus we end up echoing the names of all the computers except those in the Test Lab OU.

Like we said, this approach does the job, although it lacks a certain elegance. But, hey, style isn’t everything, right?

Alternatively, you might consider storing OU names in an unused computer account attribute; for example, you could record the OU name in the description attribute. That way, you could search on that attribute, using a query similar to this:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='computer' " & _
        "AND description<>'Test Lab OU'"

This works great; you’ll just have to remember to change the value of the description attribute should you ever move the computer to a new OU.


How Can I Use Windows PowerShell to Get a List of All My Computers?

Using Windows PowerShell, how can I retrieve a list of all the computer accounts in my domain?

-- TE

Hey, TE. You know, several years ago the Scripting Guy who writes this column was playing basketball with one of his brothers. Because there were a total of 5 people at the park they had to play 2-on-2, with one person forced to sit out. After a short while a sixth person showed up and asked if he could play. He was a little guy, very quiet and unassuming, but for some reason the Scripting Guy who writes this column took one look at him and thought, “I bet he’s good.”

“I’ll guard the new guy,” said the Scripting Brother.

“I don’t know,” said the Scripting Guy who writes this column. “Maybe we should play a zone or something.”

“No,” said the Scripting Brother. “I want to guard him.”

So how did it go? Let’s put it this way: the Scripting Brother now knows, better than anyone, what the old saying about being careful what you wish for really means. As it turned out, the newcomer may very well have been the best basketball player in the history of the world. He drove past the Scripting Brother for easy lay-ins. He soared over the Scripting Brother to grab rebounds or bury jump shots. He completely and totally dominated both the game and the Scripting Brother.

About halfway through the game the Scripting Brother turned to the Scripting Guy who writes this column and said, “Do you want to switch guys?”

“No thanks,” said the Scripting Guy who writes this column, who was guarding a guy that probably hadn’t touched the ball more than 2 or 3 times the entire game. “I’m fine.”

Now, admittedly, writing a script that retrieves a list of computer accounts from Active Directory isn’t quite as difficult as guarding an unguardable player in basketball. Nevertheless, we just wanted you to know what you’re getting yourself into, TE. Remember, you asked for this:

$strCategory = "computer"

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.Filter = ("(objectCategory=$strCategory)")

$colProplist = "name"
foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}

$colResults = $objSearcher.FindAll()

foreach ($objResult in $colResults)
    {$objComputer = $objResult.Properties; $objComputer.name}

The truth is, this script is nowhere near as bad as it looks; the problem is that it looks pretty darn intimidating. But that’s OK; after all, it is our job to try and explain how things like this work.

With an emphasis on the word try, of course.

We start out simply enough, assigning the value computer to a variable named $strCategory. We then use this line of code to create a new instance of the .NET Framework class System.DirectoryServices.DirectoryEntry. Because we didn’t specify any additional parameters when creating this instance our object reference $objDomain will automatically connect us to the root of our Active Directory domain. Neat, huh?

Incidentally, here’s the line of code we were talking about:

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

After we connect to the domain we then need to create an instance of the System.DirectoryServices.DirectorySearcher class; that’s the object we’ll use to conduct our Active Directory search. After creating the object we then use these two lines of code to assign values to the DirectorySearcher SearchRoot and Filter properties:

$objSearcher.SearchRoot = $objDomain
$objSearcher.Filter = ("(objectCategory=$strCategory)")

As you probably figured out, the SearchRoot property simply tells DirectorySearcher where to begin searching. We want to search the entire domain so we set SearchRoot to the object reference $objDomain. In turn, the Filter property enables us to target a specific subset of the items found in Active Directory. Because the only thing we care about are computers, we tell the Filter property to bring back only those objects where the objectCategory attribute is equal to the variable $strCategory (which, as you recall, is equal to computer.)

Note. A detailed explanation of how to search Active Directory using Windows PowerShell falls way outside the scope of this little column. We will note, however, that the Filter property requires the LDAP search syntax; you can’t use the more familiar SQL-style syntax here. For more information, see the DirectorySearcher documentation on MSDN.

Our next step is to specify which attributes and attribute values we want our search to bring back. (If you don’t specify a so-called “property list” you’ll get back all the attributes and attribute values, which is not only more information than you need, but probably more data than you want to have clogging up your network.) These two lines of code add the name attribute to the list of property values we want back:

$colProplist = "name"
foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}

We should point out that property lists need to be constructed as arrays. Why do we care about that? Well, suppose we wanted to get back both the name and the cn. In that case we’d have to assign the two values to $colProplist using syntax like this:

$colProplist = "name", "cn"

And yes, that is an array: in Windows PowerShell you can create an array just by assigning multiple values (with individual values separated by commas) to a variable.

After we’ve created an array our next chore is to use the PropertiesToLoad method to add those properties to our DirectorySearcher. To do that we set up a For Each loop to loop through all the items in the array, adding each item to the DirectorySearcher:

foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}

Note. What’s that? You don’t know how to create a For Each loop in Windows PowerShell? If that’s the case, you should take a look at our Task-Based Introduction to Windows PowerShell.

Now, at long last, we’re ready to search Active Directory. To do that we call the FindAll() method, storing the data the search brings back in a variable named $colResults:

$colResults = $objSearcher.FindAll()

The only thing left to do now is set up a second For Each loop to loop through all the items (computers) in the collection $colResults. For each item (computer) in the collection we use this snippet of code to store the item Properties in a variable named $objComputer; we do that because, even though we asked for only the name, by default we also get back the computer’s ADsPath, something which can make our output difficult to read:

$objComputer = $objResult.Properties

If we don’t mind difficult-to-read output we could just echo back the value of $colResults and call it good:

$colResults

As soon as the computer Properties have been stashed in $objComputer we can then use this command to echo back just the computer name:

$objComputer.name

That’s going to give us output that looks something like this:

atl-dc-01
atl-dc-02
atl-dc-03
atl-dc-04

Which is exactly what we wished for in the first place.

Like we said, this script is a little complicated, although that’s due in part to all the $’s and curly braces and other unusual characters you have to use in a Windows PowerShell script. Complicated or not, though, we know that there’s a lot of interest in using Windows PowerShell to work with Active Directory, so we’ll see what we can do about that in the future.

In the meantime, though, we’re off to play some basketball. Hmmm, there’s a quiet, unassuming little guy over there; we’ll guard him. And no, we’re not worried: don’t they always say that you never have to be careful what you wish for, because things always turn out great?

Or something like that anyway. We’ll ask the Scripting Brother the next time we see him.


How Can I Determine Whether a Domain is in Mixed Mode or Native Mode?

Hey, Scripting Guy! How can I determine whether a domain is in mixed mode or native mode?

-- WW

Hey, WW. When you listen to a Seattle Mariners game on the radio one of the regular features is “Stump the Broadcasters.” The premise to Stump the Broadcasters is that listeners send in a baseball question to be asked of the Mariners broadcasters. If you can “stump the broadcasters” (that is, if you ask a question that the broadcasters can’t answer) then you win a fabulous prize. Pretty simple, huh?

The only problem with Stump the Broadcasters is that people send in such silly questions: “Who holds the major league record for most career home runs?” To be honest, you just want to grab these people, shake them, and say, “What’s wrong with you? Don’t you want to win the fabulous prize? Even people who don’t know anything about baseball know that Hank Aaron holds the record for most career home runs. Holy smokes!”

Note. Here’s a better question: “Who holds the record for the fewest pitches thrown in a complete game?” The answer: the Boston Braves’ Red Barrett, who needed just 58 pitches (and only one hour and 15 minutes) to defeat the Cincinnati Reds 2-0 back in 1944. But you probably shouldn’t try this question, either. After all, if the Mariners broadcasters read this column every day (as they almost surely do), well, now they know the answer, too.

If you want to stump the Mariners broadcasters you need to ask about something no one in their right mind could possibly know, something like “Who holds the record for the fewest pitches thrown in a complete game?” It’s a little different when you try to Stump the Scripting Guys, however. Admittedly, no one in their right mind knows how to use a script to determine whether an Active Directory domain is in mixed mode or native mode. But who said that the Scripting Guys were in their right minds?

Set objDomain = GetObject("LDAP://dc=fabrikam,dc=com")

If objDomain.nTMixedDomain = 0 Then
    Wscript.Echo "Domain is in native mode."
Else
    Wscript.Echo "Domain is in mixed mode."
End If

As you can see, it’s actually very easy to determine whether a domain is in native mode or mixed mode. To begin with, you simply connect to the domain itself, something we do with this line of code:

Set objDomain = GetObject("LDAP://dc=fabrikam,dc=com")

Why do we connect to the domain? That’s easy: the domain object happens to have an attribute named nTMixedDomain that tells us whether the domain is in mixed mode or native mode. If nTMixedDomain is equal to 0 then the domain is in native mode (which basically means that all domain controllers must be running Windows 2000 or Windows Server 2003). If nTMixedDomain is equal to 1 then the domain is in mixed mode, which means that Windows NT 4.0 computers can function as domain controllers. How do we check and report back the value of the nTMixedDomainAttribute? By using this block of code:

If objDomain.nTMixedDomain = 0 Then
    Wscript.Echo "Domain is in native mode."
Else
    Wscript.Echo "Domain is in mixed mode."
End If

Incidentally, the nTMixedDomain attribute is a read-write attribute: if your domain is currently in mixed mode you can use a script to change the value to 0 (and thus put the domain into native mode). Are we going to show you an example of how to do that? No, we’re not. Why? Well, moving from mixed mode to native mode is an irreversible process; because of that we don’t want to give you a script that you might accidentally run, and thus accidentally change the domain mode. But that’s no big deal: now that you know about the nTMixedDomain attribute, and now that you know this attribute is read-write, you should be able to put together a script to move you from mixed mode to native mode without too much trouble.

As long as we’re on the subject, here’s another attribute you might find interesting: msDS-Behavior-Version. If you connect to a Windows Server 2003 domain and retrieve the value of this attribute it will tell you the domain functional level (see this Knowledge Base article for more details). If you connect to the Partitions container and retrieve the value of this attribute it will tell you the forest level setting (discussed in that same Knowledge Base article). We won’t explain the following code in any detail, but this script does return the forest level setting for a Windows Server 2003 forest:

Set objRootDSE = GetObject("LDAP://rootDSE")

Set objPartitions = GetObject("LDAP://CN=Partitions," & _
    objRootDSE.Get("configurationNamingContext"))

If objPartitions.[msDS-Behavior-Version] = 0 Then
    Wscript.Echo "This is a mixed-level forest."
ElseIf objPartitions.[ msDS-Behavior-Version] = 1 Then
    Wscript.Echo "This is a Windows Server 2003 interim-level forest."
Else
    Wscript.Echo "This is a Windows Server 2003 forest."
End If

Note. Why did we put square brackets around the attribute name ([msDS-Behavior-Version])? You obviously haven’t read this previous Hey, Scripting Guy! column, have you?

We hope that helps, WW. Now here’s another question for you, and for everyone else out there: From1967 to 1983, Aurelio Rodriguez played third base for several major league baseball teams, most notably the Detroit Tigers. During his tenure there was one thing that made Aurelio Rodriguez absolutely unique among major league ball players. What was that? (Answer below)

Answer. During his time in the big leagues Aurelio Rodriguez was the only major league ball player whose first name contained all five vowels: a-e-i-o-u. Raise your hand if you got that one right.

Wait a second: everyone got that one right? Hmmm ….


How Can I Get a List of All the Domains in a Forest?

Hey, Scripting Guy! How can I get a list of all the domains in a forest?

-- NS

Hey, NS. Any time you want information about Active Directory your best bet is to go right to the source: simply ask Active Directory to provide you with this information. Here’s a sample script that binds to the global catalog for the domain Fabrikam.com and searches for all the domain objects:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name FROM 'GC://dc=fabrikam, dc=com' WHERE
objectCategory='domain'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Wscript.Echo objRecordSet.Fields("Name").Value
objRecordSet.MoveNext
Loop

If you’ve had some experience with Active Directory search scripts this particular piece of code should look very familiar; if you haven’t had much (or any) experience with Active Directory search scripts we recommend you take a look at the Scripting Guys Poking Your Nose into Active Directory webcast. (And it’s worth doing, too, simply because searching Active Directory is such a powerful and useful tool. Besides, Scripting Guys webcasts are always worth watching!)

The only thing to be careful of occurs when we bind to Active Directory. Because we want to search the global catalog we need to use the global catalog provider. Thus instead of specifying LDAP: in our binding string we specify GC:, like so:

"SELECT Name FROM 'GC://dc=fabrikam, dc=com' WHERE objectCategory='domain'"

Other than that, however, this is a pretty straightforward little script, and should return a list of all the domains in your forest.


How Can I Access Active Directory on a Specific Domain Controller?

Hey, Scripting Guy! How can I access Active Directory on a specific domain controller?

-- SD

Hey, SD. You know, one of the cool things about Active Directory is that you usually don’t need to access the directory service on a specific domain controller. Because Active Directory information is replicated between all the domain controllers in a domain, that information will be the same regardless of which domain controller you bind to. Want to know which department a user is in? Then you can just use “serverless binding” like this:

Set objUser = GetObject _
    ("LDAP://CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com")
Wscript.Echo objUser.department

Notice that we don’t bind to a specific domain controller; instead we just bind to Active Directory and let the directory service choose a domain controller for us. It makes no difference: after all, the user’s department will be the same on all domain controllers.

Of course, if that’s the case then why in the world would you ever want to bind to a specific domain controller? Well, here’s one reason. While it’s true that most Active Directory information is replicated between all the domain controllers in a domain there are a few attributes that are not replicated. For example, take the badPwdCount attribute, which tracks the number of times a user has tried to log on using an incorrect password. This value is not replicated between domain controllers; if you need to check this value for a user you’ll have to individually bind to each domain controller and retrieve the value from that machine. You can then add all these individual values to determine the total number of times that the user typed in an incorrect password when trying to log on to the domain.

That’s just one example of why you might want to bind to an individual domain controller instead of binding to any old domain controller.

So how do you bind to a specific domain controller? All you do is include the computer name in your Active Directory binding string. For example, this script binds to Active Directory on the computer atl-dc-01:

Set objUser = GetObject _
    ("LDAP://atl-dc-01/CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com")
Wscript.Echo objUser.badPwdCount

Want to bind to Active Directory on the computer red-dc-22? Then use a script like this:

Set objUser = GetObject _
    ("LDAP://red-dc-22/CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com")
Wscript.Echo objUser.badPwdCount

That’s all you gotta do.


How Can I Change the User and Computer Account Description Attributes Each Time a User Logs On?

Hey, Scripting Guy! How can I change the user and computer account Description attributes each time a user logs on? I’d like the Description to indicate who logged on, and when.

-- GG

Hey, GG. This is actually a pretty good idea. One question we get asked all the time is this: “How can I get a list of all my computers and who’s logged on to them?” The truth is, there really isn’t a good way to do that. The answer to today’s question is a nice solution to that problem: all you’d have to do is query Active Directory and return a list of computers (or users) and the Description attribute. That would give you your list right there.

A couple caveats before we begin. First, you’ll probably want to configure this as a logon script; you’re on your own for that. Second, you’ll have to make sure that all your users have permissions to change the Description attribute for both their own account and for the computer account. That’s likely the case anyway, but you’ll need to double-check it before implementing this solution.

OK, let’s take a look at the script that – each time a user logs on – changes the Description attribute for both the user account and the computer account in Active Directory:

Set
objSysInfo = CreateObject("ADSystemInfo")

Set objUser = GetObject("LDAP://" & objSysInfo.UserName)
Set objComputer = GetObject("LDAP://" & objSysInfo.ComputerName)

strMessage = objUser.CN & " logged on to " & objComputer.CN
& " " & Now & "."

objUser.Description = strMessage
objUser.SetInfo

objComputer.Description = strMessage
objComputer.SetInfo

We begin by creating an instance of ADSystemInfo, an Active Directory class that returns a lot of useful information about the current user and computer. (For a script showing all the data that can be returned using ADSystemInfo click here.) We then use the UserName and ComputerName properties of this object to create a pair of object references, one (objUser) that binds us to the logged-on user’s Active Directory account, the other (objComputer) that binds us to the computer’s Active Directory account.

Next we create the string that we want to write to the Description attribute. Obviously you can write anything you want to this attribute; we chose to combine the user’s CN, the computer’s CN, and the current date and time (plus a few additional words just to turn the thing into a sentence). That’s what this code is all about:

strMessage = objUser.CN & " logged on to " & objComputer.CN & " " & Now & "."

Ultimately, that results in a description similar to this:

Ken Myer logged on to atl-ws-01 4/25/2005 8:04:54 AM

So how do we actually get the Description set to this string? Well, to set the Description for the logged-on user we use these two lines of code:

objUser.Description = strMessage
objUser.SetInfo

All this does is configure the Description on the local cache copy of the user account, and then use the SetInfo method to write that information back to Active Directory. (Have no idea what we mean by the local cache copy? See this section of the Microsoft Windows 2000 Scripting Guide for more information.) We then use similar code to set the Description for the computer account:

objComputer.Description = strMessage
objComputer.SetInfo

One thing you might want to do as well is include a logoff script that either erases the Description or changes the message (e.g., Ken Myer logged on to atl-ws-01 4/25/2005 8:04:54 AM). That way you can not only keep track of which users are logged on to which computers, but you can also keep track of which users are not logged on at all (as well as which computers have no current user).

Good idea, GG. We only wish we’d thought of it!


How Can I Convert a DN to this Format: finance

How Can I Convert a DN to this Format: finance.fabrikam.com/Users?

Hey, Scripting Guy! How can I convert a DN to this format: finance.fabrikam.com/Users?

-- CW

Hey, CW. You know, this is a pretty good question; in fact, we should have used it for the Winter Scripting Games. Ah, but, on second thought, maybe it’s a good thing we didn’t use it. Why not? Well, because all the Scripting Games competitors could find the answer right here:

On
Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

strDN = objUser.distinguishedName

arrDN = Split(strDN, ",")

For i = 0 to UBound(arrDN)
intLength = Len(arrDN(i))
intCounter = intLength - 3
arrDN(i) = Right(arrDn(i), intCounter)
Next

For i = 1 to UBound(arrDN) - 1
strNewName = strNewName & arrDN(i) & "."
Next

intLastItem = UBound(arrDN)
strNewName = strNewName & arrDN(intLastItem) & "/" &
arrDN(0)

Wscript.Echo strNewName

OK, yes: it’s a little complicated. But really, it’s not as bad as it first looks, something you’ll soon see for yourself.

To begin with, CW, it appears your example referenced an OU, something with a DN (distinguished name) like this:

ou=Users,ou=finance,dc=fabrikam,dc=com

We thought we should point out that we’re using a user account as our example. However, that shouldn’t matter: this technique will work with any DN, regardless of the underlying object type. We went with the user account simply because it provided a way for us to offer up a sample script that anyone could use as-is, without having to change a thing.

You’re welcome.

The script starts off by creating an instance of the ADSystemInfo object; this is an ADSI object that returns information about the logged-on user. We grab the value of the UserName property and store it in a variable named strUser, then use that value to bind to the actual user account in Active Directory. All that takes just two lines of code:

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

Once we’ve made a connection to the user account we can then get the value of the distinguishedName attribute, something we store in a variable named strDN. That DN will look something like this:

cn=KenMyer,ou=finance,dc=fabrikam,dc=com

OK, that part was easy. But now what do we do?

Here’s what we do. First, we issue this command:

arrDN = Split(strDN, ",")

What this does is split our DN into an array named arrDN. By splitting the DN on the comma we end up with an array that looks like this:

cn=KenMyer
ou=finance
dc=fabrikam
dc=com

In turn, that brings us to this block of code:

For i = 0 to UBound(arrDN)
    intLength = Len(arrDN(i))
    intCounter = intLength - 3
    arrDN(i) = Right(arrDn(i), intCounter)
Next

At the moment, arrDN consists of all the items we need to construct our new name format. However, there’s a problem: the first three characters in each of these elements (e.g., cn=KenMyer) are characters we don’t want in our new name. (In other words, we want to chop the cn= off cn=KenMyer). That’s what the preceding block of code does. For each item in the array we:

· Use the Length function to determine the length of the item (that is, the number of characters in the string).

· Subtract 3 from the length. Why 3? Because that’s the number of characters we want to get rid of.

· Use the Right function to extract all but the first three letters of the string. That means that, working backwards, we take the characters KenMyer but don’t take the characters cn=. We then assign this truncated value to the value of the array item.

That should make more sense when you see what our array looks like now:

KenMyer
finance
fabrikam
com

As you can see, we’re getting closer.

Next we take the middle items in the array (excluding the first and last items) and construct a string that looks like this:

finance.fabrikam.

Here’s how we do that:

For i = 1 to UBound(arrDN) - 1
    strNewName = strNewName & arrDN(i) & "."
Next

Notice that in this For Next loop we start with the second item in the array (remember, the first item in an array is always item 0, making the second item item 2). Inside the loop we build up a string value named strNewName that consists of the value of each array item plus a period tacked on to the end. Note again that we’re not using all the array items: in addition to starting with item 2 we also stop before we get to the last item. Why? Well, we don’t want to append a period to the very last item; working with all the items except the last one makes it easy to avoid putting a period at the end of that last item. When this block of code finishes we’ll have a string that looks like this:

finance.fabrikam.

We’re now so close we can taste it. To finish this off all we have to do is add three things to the end of the string:

· The last item in the array (we can easily determine the item number of the last item by using the UBound function).

· The / character.

· The user’s CN (which happens to be the first item in our array).

Sounds hard, doesn’t it? And yet, all those chores can be achieved with just two lines of code:

intLastItem = UBound(arrDN)
strNewName = strNewName & arrDN(intLastItem) & "/" & arrDN(0)

And then we simply echo back our new name and call it a day:

finance.fabrikam.com/KenMyer

Cool, huh?


How Can I Delete an Active Directory Object that Contains Other Objects?

Hey, Scripting Guy! How can I delete an Active Directory object that contains other objects?

-- TS

Hey, TS. Ah, yes, the dreaded “objects that contain other objects” dilemma. Is there a way to delete an Active Directory object that contains other objects? Let’s see if we can find one.

For those of you new to Active Directory scripting, deleting an object (a user account, a computer account, whatever) typically takes just two lines of code. For example, this little script deletes an object from Active Directory; it simply binds to the domain root (dc=fabrikam, dc=com) and then uses the Delete method to delete an OU (organizationalUnit) named Finance:

Set objDomain = GetObject("LDAP://dc=fabrikam,dc=com")
objDomain.Delete "organizationalUnit", "ou=Finance"

Well, check that: it deletes an OU named Finance provided that the OU is empty; that is, the OU will get deleted as long as it doesn’t contain any other objects. If Finance does contain other objects (for example, a bunch of user accounts) then the OU will not be deleted; instead, you’ll get this somewhat cryptic error message:

An invalid dn syntax has been specified.

Why is the syntax invalid? Well, the Finance OU is a container (an object that can contain other objects), and the Delete method cannot delete a container unless that container is empty. Because the Finance OU isn’t empty it’s not valid the call the Delete method. As a result, the syntax is invalid, and the script fails.

So what do we do about that? Well, we could get a list of all the objects stored in the Finance OU, delete each of those objects one-by-one, and then delete the Finance OU. Or we could use this script instead:

Set objOU = GetObject("LDAP://OU=finance,DC=fabrikam,DC=com")
objOU.DeleteObject(0)

Yes, it looks very similar, but there are a couple differences. To begin with, we bind directly to the Finance OU itself; in our previous script we bound to the parent object (in that case, the domain root). Second, we don’t use the Delete method; instead, we use the DeleteObject method. Unlike its counterpart, DeleteObject will delete an object regardless of whether or not it contains other objects. In one fell swoop, the Finance OU and everything in it will be deleted.

Note, too that we passed DeleteObject a single parameter: 0. This is a required parameter; leave it out and the call to DeleteObject will fail. Interestingly enough, however, 0 is the only possible value that can be passed to DeleteObject; there are no options other than 0, which means, “Go ahead and delete everything.” Why? To tell you the truth we don’t know, just like we don’t know for sure why there are two methods (Delete and DeleteObject) for deleting things from Active Directory. But you know what they say: when it comes to certain things – like what they put into hot dogs and how they delete objects from Active Directory – well, maybe you’re better off not knowing. Just use DeleteObject and delete away.


How Can I Determine an Attribute’s Data Type and Whether It is Single- or Multi-Valued?

Hey, Scripting Guy! How can I query the Active Directory schema for an attribute’s data type and whether it is single-valued or multi-valued?

-- MS

Hey, MS. You know, this turned out to be one of those questions that drive us crazy: after all, it seemed like it would be so easy to answer. As you’ve probably already figured out, however, it turned out to be a bit more complicated than we expected.

Why? Well, it’s easy enough to bind to any Active Directory attribute and find out whether it’s single-valued or multi-valued; in fact, each attribute has its own attribute (isSingleValued) that tells you that very thing. We also knew that there was an attribute – ADsType – that tells you the data type of an attribute. Case closed, right?

Well, no, unfortunately not. It turns out that you can’t get the ADsType when binding to an attribute in the schema; instead, you have to retrieve the property from an actual object (such as a user account) and then you can determine the data type. That sounded way too complicated: you bind to the attribute in the schema to get one bit of information, then have to bind to an actual object to get the second bit of information. Not very elegant, to say the least. (And we Scripting Guys pride ourselves on our elegance.)

So we started digging through the Active Directory schema, looking for something that would tell us the data type of the object. Eventually we stumbled upon an attribute we had never heard of: oMSyntax, which returns the syntax value assigned to an attribute. There are some slight differences between ADsType and oMSyntax, but for script writers they don’t really matter; the information oMSyntax returns will likely tell you everything you need to know.

At least we hope so, because that’s the route we ended up taking:

Set objAttribute = GetObject _
    ("LDAP://cn=telephone-Number,cn=schema,cn=configuration,dc=fabrikam,dc=com")

Wscript.Echo "Single-valued: " & objAttribute.isSingleValued

Select Case objAttribute.oMSyntax
    Case 1
        Wscript.Echo "Datatype: " & "Boolean"
    Case 2
        Wscript.Echo "Datatype: " & "Integer or Enumeration"
    Case 4
        Wscript.Echo "Datatype: " & "String (Octet or SID)"
    Case 6
        Wscript.Echo "Datatype: " & "String (Object-Identifier)"
    Case 10
        Wscript.Echo "Datatype: " & "Integer or Enumeration"
    Case 18
        Wscript.Echo "Datatype: " & "String (Numeric)"
    Case 20
        Wscript.Echo "Datatype: " & "Case-Ignore String (Teletex)"
    Case 27
        Wscript.Echo "Datatype: " & "Case-Sensitive String"
    Case 19
        Wscript.Echo "Datatype: " & "String (Printable or IA5)"
    Case 22
        Wscript.Echo "Datatype: " & "String (Printable or IA5)"
    Case 23
        Wscript.Echo "Datatype: " & "String (UTC-Time or Generalized-Time)"
    Case 24
        Wscript.Echo "Datatype: " & "String (UTC-Time or Generalized-Time)"
    Case 64
        Wscript.Echo "Datatype: " & "String (Unicode)"
    Case 65
        Wscript.Echo "Datatype: " & "Large Integer"
    Case 66
        Wscript.Echo "Datatype: " & "String (NT Security Descriptor)"
    Case 127
        Wscript.Echo "Datatype: " & "Object"
    Case Else
        Wscript.Echo "Datatype: " & "Undefined"
End Select

The script begins by binding to the desired attribute (in this case, telephone-Number) in Active Directory:

Set objAttribute = GetObject _
    ("LDAP://cn=telephone-Number,cn=schema,cn=configuration,dc=fabrikam,dc=com")

Notice two things here. First, we use the CN for the attribute (telephone-Number) rather than the LDAP display name (telephoneNumber). That’s important to keep in mind because the odds are you’re used to using the LDAP display name in your ADSI scripts.

Note. If you don’t know the CN for a particular attribute, take a look at the Active Directory Schema Reference on MSDN. Generally speaking, you can construct the CN just by inserting a hyphen between the individual words in the LDAP display name: thus, telephoneNumber becomes telephone-Number. That’s not guaranteed to work in all cases, however, so refer to the Schema Reference as needed.

Second, we bind to the schema itself, which is located in the Configuration container. That’s not hard, it’s just different from most of your ADSI scripts. So, again, just something to keep in mind as you play around with this.

From there we retrieve and report the value of the isSingleValued attribute:

Wscript.Echo "Single-valued: " & objAttribute.isSingleValued

Like we said: that part was simple. And, really, the next part is pretty simple, too: we just use a Select Case statement to determine the data type of the attribute (based on the value of oMSyntax). Our Select Case statement looks like this:

Select Case objAttribute.oMSyntax

Inside that Select Case block we simply look for the possible values of oMSyntax and then echo a corresponding message. For example, if oMSyntax equals 64 (which it does in this case) the following lines of code will be triggered:

Case 64
    Wscript.Echo "Datatype: " & "String (Unicode)"

Run the script for the telephone-Number attribute and we’ll get back the following information:

Single-valued: True
Datatype: String (Unicode)

OK, so maybe it wasn’t all that hard after all; just a little tricky. But you know the Scripting Guys: if something involves even the tiniest bit of effort, well ….

Note. By the way, we realize that some of the data types shown in the Select Case statement might seem a bit obtuse. Fortunately, you can find out more about the possible values of oMSyntax (and what those values mean) by taking a peek at the Syntaxes reference on MSDN.


How Can I Determine the Topmost OU For an Active Directory Object?

Hey, Scripting Guy! Given a distinguished name, how can I determine the “topmost” OU?

-- HF

Hey, HF. You know, as he was driving in to work this morning, the Scripting Guy who writes this column heard an advertisement for a series of adult education courses being offered by a local community college. As you might expect, this commercial pointed out that taking these courses will make you smarter and help you get a better job; what was interesting, though, is that the ad also implied – slyly – that taking these courses will make it easier for to pick up girls (or guys). In other words, not only will education make you smarter, but it will make you more attractive as well.

Oddly enough, that caused the Scripting Guy who writes this column to reflect back on his college days. One of the first college classes this Scripting Guy ever took was Astronomy 101. (It was also one of the best classes he ever took, and to this day he still wonders why he didn’t take any more astronomy classes, or maybe even become an astronomer.) During one class the instructor noted that “The obliquity of the ecliptic is 23½ degrees.” He paused for a moment and then added, “If you remember nothing else from this course remember that. People will think you’re pretty sexy if you know that the obliquity of the ecliptic is 23½ degrees.”

As it turns out, the Scripting Guy who writes this column did remember that the obliquity of the ecliptic is 23½ degrees. (Actually it’s a tiny bit less than that, but that’s close enough, especially for a column that’s supposed to be about system administration scripting.) So did it work, do people think that the Scripting Guy who writes this column is pretty sexy? Let’s put it this way: not everything you learn in college turns out to be true.

Not by a long shot.

In other words, we can’t promise you that a little knowledge will make you more attractive. However, it will help you determine the topmost OU for an Active Directory object:

Set objSysInfo = CreateObject("ADSystemInfo")
strDN = objSysInfo.UserName

arrDN = Split(strDN, ",")

For i = UBound(arrDN) to 0 Step -1
If Left(arrDN(i), 3)
= "OU=" Then
arrOU = Split(arrDN(i), "=")
Wscript.Echo arrOU(1)
Exit For
End If
Next

Before we begin explaining the script let’s take a minute to explain the scenario. We have the distinguished name for an object (say, a user account). That’s going to look something like this:

cn=Ken Myer,OU=Level4,OU=Level3,OU=Level2,OU=Level1,DC=fabrikam,DC=com

As you can see, the Ken Myer account resides in the Level4 OU. Level4, in turn, is a child OU of Level3, which is a child OU of Level2, which is a child OU of Level1. (In a distinguished name the item to the right is always a parent of the item to the left.) Level1 also happens to be the “topmost” OU; it’s not a child of any other OU. This is the OU that we need to identify.

Now, in a previous column we showed you how to identify the “grandparent” OU for a given container. We can’t use that approach here simply because we aren’t sure how many levels we need to go up in order to find the topmost OU. Well, OK, if you want to get technical we could use that approach, albeit with a few modifications; however, we think we came up with a solution that’s a little bit easier to implement.

Of course, none of these approaches are of much use unless we have a distinguishedName attribute to work with. To take care of that problem, and for illustration purposes, we’ve used this block of code to grab the distinguished name of the logged-on user:

Set objSysInfo = CreateObject("ADSystemInfo")
strDN = objSysInfo.UserName

All we’re doing here is creating an instance of the ADSystemInfo object, an object that retrieves information about the logged-on user and his or her computer. (And before you ask, sorry: the ADSystemInfo object can only be used locally. That means this script can’t be readily-modified to retrieve information about the user logged on to a remote computer. But don’t despair; in an upcoming edition of TechNet Magazine we’ll discuss that very issue.) After creating the ADSystemInfo object we then assign the value of the UserName property to the variable strDN. And guess what? The value of the UserName property just happens to be the distinguished name of the logged-on user:

cn=Ken Myer,OU=Level4,OU=Level3,OU=Level2,OU=Level1,DC=fabrikam,DC=com

That’s nice, isn’t it? Except for one thing: how the heck do we determine the topmost OU given a value like that?

There are, admittedly, different ways of doing this. For various reasons (simplicity being the primary reason) we decided to start out by using the Split function to turn this distinguished name into an array named arrDN:

arrDN = Split(strDN, ",")

By splitting the string on the comma we end up with an array consisting of the following items:

cn=Ken Myer
OU=Level4
OU=Level3
OU=Level2
OU=Level1
DC=fabrikam
DC=com

As you probably know, in a distinguished name the topmost OU will always be the OU nearest the domain components (the DC= things). In this case, it’s the Level1 OU that’s nearest the domain components: cn=Ken Myer,OU=Level4,OU=Level3,OU=Level2,OU=Level1,DC=fabrikam,DC=com. So is there an easy way to determine which OU is nearest the domain components? You bet there is:

For i = UBound(arrDN) to 0 Step -1

What we’ve done here is set up a For Next loop to loop through all the items in the array arrDN. But this isn’t your ordinary For Next loop, one that starts with the first item in the array and works its way towards the last item. Instead, we’re starting with the last item in the array (something we can determine using the UBound function) and working our way backwards towards the first item. In our example, we have an array with 7 items; that means that the last item in the array has an index number of 6. (Because the first item in an array is 0 the last item will always be the total number of items minus 1.) That means we’re going to start out with the item that has the index number 6. What do we do when we’re done with that item? That’s easy: we’ll then look at item 5; that’s what the Step -1 parameter is for. We’ll continue doing this until we’ve disposed of item 0; at that point our loop will be complete, and we’ll have looked at each and every item in the array.

Note. OK, to be completely accurate, we probably won’t look at all the items in the array. But we’ll explain that in just a second.

Inside the loop we start out by checking to see if the first three characters in the item happen to be OU=. That’s what we use this line of code, and the Left function, for:

If Left(arrDN(i), 3) = "OU=" Then

In our example item 6 (the first item we look at) is this: DC=com. Are the first three letters in DC=com equal to our target value (OU=)? No, they aren’t. Therefore, we loop around and check the next item in the array, DC=fabrikam. Do we have a match? Nope. And so we loop around and check the next item: OU=Level1. What do you know: we’ve got ourselves a winner!

Having found the topmost OU we’re now practically done; in fact, if we wanted to, we could simply echo back the value OU=Level1 and call it good. However, we decided to get rid of the OU=, just to make things a little neater and cleaner. That’s what we do here:

arrOU = Split(arrDN(i), "=")
Wscript.Echo arrOU(1)

In the first line above we use the Split function to create another array named arrOU. This time we split the value on the equals sign (=), yielding an array with these two items:

OU=
Level1

And then in line 2 we echo back the value of the second item in the array:

Level1

At that point we truly are finished. Consequently, we call the Exit For statement to exit our For Next loop and then call it good. That should be all you need, HF, at least when it comes to determining the topmost OU of an Active Directory object.

Incidentally, the Scripting Guy who writes this column was a little disappointed to discover that learning that the obliquity of the ecliptic was 23½ degrees didn’t make him more attractive and desirable. On the other hand, back when the Scripting Guy who writes this column first started college people still believed that the Sun revolved around the Earth. All things considered, he was just happy that no one tried burning him at the stake for claiming that the obliquity of the ecliptic was 23½ degrees.

Note. What if someday he changes his mind and wants to be burned at the stake? No problem; there are plenty of people here at Microsoft who would be more than happy to help him out with that.


How Can I Determine Which Attributes are Replicated to the Global Catalog?

Hey, Scripting Guy! How can I find out which attributes are replicated to the global catalog?

-- AS

Hey, AS. When people envision the future, they often foresee enormous skyscrapers, buildings so large that you might live your whole life without ever leaving them: you’ll live there, work there, go to school there, buy your groceries there, etc. (In fact, about the only time you’ll ever leave the building is when you go out to get the newspaper: even in the future, newspaper delivery people will still toss the paper under the car, in the rose bushes, or halfway out into the street.)

Active Directory serves as sort of a sneak peek of what the future might be like: pretty much everything you need in order to manage Active Directory is actually stored in Active Directory. Obviously Active Directory houses your user and computer accounts. However, via the schema Active Directory also houses information about everything that could be stored in Active Directory. Objects, classes, properties, methods, even information about which attributes are replicated to the global catalog: it’s all there in Active Directory. And it’s all accessible to scripters.

In other words, if you want to know which attributes are replicated to the global catalog, all you have to do is run a script like this:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://cn=Schema,cn=Configuration,dc=fabrikam,dc=com' WHERE " & _
        "objectClass='attributeSchema' AND isMemberOfPartialAttributeSet=TRUE"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

What we’re doing here is using ActiveX Data Objects (ADO) to do a search of the Configuration container in Active Directory; that’s where we’ll find the schema, and the schema has information about all the attributes available to us. We won’t talk about the code for conducting the search itself; if you’d like to learn more about searching Active Directory you should take a look at our two-part Tales from the Script series on that very subject. What we will talk about is the query we use when conducting our search:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://cn=Schema,cn=Configuration,dc=fabrikam,dc=com' WHERE " & _
        "objectClass='attributeSchema' AND isMemberOfPartialAttributeSet=TRUE"

As you can see, we’re binding to the schema and then looking for items that meet two criteria. First, the items must have an objectClass equal to attributeSchema; that’s just a fancy way of saying that we only want attributes. Second, we only want attributes where the isMemberOfPartialAttributeSet attribute is True. (Yes, attributes can have attributes.) Needless to say, isMemberOfPartialAttributeSet isn’t one of the better names we here at Microsoft have ever come up with; in fact, it might be one of the least obvious and intuitive. But if this value is True, then the attribute is replicated to the global catalog. (We assume the name derives from the fact that the global catalog represents only a partial set of Active Directory attributes, seeing as how only a subset of all the attributes is replicated to the global catalog.)

At any rate, run the script and you should get back a list of all the attributes that are replicated to the global catalog. And, if nothing else, that will give you something to read if you don’t feel like venturing into the neighbor’s yard looking for your newspaper.

True story. A few weeks ago a short in the electrical box caused one of the Scripting Garages to catch fire, a fire which started around 2:00 AM. The Scripting Guy who owns the garage was standing in the middle of the road, watching the firefighters put out the fire, when a car weaved its way through the mass of fire trucks, drove over a fire hose, pulled up to the driveway, and tossed the newspaper into the middle of the action (close enough to the fire that the plastic bag the paper was wrapped in actually melted a little). Talk about neither rain nor snow nor gloom of night!

Yes, we know: that’s the Postal Service motto. But you get the idea.

Incidentally, no one was hurt in the fire and the damage was relatively minor. In fact, at the rate they’re going now, the contractors ought to have the garage fully repaired about the same time those gigantic skyscrapers get built.


How Can I Display a Multi-Valued Attribute Returned from an Active Directory Search?

Hey, Scripting Guy! How can I display a multi-valued attribute that I get back when using ADO to search Active Directory?

-- ES

Hey, ES. For the most part Active Directory is a script writer’s paradise; with very few exceptions anything you can do using the Active Directory snap-ins and GUI tools you can also do using a script. Of course, that doesn’t mean there aren’t a few bumps in the road. For example, dealing with dates and times in Active Directory can be a bit... challenging. After all, not only does Active Directory use some crazy date-time formats, but it also uses different date-time formats for different attributes. How…nice….

Another challenge crops up with multi-valued attributes. Most Active Directory properties are single-valued; in other words, the attribute can hold only one value. Names, for example, are single-valued: a user can have only one distinguished name, one SAM Account name, one first name, one last name, etc. Likewise, an account can have only one creation date and one last modified date; an account can be disabled or enabled but it can’t be both. Single-valued attributes? Piece of cake.

Multi-valued attributes are a different story. As the name implies, multi-valued attributes can contain more than one value. Take phone numbers, for example. As scary as this might be, it’s not unusual for people to have more than one home phone number or more than one cell phone number. The designers of Active Directory could have chosen to ignore that reality and limit people to a single home phone number; alternatively, they could have created scores of additional attributes: homePhone1, homePhone2, homePhone3, homePhone4, etc. Neither of those approaches was particularly appealing. Therefore the decision was to create multi-valued attributes: a single property that could contain a whole bunch of values. There’s a single-valued attribute that represents your primary home phone number, but there’s also a multi-valued attribute that can hold as many additional home phone numbers as you might have (555-1111; 555-2222; 555-3333; etc.).

That’s actually a pretty good solution; the only problem is that you can’t directly echo the value of a multi-valued attribute. Suppose you’ve just conducted a search of Active Directory and returned the otherHomePhone attribute. If you try echoing the value using this line of code you’re going to get a “Type mismatch” error:

Wscript.Echo objRecordSet.Fields("otherHomePhone").Value

The problem is that a multi-valued attribute is actually an array of values; you can’t echo the value because arrays don’t have a single value. Instead, you need to create a For Each loop and echo each of the values found in the array of values:

For Each strPhone in objRecordSet.Fields("otherHomePhone").Value
    Wscript.Echo strPhone
Next

That’s the secret right there: create a For Each loop and display each value in the array of values. This will work whether the user happens to have one additional home phone number, no additional home phone numbers, or scores of additional home phone numbers.

See, not too bad. Here’s a sample script that uses ADO to search Active Directory and echo back the Name and the otherHomePhone attribute values for each user in the domain. As you might have guessed, otherHomePhone happens to be a multi-valued attribute. But that’s all right; we just use a For Each loop to loop through all the other home phone numbers for each user.

Here’s the script:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name,otherHomePhone FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    If Not IsNull(objRecordSet.Fields("otherHomePhone").Value) Then
        For Each strPhone in objRecordSet.Fields("otherHomePhone").Value
            Wscript.Echo "Home phone: " & strPhone
        Next
    End If
    objRecordSet.MoveNext
Loop

We did do one additional thing here: we checked to see if the otherHomePhone attribute was null (that is, we checked to see if the user actually had another home phone number). That’s what we do with this line of code:

If Not IsNull(objRecordSet.Fields("otherHomePhone").Value) Then

Why did we do that? Well, suppose the user didn’t have another home phone number. In that case our script would echo a line like this:

Home phone:

In our case we didn’t want to clutter the output with lines like this. However, if you’d prefer to see that a user doesn’t have any additional home phone numbers then simply remove the If-Then statement. In other words:

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    For Each strPhone in objRecordSet.Fields("otherHomePhone").Value
        Wscript.Echo "Home phone: " & strPhone
    Next
    objRecordSet.MoveNext
Loop

Incidentally, if you’d like to know more about searching Active Directory (something we just plain ignored in today’s column) check out this month’s Tales from the Script.


How Can I Get a List of All the Objects that have been Added to Active Directory Since a Specified Date?

Hey, Scripting Guy! How can I get a list of all the objects that have been added to Active Directory since a specified date?

-- KR

Hey, KR. First of all we’d like to apologize if our answer here is still a little soggy. No doubt you’ve seen cartoons where someone receives a package in the mail, they hear it ticking and – suspecting the worse – they immediately plunge the box into a bathtub full of water. (Fortunately, in cartoons, there’s always a bathtub full of water whenever you need one.) Inevitably, of course, they discover that the package wasn’t a bomb after all, just a clock.

Hey, we didn’t say it was all that funny, just something that happens over and over again.

To tell you the truth, we have the same reaction any time we get asked a question involving dates and Active Directory: we immediately plunge the email into a bathtub full of water. (Which, fortunately, we also have available whenever we need one.) That’s because working with dates and times in Active Directory can be a bit … interesting …. (Yes, like the infamous date-time values that represents the number of 100-nanosecond intervals that have occurred since January 1, 1601. Which – coincidentally enough – was also the last time any of the Scripting Guys got a raise.)

Of course, and, inevitably, as soon as we pull the email out of the bathtub we discover the question really wasn’t so bad after all. Such was the case here. Although the date-time value for the whenCreated attribute (which tells us when an object was created) is a little bit tricky, at least it doesn’t deal with nanoseconds or random fluctuations in the space-time continuum. That means we can use it in a relatively simple Active Directory search script. In fact, we can use it in a script just like this one:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

dtmCreationDate = "20060101000000.0Z"

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name, objectCategory, whenCreated FROM 'LDAP://dc=fabrikam,dc=com' WHERE "  & _
        "whenCreated>='" & dtmCreationDate & "'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    Wscript.Echo objRecordSet.Fields("objectCategory").Value
    Wscript.Echo objRecordSet.Fields("whenCreated").Value
    objRecordSet.MoveNext
Loop

As usual, we’re not going to bother explaining most of the code shown here; if you’re not familiar with scripts used for searching Active Directory we recommend you take a look at the two-part Tales from the Script series Dude, Where’s My Printer? (And yes, we know: by now anyone who’s ever read this column has already memorized the URL to Dude, Where’s My Printer? But we offer it once again, just in case.)

What we will explain is how to work with the whenCreated attribute. You might notice that, early on in the script, we assign a value to the variable dtmCreationDate:

dtmCreationDate = "20060101000000.0Z"

This is actually the date January 1, 2006. Admittedly, it doesn’t look like January 1, 2006, so let’s show you how we went from January 1, 2006 to 20060101000000.0Z:

Item | Description
2006 | Year
01 | Month
01 | Day
00 | Hours
00 | Minutes
00 | Seconds
.0Z | Time zone (for now, just use .0Z and you’ll be OK)

What if we wanted to use 1:35 PM on November 27th, 2005? That would look like this, keeping in mind that, using a 24-hour clock, 1:35 PM is equal to 13 hours and 35 minutes:

Item | Description
2005 | Year
11 | Month
27 | Day
13 | Hours
35 | Minutes
00 | Seconds
.0Z | Time zone (for now, just use .0Z and you’ll be OK)

Or, programmatically:

dtmCreationDate = "20051127133500.0Z"

See? A little quirky, but not too bad. Besides, once you understand how to format the date-time value then you’re home free. With a properly-formatted date-time value in hand we can write a query for conducting the search:

objCommand.CommandText = _
    "SELECT Name, objectCategory, whenCreated FROM 'LDAP://dc=fabrikam,dc=com' WHERE "  & _
        "whenCreated>='" & dtmCreationDate & "'"

Notice what we’re looking for: all the objects that were created after the 0 hour (00 hours, 00 minutes, 00 seconds) on January 1, 2006. In other words, show us all the objects that have been added to Active Directory during the year 2006. We should also point out that we’re retrieving the Name, objectCategory, and whenCreated attributes; that means we’ll end up with output similar to this:

KenMyer
CN=User,CN=Schema,CN=Configuration,DC=fabrikam,,DC=com
1/3/2006 8:55:27 AM

In other words, we’ll know the name of the object, the type of object (in this case, a user account) and the date and time the object was created. And, fortunately, we don’t have to do any crazy re-formatting in order to display the value of the whenCreated attribute: it automatically gets displayed as a standard date-time value. We only have to use the 20060101000000.0Z syntax when we’re using whenCreated in a Where clause.

And now, if you’ll excuse us, we have to go dry our clothes. Again. (We really do need to stop overreacting any time someone asks us a question, don’t we?)


How Can I Get the “Grandparent” of an Object in Active Directory?

Hey, Scripting Guy! How can I get the “grandparent” of an object in Active Directory?

-- JG

Hey, JG. Wait a second: is this “grandparent” thing some sneaky reference to the fact that the Scripting Guys (well, except for Jean, of course) are all starting to show their age a bit? OK; we just wanted to make sure. (Editor’s Note: Yes, that originally said “except for Greg,” but it’s the editor’s job to keep these articles accurate….)

Note. It’s not that the Scripting Guys (well, except for Jean, of course) aren’t getting old; it just doesn’t seem very sporting to rub it in. Although at their age they probably won’t remember what was said about them anyway.

Actually, what JG is referring to is this. Suppose we have an Active Directory object with the following ADsPath:

LDAP://cn=ken myer, ou=finance, dc=fabrikam, dc=com

In this case, the Ken Myer user account can be found in the Finance OU; that makes the Finance OU the parent object for this account. Meanwhile, the Finance OU can be found in fabrikam.com; that makes fabrikam.com the parent object for the Finance OU. And because the parent of a parent is a grandparent, fabrikam.com is also the grandparent of the Ken Myer user account.

Interesting historical fact. National Grandparents Day falls each year on the first Sunday after Labor Day. This holiday was started by a married couple that had 40 grandchildren. The Scripting Guys fully support the idea of a Grandparents Day; after all, who could be more deserving? Still, we find it interesting that Grandparents Day was started by a couple who would then be eligible to receive 40 additional gifts each and every year.

Say, did we mention that January 11th is Scripting Guys Day…?

Here’s a script that can determine the grandparent for an Active Directory object:

Set objUser = GetObject("LDAP://cn=ken myer, ou=finance, dc=fabrikam, dc=com")
Set objParent = GetObject(objUser.Parent)
Wscript.Echo "Grandparent: " & objParent.Parent

That’s right: this is very easy. That’s because Active Directory objects all have a Parent attribute that returns the ADsPath to the parent object. That’s the secret to determining the grandparent of an object. The script begins by binding to the Ken Myer account in Active Directory. We then encounter this line of code:

Set objParent = GetObject(objUser.Parent)

All we’re doing here is binding to the parent object for the Ken Myer account (objUser.Parent). As we noted, the Parent attribute returns the ADsPath for the parent object; in this case, that’s LDAP://ou=finance, dc=fabrikam, dc=com. After we’ve made a connection to the Finance OU we can then echo back the value of its Parent attribute:

Wscript.Echo "Grandparent: " & objParent.Parent

As we noted, the parent of the parent will be the grandparent of the Ken Myer account.

Hey, no need to thanks us. Just be sure to mark January 11th on your calendar and we’ll call it good.


How Can I Retrieve the Value of an Active Directory Attribute that has a Hyphen in Its Name?

Hey, Scripting Guy! We have a custom application that adds some attributes to the Active Directory schema. These attributes have hyphens in their names, and I can’t figure out how to retrieve the value of those attributes; any time I try to connect to one I get an error message. Any suggestions?

-- TW

Hey, TW. You know a lot of people don’t believe in coincidence or chance occurrence; instead, they believe that everything happens for a reason. (Of course, in the case of the Scripting Guys we usually can’t figure out what the heck that reason could be, but, still….)

Why do we mention the fact that many people don’t believe in coincidences? Well, interestingly enough, about the same time we received your email we were fooling around with some of the new attributes that have been added to the Active Directory schema for Windows Server 2003. Many of these new attributes also have hyphens in their names; thus we had simple little scripts similar to this:

Set objUser = GetObject("LDAP://cn=Ken Myer, OU=Finance, DC=fabrikam, DC=com")
Wscript.Echo "User account control computed: " & objUser.msDS-User-Account-Control-Computed

And what do you suppose happened when we tried running one of these simple little scripts? If you guessed, “It probably worked and you got back the value you were looking for,” well, take a look at this:

C:\Scripts\test.vbs(2, 1) Microsoft VBScript runtime error: Object doesn't support
this property or method: 'objUser.msDS'

Yikes. ADSI doesn’t like seeing a hyphen in an attribute name; in fact, as far as ADSI is concerned the name of our property (msDS-User-Account-Control-Computed) consists of just the four letters msDS. As soon as it encounters the hyphen ADSI assumes it has reached the end of the attribute name; it then tries looking for an attribute named msDS and, not surprisingly, can’t find one.

Ah, but now the chain of “coincidences” gets really spooky. Just a week or so before that we had resolved a problem that had been bugging us for nearly a year: how can you use ADO to read a text file that contains a space in the file name? As it turned out, the answer was to enclose the file name in square brackets, like so:

strFile = "[Fabrikam Employees.txt]"

Although it seemed like a million-to-one shot, we decided to put square brackets around our Active Directory attribute name:

Set objUser = GetObject("LDAP://cn=Ken Myer, OU=Finance, DC=fabrikam, DC=com")
Wscript.Echo "User account control computed: " & objUser.[msDS-User-Account-Control-Computed]

If you’ve ever seen the Twilight Zone then you already know what happened: the square brackets worked like a charm! Do you need to get or set the value of an Active Directory attribute that has a hyphen in the name? Then just put square brackets around the name. Do that and – like magic – everything will work perfectly. Try it and see.

Yes, very eerie, isn’t it?

By the way, you can put square brackets around any attribute name and your script will work just fine. We aren’t sure why you’d do this, but following script will return the CN for the user Ken Myer:

Set objUser = GetObject("LDAP://cn=Ken Myer, OU=Finance, DC=fabrikam, DC=com")
Wscript.Echo "CN: " & objUser.[CN]

Makes you shudder just thinking about it, doesn’t it? But, then again, maybe it’s all just a coincidence….


How Can I Set an Active Directory Attribute Value to NULL?

Hey, Scripting Guy! How can I set an Active Directory property to NULL? I tried setting the value to an empty string (“”) but it didn’t work.

-- JJ

Hey, JJ. You know, it’s not like the Scripting Guys to get philosophical; one of the Scripting Guys, for example, bases his entire life on this credo: never answer the phone early on Saturday morning. (His reasoning is fairly straightforward: if it’s important, they’ll call back. And if it’s not important, then why the heck are they calling early on Saturday morning?!?!) That’s about as deep as the Scripting Guys ever get.

For better or worse, though, we have to get a little philosophical – and maybe even a little metaphysical – when trying to answer this question. The problem you are running into is the fact that while you might think an empty string (“”) is nothing, scripting languages tend to see an empty string as something. For example, suppose you set User A’s telephone number to an empty string. You then run a script that retrieves a list of all the users who have telephone numbers. Guess who shows up in that list? That’s right: good old User A. That’s because, in the crazy world of scripting, User A actually has a telephone number; it’s just that his telephone number happens to consist of an empty string. (Yes, sort of a sound-of-one-hand-clapping thing.)

If you want to get rid of an attribute value altogether you have to set the value of that attribute to NULL. And in Active Directory the best way to do that is to use the PutEx method and clear the value. Let’s show you a script that truly does set a user’s telephone number to nothing (NULL) and then we’ll explain how it works:

Const ADS_PROPERTY_CLEAR = 1

Set objUser = GetObject _
   ("LDAP://cn=ken myer, ou=finance, dc=fabrikam, dc=com")

objUser.PutEx ADS_PROPERTY_CLEAR, "telephoneNumber", 0
objUser.SetInfo

We begin by defining a constant named ADS_PROPERTY_CLEAR and setting the value to 1; we’ll use this later in the script to tell the PutEx method the kind of operation we want to carry out. (PutEx has additional uses beyond clearing attribute values; for more information, check out the ADSI Scripting Primer in the Microsoft Windows 2000 Scripting Guide.) We then bind to the desired user account, in this case the Ken Myer account found in the Finance OU of fabrikam.com

Next we use these two lines of code to completely erase Ken Myer’s telephone number:

objUser.PutEx ADS_PROPERTY_CLEAR, "telephoneNumber", 0
objUser.SetInfo

As you can see, we call the PutEx method and pass it three parameters: 1) the constant ADS_PROPERTY_CLEAR which, again, indicates the operation we want to perform; 2) telephoneNumber, the attribute we want to clear; and 3) 0, a parameter required any time you clear an attribute. We then call the SetInfo method to write the changes back to Active Directory. If you re-run the script that retrieves a list of users who have phone numbers, Ken Myer will no longer appear in the list. That’s because he no longer has a phone number, not even one consisting of an empty string.


How Can I Work with a CN that Has a Comma in It?

Hey, Scripting Guy! My user accounts have commas in their CN attributes; for example, Myer, Ken. How do I bind to those user accounts using a script? I always get the error message “An invalid dn syntax has been specified.”

-- GT

Hey, GT. For those of you who don’t know, the CN attribute is the attribute that uniquely identifies a user account within an OU or other Active Directory container. (You can have more than one Myer, Ken in your domain, but you can have only a single Myer, Ken in, say, the Accounting OU.) The CN attribute is also the attribute used to display user names in Active Directory Users and Computers. When you add a user from within Active Directory Users and Computers, one of the fields you typically fill out is labeled Full Name (and then, for some reason, when you later view the user account properties, this same field gets re-labeled Display Name). Administrators like to specify CNs (full names) using the last name, first name syntax; that way, when they look at an OU all the users are arranged in alphabetical order by last name. (If you specified Ken Myer as the full name, users will be arranged in alphabetical order by first name.)

In other words, it’s very common for users to have CNs similar to Myer, Ken. That’s great when you’re working in Active Directory Users and Computers, but not so great when you’re trying to manage these users with ADSI scripts. For example, this script – which attempts to bind to the Myer, Ken user account – will fail with an invalid syntax error:

Set objUser = GetObject("LDAP://CN=Myer, Ken,OU=Accounting,DC=fabrikam,DC=com")
Wscript.Echo objUser.CN

So what’s the problem here? Well, it’s not with the user’s CN; Myer, Ken is perfectly valid. The problem is that when you write a binding string in ADSI the comma is used to separate the individual values within the ADsPath. When we write CN=Myer, Ken,OU=Accounting, our script thinks CN=Myer is the first value, and Ken is the second value. That’s because of the comma between Myer and Ken. And because Ken by itself doesn’t make any sense within an ADsPath, the script blows up.

So what do you about that? As it turns out, the comma is a reserved character when it comes to Active Directory paths. (Other reserved characters include the semicolon, the plus sign, the backslash, and the left and right angle brackets. For a complete list, see the Lightweight Access Directory Protocol documentation on MSDN.)

For most reserved characters, you can use them in binding strings simply by placing a backslash (\) in front of the characters. This script will successfully bind to the Myer, Ken user account and report back the user’s CN:

Set objUser = GetObject("LDAP://CN=Myer\, Ken,OU=Accounting,DC=fabrikam,DC=com")
Wscript.Echo objUser.CN

Again, note how the user CN is specified: CN=Myer\, Ken,OU. After Myer we place a backslash and a comma, then we continue with the rest of the path (including the blank space that separates Myer, and Ken).


How Can I Add a User to a Group If I Don’t Know the Distinguished Name of That User?

Hey, Scripting Guy! How can I add a user to an Active Directory group if I don’t know the distinguished name of that user?

-- VO

Hey, VO. You know, if we wanted to take the easy way out here we’d just tell you, “Sorry, but if you don’t know the distinguished name of the user you can’t add that user to an Active Directory group.”

Note. If you want to get technical, even the distinguished name isn’t good enough: you really need to know the ADsPath of the user (a value similar to this: LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com). But we’ll let that slide; after all, if you do have the distinguished name (cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com) then you can generate the ADsPath just by sticking LDAP:// on the front.

In other words, if we just wanted to dismiss this question without even trying, pointing out that this simply can’t be done, well, we’d be well within our rights to do so; after all, it can’t be done. But you know what? That would be taking the easy way out. And when have you ever known the Scripting Guys to take the easy way out?

OK, true: we forgot about that. And that. That other time? Well, that wasn’t really our fault; we did the best we could. The entire month of July? Come on, it was summer, for crying out loud; everyone takes the easy way out during the summer. And – well, OK, we get the point. But this time we absolutely will not take the easy way out. No way.

Well, OK. But at least we’ll answer your question.

Let’s recap the situation here. We want to add a user to an Active Directory group. To do that we need to know the ADsPath for the user. However, we don’t know the ADsPath for the user. Our prospects for success here seem kind of bleak, don’t they?

Nah. You say you don’t know the ADsPath for the user? That’s fine; we’ll just go out and find the ADsPath for the user:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")

objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND samAccountName = 'kenmyer'"

Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    strADsPath = objRecordSet.Fields("ADsPath").Value
    Set objGroup = GetObject("LDAP://cn=Finance Managers,ou=Finance,dc=fabrikam,dc=com")
    objGroup.Add(strADsPath)
    objRecordSet.MoveNext
Loop

What we’re doing here is assuming that you know something about the user in question. (Admittedly if you don’t know anything about the user then it’s going to be a bit more difficult to locate that user and add him or her to a group.) For example, even though you don’t know the user’s distinguished name there’s a very good chance that you know his or her samAccountName (which, for all intents and purposes, is just the user’s logon name). Therefore, all we have to do is search Active Directory for a user with that particular samAccountName (a good attribute to search for, by the way, because samAccountNames must be unique within a domain). After we locate the user we can grab his or her ADsPath. And once we have the ADsPath we can add that user to an Active Directory group.

That’s exactly what we do in our script. At the risk of taking the easy way out (heaven forbid!) we won’t explain the ins and outs of searching Active Directory in any detail today; if you need more information take a look at our two-part series Dude, Where’s My Printer? About all we will do is point out that this is the query we use to search the fabrikam.com domain ('LDAP://dc=fabrikam,dc=com') for a user (objectCategory=’user’) that has a logon name of kenmyer (samAccountName = 'kenmyer'):

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND samAccountName = 'kenmyer'"

When we execute this query (using the Execute method) we’ll get back a recordset containing the ADsPath of each and every user who has the samAccountName kenmyer. Because samAccountNames must be unique in the domain, that recordset will contain – at most – just one item: the ADsPath of the user we want to add to the group.

That’s great, except for one thing: now that we have a recordset what do we do with it? Well, to begin with, we set up a Do Until loop that runs until we’ve looped through all the records in our recordset; in other words until the recordset’s EOF (end-of-file) property is True. (And yes, we need to set up a loop like this even though we’ll never have more than one record in the recordset.) Inside that loop we use this line of code to grab the value of the user’s ADsPath and store it in a variable named strADsPath:

strADsPath = objRecordSet.Fields("ADsPath").Value

Once we have the ADsPath we can then add the user to the group. That’s a task that requires two lines of code: one line to bind to the group account in Active Directory; the other to call the Add method and add the new group member, making sure we pass the ADsPath of the user to the Add method. In other words:

Set objGroup = GetObject("LDAP://cn=Finance Managers,ou=Finance,dc=fabrikam,dc=com")
objGroup.Add(strADsPath)

After adding the user to the group we call the MoveNext method to advance to the next record in the recordset. Because we don’t have a next record in our recordset that causes us to exit the loop, which in turn brings the script to a close. The net result? The user gets added to the group, and without us doing very much beyond writing a few lines of code, most of which is boilerplate code for searching Active Directory.

In all fairness, we should add that, at least when it comes to being lazy and not trying very hard, the Scripting Guys are mere amateurs compared to some people. Several years ago the Scripting Guy who writes this column came home to find that his cable TV was no longer working. When he called the cable company (which has since gone out of business) he was told that the cable had been shut off because he hadn’t paid his bill.

“I’m positive we paid the bill,” said the Scripting Guy, “Let me get the checkbook and –“

“Would that be check number 1895, dated July 14th?” interrupted the cable guy.

“Yes,” said the Scripting Guy. “How did you know that?”

“I’m looking at the check right now,” said the cable guy. “Apparently we received it about three weeks ago. However, Marcie has been on vacation since then so it’s just been sitting on her desk. I was supposed to enter all these checks into the computer, but I haven’t gotten around to it.”

“So you shut off our cable for non-payment of our bill even though you received our payment three weeks ago?” asked the Scripting Guy.

“Yeah. Kind of funny, isn’t it?”

Well, not really. What was funny was when the cable guy said it would take an additional three weeks to restore service.

“Wait a minute,” said the Scripting Guy who writes this column. “It takes three weeks to restore existing service, yet you can activate a brand-new account in a week?”

“These things take time,” replied the cable guy, who paused for a moment and then added, “There: I just flipped the switch and your cable has been restored. Try not to let this happen again, OK?”

Now that was a company who knew about taking the easy way out!


How Can I Add a User to a Group if That User Belongs to Two Other Groups?

Hey, Scripting Guy! How can I check to see if a user is in both group A and group B and, if so, add that user to Group C?

-- DH

Hey, DH. Good heavens, how are we supposed to know that?!? Sheesh.

Sorry; we just always wanted to say that. Now that we have that out of our system, here’s a sample script that checks to see if Ken Myer belongs to both the Finance Users and the Fabrikam Managers groups in Active Directory. If he does belong to both groups, then the script adds him to a third group, Finance Managers (if A and B, then C):

On Error Resume Next

Set objUser = GetObject("LDAP://cn=Ken
Myer,ou=Finance,dc=fabrikam,dc=com")

i = 0

For Each strGroup in objUser.memberOf
Set objGroup = GetObject("LDAP://" &
strGroup)
If objGroup.CN = "Finance Users" Then
i = i + 1
End If
If objGroup.CN = "Fabrikam Managers" Then
i = i + 1
End If
Next

If i = 2 Then
Set objGroup = GetObject("LDAP://cn=Finance
Managers,ou=Finance,dc=fabrikam,dc=com")
objGroup.Add(objUser.ADsPath)
End If

The script begins by creating an object reference to the Ken Myer user account in Active Directory. We then assign the value 0 to a counter variable named i; we’ll use this variable to keep track of how many of our two target groups Ken belongs to.

As it turns out, group membership in Active Directory is stored in a multi-valued attribute named memberOf. With that in mind, we use this line of code to walk through the collection of groups that Ken Myer is a member of:

For Each strGroup in objUser.memberOf

As it further turns out, the memberOf attribute returns the distinguished name of each group Ken Myer belongs to. That’s nice, but the distinguished name looks something like this:

cn=Finance Users,ou=Finance,dc=fabrikam,dc=com

Needless to say, we’re used to dealing with groups by name (e.g., Finance Users); most likely we have no idea where the group account is stored in Active Directory. Therefore, we don’t even bother checking the distinguished name; instead, we use that value to connect to the group account itself. That’s what we do here:

Set objGroup = GetObject("LDAP://" &  strGroup)

Once we connect to the group account we can then use code like this to check the name (CN) of the group, something a bit easier and a bit more intuitive:

If objGroup.CN = "Finance Users" Then

As we loop through the groups, we’re checking to see if any of those groups have a CN equal to Finance Users (one of our two target groups). What if one of those groups does have a CN equal to Finance Users? Well, in that case we increment the value of i by 1:

i = i + 1

Pretty fancy coding, huh?

Meanwhile, we have a similar block of code that checks to see if the group has a CN equal to Fabrikam Managers. If it does, then we again increment the value of our counter variable by 1:

If objGroup.CN = "Fabrikam Managers" Then
    i = i + 1
End If

We then loop around and check the next group in the collection.

Why do we increment the value of our counter variable? Well, if Ken doesn’t belong to either of our target groups then i will never be changed and will thus equal 0. If Ken belongs to one group, but not the other, then i will be changed one time, and thus be equal to 1. So what does it mean if i is equal to 2? You got it: Ken must be a member of both Finance Users and Fabrikam Managers (because the value if i was incremented twice). Consequently, we want to add Ken to our third group, Finance Managers:

If i = 2 Then
    Set objGroup = GetObject("LDAP://cn=Finance Managers,ou=Finance,dc=fabrikam,dc=com")
    objGroup.Add(objUser.ADsPath)
End If

Nothing too fancy here: we create an object reference to the Finance Managers group, then use the Add method to add Ken Myer. Note that when calling the Add method we pass the value objUser.ADsPath: that’s the ADsPath to the Ken Myer user account in Active Directory.

Two quick notes regarding this script. First, it’s possible that you could have multiple groups with the same CN; if that’s the case, then examining the value of the CN attribute won’t do you much good. Instead, you’ll need to look at the sAMAccountName attribute, which must be unique within the domain. (Of course, the sAMAccountName is going to be something along the lines of fabmgrs, another value you typically don’t know off the top of your head.)

Second, this script doesn’t deal with nested groups: if Ken is a member of a group which is a member of a group which is a member of Fabrikam Managers, well, then Ken is also a member of Fabrikam Managers. However, this script doesn’t deal with situations like that. Why? Because nested groups can get a bit messy, and they require a level of explanation that lies outside the scope of this column. However, you can find a sample script (and an accompanying explanation) for dealing with nested groups in this Scripting Guys Webcast.


How Can I Add a User to a Group, but Only if that User is a Member of the IT Department?

Hey, Scripting Guy! How can I add a user to a group, but only if that user is a member of the IT department?

-- JV

Hey, JV. You know, this is your lucky day: not only are we going to show you how you can add a specified user to a group (assuming, of course, that this user is a member of the IT department), but we’re also going to show you a way to automatically add all members of the IT department to a group. Talk about a deal, huh?

And, no, your thanks are enough for us. Well, that is, your thanks and our standard consulting fee of $12,343.50. We’ll send you a bill.

So what do you get in return for that $12,343.50? (And no, you don’t have to send us $12,343.50. Not unless you want to ….) Well, for starters, you get a script like this one, which adds the user Jack Richins to the group IT Staff … provided, of course, that Jack is a member of the IT department:

Set objUser = GetObject("LDAP://cn=Jack Richins,ou=canada,dc=fabrikam,dc=com")

If objUser.Department = "IT" Then
    Set objGroup = GetObject _
        ("LDAP://cn=IT Staff,ou=support,dc=fabrikam,dc=com")
    objGroup.Add(objUser.ADsPath)
End If

As you can see, this is a simple little script. We begin by using this line of code to bind directly to Jack Richins’ user account in Active Directory:

Set objUser = GetObject("LDAP://cn=Jack Richins,ou=canada,dc=fabrikam,dc=com")

We then check to see whether or not Jack’s Department attribute is equal to IT:

If objUser.Department = "IT" Then

Let’s assume that it is. In that case, we then create a second object reference, one that connects us to the IT Staff group account:

Set objGroup = GetObject _
    ("LDAP://cn=IT Staff,ou=support,dc=fabrikam,dc=com")

Once we’ve made that connection we can then call the Add method (passing the value of Jack’s ADsPath attribute as the sole parameter) and add Jack to the group. If Jack isn’t part of the IT department then we don’t do anything at all.

Not bad, huh? Now here’s the bonus script. This script searches Active Directory and returns a list of all the users (objectCategory='user') who happen to be members of the IT department (Department=’IT’). For each user meeting those criteria (that is, each user in the IT department), the script adds the user to the IT Staff group:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
         "AND Department='IT'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Set objGroup = GetObject _
    ("LDAP://cn=IT Staff,ou=support,dc=fabrikam,dc=com")

Do Until objRecordSet.EOF
    objGroup.Add(objRecordSet.Fields("ADsPath").Value)
    objRecordSet.MoveNext
Loop

We’re not going to talk about the bonus script in any detail; if you aren’t sure how Active Directory scripts work we recommend you take a peek at the two-part Tales from the Script series Dude, Where’s My Printer? About all we will do is mention that the On Error Resume Next statement is very important in this particular script. Why? Well, suppose Jack Richins is already a member of the IT Staff group and you try adding him (again) to the group. That’s going to generate an error and the script will blow up. If you add the On Error Resume Next statement, however, the script won’t blow up; instead, it will simply skip Jack and instead try adding the next user in the IT department.

Note. If you’re wondering how the Scripting Guys came up with a standard consulting fee of $12,343.50, well, it’s purely coincidental that one of the Scripting Guys (with great reluctance) recently purchased a car for his son and the total bill came to $12,343.50. Like we said, purely coincidental ….


How Can I Add All the Users from an Existing Active Directory Group to a New Group?

Hey, Scripting Guy! How can I add all the users from an existing Active Directory group to a new group?

-- TG

Hey, TG. So you have a group (we’ll call it Group A) and you want to create an exact replica of that group (we’ll call it Group B). If we were talking about Text File A and Text File B this would be an easy task: all you’d have to do is make a copy of File A and name that copy File B. Unfortunately, that won’t work with an Active Directory group. Although there is such a thing as a CopyHere method in ADSI, that method isn’t supported by Active Directory; if you try using CopyHere, you’ll just get back a “Not implemented” error message. Bummer.

So if we can’t just make a copy of Group A what can we do? Let’s think about our text files for a moment. Suppose for some reason we couldn’t use the file system to make a copy of File A; how else could we replicate that file? Well, one thing we could do would be to open that file, read in the contents, and then write those contents to our new file, File B. And that’s pretty much the same approach we’re going to use to add all the users from an existing Active Directory group to a new Active Directory group.

Let’s show you the script and then explain how it works. This script grabs a list of all the members of the Finance Managers group and then adds them to a brand-new group named Finance Department:

Const ADS_GROUP_TYPE_GLOBAL_GROUP = &H2

Set objOU = GetObject("LDAP://OU=Finance, dc=fabrikam, dc=com")
Set objOldGroup = GetObject("LDAP://CN=Finance Managers, ou=Finance, dc=fabrikam, dc=com")

Set objNewGroup = objOU.Create("Group", "Finance Department")
objNewGroup.sAMAccountName = "financedept"
objNewGroup.groupType = ADS_GROUP_TYPE_GLOBAL_GROUP
objNewGroup.Set Info

For Each objUser in objOldGroup.Member
    objNewGroup.Add "LDAP://" & objUser
Next

So how does this work? Well, we start by defining a constant named ADS_GROUP_TYPE_GLOBAL_GROUP and setting the value to &H2; we’ll use this later on to make our new group a global security group.

Next we create object references to two different objects. Object reference objOU binds to the Finance OU in Active Directory; this is the OU that will house our new security group. Meanwhile, objOldGroup binds to the Finance Managers group in Active Directory; as we already noted, this group has the list of users we want to add to the new group.

We then take a brief detour to create a new group named Finance Department; that’s what this block of code does:

Set objNewGroup =
objOU.Create("Group", "Finance Department")
objNewGroup.sAMAccountName = "financedept"
objNewGroup.groupType = ADS_GROUP_TYPE_GLOBAL_GROUP
objNewGroup.SetInfo

We won’t go into the details of what we’re doing here and why; for more information, see this section of the Microsoft Windows 2000 Scripting Guide.

We now have an existing group – Finance Managers – that has lots of members, and a new group – Finance Department – that doesn’t have any members. But we can change that: all we have to do is grab the membership from Finance Managers and copy it to Finance Department. Believe it or not, that only takes three lines of code:

For Each objUser in objOldGroup.Member
    objNewGroup.Add "LDAP://" & objUser
Next

As it turns out, the Member attribute of a group contains a collection of all the members of that group. We can use a simple For Each loop to loop through the value of the Member attribute and return the distinguishedName (DN) attribute for each group member.

Note. The distinguishedName attribute will look similar to this:

CN=Ken Myer, ou=Finance, dc=fabrikam, dc=com

Fortuitously enough, the only information we need in order to add a user to a group is the ADsPath for that user. The ADsPath is a combination of the ADSI provider (in this case, LDAP://) plus the user’s DN. We now have both these pieces: the ADSI provider (which has to be LDAP:// seeing as how we’re working with Active Directory) and the user DN. In this line of code, we add the user to the new group, passing as the sole parameter to the Add method a combination of the ADSI provider and the user DN:

objNewGroup.Add "LDAP://" & objUser

We then continue on our merry way, looping through all the members of the Finance Managers group, grabbing the value of their distinguishedName attribute, and using that value to add the user (or computer or group or whatever) to the Finance Department group. The net result: despite the fact that we have no Copy command we’ve managed to duplicate the membership list from one group to another. Can human cloning via VBScript be far behind? (Let’s put it this way: originally there was only one Scripting Guy….)


How Can I Compare the Memberships of Two Groups?

Hey, Scripting Guy! I’m looking for a script that can get the memberships of two different groups, compare them, and then write out the differences. Can you help?

-- LS

Hey, LS. When we received your question a week or so ago we thought, “That’s a good question. Let’s put it on the list and answer it sometime.” Since then we’ve had 3 or 4 more people ask the same question. Well, there’s one thing about us Microsoft Scripting Guys: you might have to ask us 5 or 6 times, but by the 7th time we finally get the hint. Here’s how you can do this.

Because a couple of people specifically mentioned being able to compare the local Administrators group on two different computers that’s the example we’re going to use. But, needless to say, you can compare any two groups using this same technique. Want to compare two groups on the same computer? No problem. Want to compare two Active Directory groups? No problem. How about a local group and an Active Directory group? That’s right: no problem.

To begin with, we need some code that can grab the list of members of our two local groups and then stash that information somewhere so that we can then compare the two lists. Getting the membership is easy; here’s a script that retrieves the members of the Administrators group on the computer atl-ws-01:

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
     Wscript.Echo objUser.Name
Next

Of course, the preceding script echoes the user names to the screen. We don’t really want to that; instead, we want to hang on to those user names so that we can then compare them with the names we get from the second computer. Fortunately, there’s an easy way to do this: we just stuff the names into a Dictionary object:

Set objGroup1 = CreateObject("Scripting.Dictionary")

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    objGroup1.Add objUser.Name, objUser.Name
Next

If you aren’t familiar with the Dictionary object, you might want to check out this section of the Microsoft Windows 2000 Scripting Guide. For now, consider the Dictionary object a collection of key/value pairs; typically you store things like states and their state capitals in a Dictionary object:

Washingon/Olympia
Oregon/Salem
Idaho/Boise

In this example, Idaho is a key and Boise is its corresponding value. Oregon is a key, and Salem is its corresponding value.

The whole idea here is that you can ask a question like, “What is the capital of Washington?” and the script can check the key/value pair and respond, “Olympia.” It does this by searching for the key – Washington – and then reporting back the value: Olympia. Of course, in our case, we’re really only interested in the keys; we want to be able to ask a question like, “Is Bob a member of Group 1?” However, since the Dictionary object requires both a key and a value, we end up putting the same information (the user name) in each. Thus we end up with a list like this:

Bob/Bob
Jill/Jill
Joe/Joe
Susan/Susan

But don’t worry about the values. The important thing is that the collection of keys looks like this:

Bob
Jill
Joe
Susan

If we repeat this process for Computer 2 (using a second Dictionary object to hold the membership of that group), we’ll have a script that looks similar to this:

Set objGroup1 = CreateObject("Scripting.Dictionary")
Set objGroup2 = CreateObject("Scripting.Dictionary")

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    objGroup1.Add objUser.Name,objUser.Name
Next

strComputer = "atl-ws-02"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    objGroup2.Add objUser.Name,objUser.Name
Next

No, sorry, we aren’t done yet. But we’re getting close. We now have the two membership lists, and all we have to do is compare them and write out any differences. How do we do that? As it turns out, you can use a For Each loop to cycle through all the keys in a Dictionary object. For example, if you want to echo all the key names you can use code like this:

colKeys = objGroup1.Keys
For Each strUser in colKeys
    Wscript.Echo strUser
Next

Of course, we don’t want to echo the membership list, we want to find out who is in Group 1 and then check to see if they are also in Group 2. To do that, we use the For Each loop to get the first user in Group 1 – Bob – and we then use the Dictionary object’s Exists method to see if Bob is also in Group 2. If he is, that’s fine; if he’s not, we then echo a message that says “Bob is not in Group 2.” We know, that sounds complicated, but it only takes a few lines of code:

colKeys = objGroup1.Keys
For Each strUser in colKeys
    If Not objGroup2.Exists(strUser) Then
        Wscript.Echo strUser & " is not in Group 2."
    End If
Next

We then repeat the process with Group 2, checking to see if all the users in that group are members of Group 1. When that part of the script ends, we’ll have a list of the differences between the two groups.

Here’s the completed script. Again, it might look a little weird, but that’s simply because people don’t use the Dictionary object very often. It’s actually very simple, though, and even – heaven forbid! – has some logic to it:

Set objGroup1 = CreateObject("Scripting.Dictionary")
Set objGroup2 = CreateObject("Scripting.Dictionary")

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    objGroup1.Add objUser.Name,objUser.Name
Next

strComputer = "atl-ws-02"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    objGroup2.Add objUser.Name,objUser.Name
Next

colKeys = objGroup1.Keys
For Each strUser in colKeys
    If Not objGroup2.Exists(strUser) Then
        Wscript.Echo strUser & " is not in Group 2."
    End If
Next

colKeys = objGroup2.Keys
For Each strUser in colKeys
    If Not objGroup1.Exists(strUser) Then
        Wscript.Echo strUser & " is not in Group 1."
    End If
Next


How Can I Convert a Global Security Group to a Universal Security Group?

Hey, Scripting Guy! How can I convert a global security group to a universal security group?

-- BB

Hey, BB. We have to tell you the truth, we were pretty excited when we first read your email. Convert a global security group to a universal security group? That sounded like a mad scientist sort of thing, and we had visions of setting up a huge lab with all sorts of cool machines and beakers of chemicals, and then one of us would throw a switch and a great bolt of lighting would surge through the room. Everything would be quiet, and then, through the smoke, we’d see our little security group beginning to twitch. “Heaven help us,” we’d say. “We’ve created a universal security group!”

Cool, huh? In fact, we had already written the screenplay and signed a contract for a series of action figures before we found out that we didn’t actually need a mad scientist’s laboratory to convert a global group to a universal group. Instead, all we needed was this:

Const ADS_GROUP_TYPE_UNIVERSAL_GROUP = &H8
Const ADS_GROUP_TYPE_SECURITY_ENABLED = &H80000000

Set objGroup = GetObject _
    ("LDAP://cn=Managers,ou=Finance,dc=fabrikam,dc=com")

objGroup.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP OR ADS_GROUP_TYPE_SECURITY_ENABLED
objGroup.SetInfo

Hey, if you think you’re disappointed imagine how we feel. As it turns out, though, each Active Directory group has a property named groupType that, well, determines the group type. GroupType is a bitmask attribute that can be made up of as many as two different values. Value 1 determines whether the group is a security group or a distribution group. (What’s the difference? Security groups can be given access to resources; distribution groups can’t.) Value 2, meanwhile, determines the type of security group or the type of distribution group. Either security groups or distribution groups can be one of the following:

· Global groups, in which all the users must come from the same domain.

· Domain local groups, in which members can be drawn from any domain in the forest, but permissions can only be granted to the local domain (that is, the domain where the group account resides).

· Universal groups, in which members can be drawn from any domain in the forest and be granted permissions anywhere in the forest.

Fascinating, isn’t it?

By the way, that’s a good question: how can a single attribute consist of more than one value? That’s bitmask attributes for you. We won’t discuss bitmasks in any detail today; if you’d like a more thorough discussion, check out this Scripting Guys webcast. For now, think of the groupType attribute as being like a series of switches. Suppose the switch for security group is on, the switch for global group is on, and all the other switches are off? In that case, we’d have a global security group. That implies that we can convert a group to a universal security group simply by ensuring that only the security group and the universal group switches are on. And that’s exactly what our script does.

Note. This is probably a good time to point out that Active Directory imposes a few limitations when it comes to converting from one group to another. For example, you can’t convert a global group to a domain local group; Active Directory won’t allow that. Likewise, you can only convert groups if your domain is in native mode. Groups in a mixed-mode domain can’t be converted. Check the Active Directory documentation for more information.

These “switches” are actually a series of constants and their corresponding values. In fact, they’re actually this series of constants and their corresponding values:

Group Type | Constant | Value
Global group | ADS_GROUP_TYPE_GLOBAL_GROUP | &H2
Domain local group | ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP | &H4
Universal group | ADS_GROUP_TYPE_UNIVERSAL_GROUP | &H8
Security group | ADS_GROUP_TYPE_SECURITY_ENABLED | &H80000000

To explain how you can set these switches, let’s go back to our script. We start out by defining a pair of constants: ADS_GROUP_TYPE_UNIVERSAL_GROUP and ADS_GROUP_TYPE_SECURITY_ENABLED. Why these two constants? That’s easy: we want to create a universal security group, which means we need to flip the switches for universal group and for security group. If we wanted to create a universal distribution group then we’d only need to flip one switch and define one constant: ADS_GROUP_TYPE_UNIVERSAL_GROUP. (Because, of course, the security group switch needs to be off in order for us to have a distribution group.)

After binding to the group account in Active Directory (for this sample script that’s the Managers group in the Finance OU) we encounter this line of code:

objGroup.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP OR ADS_GROUP_TYPE_SECURITY_ENABLED

This is where we set the value of the groupType attribute. To do that, we pass our two constants as parameters, joining the two with the OR operator. Don’t worry too much about OR; in this case we can read it as though it was the word and: “Set the group type to a universal group and a security group.” If we wanted to convert the group to a global security group we would simply need to define the constant ADS_GROUP_TYPE_GLOBAL_GROUP and then use this code:

objGroup.Put "groupType", ADS_GROUP_TYPE_GLOBAL_GROUP OR ADS_GROUP_TYPE_SECURITY_ENABLED

Only the switch values we pass to groupType will be flipped on; any other switches will be turned off. And, yes, if you pass a nonsensical request – for example, passing all the possible constants – the script will simply fail. Active Directory won’t allow you to create, say a domain local universal security distribution group.

And for good reason.

Make sense? To make it a universal distribution group we’d pass just one parameter, ADS_GROUP_TYPE_UNIVERSAL_GROUP:

objGroup.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP

Again, that’s because we don’t enable the security group switch (ADS_GROUP_TYPE_SECURITY_ENABLED) for distribution groups.

At any rate, after setting the groupType all we have to do is call the SetInfo method and the group will change from a global security group to a universal security group, just like that.

And, no, sorry: the change is made without any bolts of lightning, bursting beakers of chemicals, or other special effects. Hmmm, maybe that’s why they turned down our proposal to use a scripting theme for the latest Mission:Impossible movie.

Oh, well. Wonder if we’re too late to propose a plot for X-Men 3 ….


How Can I Count the Number of Users in a Group?

Hey, Scripting Guy! How can I count the number of users in an Active Directory group?

-- TP

Hey, TP. You know, it’s our sad duty to confirm that the reports currently sweeping the Internet are true. Without wishing to alarm anyone, we must admit that one of the Around the World With Dr. Scripto Bobblehead pictures appears to be a fake! (And no, we’re not talking about the obvious one at the end.)

Please, please; there’s no need to panic. The Scripting Guys are working closely with representatives of the World Health Organization and we are confident that this fake picture poses no health hazard of any kind. (Although doctors do recommend that pregnant women and those with weak hearts stay away from the Script Center until the this issue has been resolved.) In addition, you can rest assured that the safety and well-being of Script Center users remains our highest priority: if the situation begins to get even slightly out of control we will shut down the Script Center and quarantine the Scripting Guys immediately.

Which, come to think of it, is the very thing our manager recommends each time we meet with him.

We should hasten to add that, at this point in time, we do not know for sure that one of the pictures is a fake; we currently have a team of forensic scientists and art experts examining all the evidence. Obviously this is a very serious matter, and we do not want to make any hasty accusations. Because of that, our investigation will take some time to complete. But trust us: we will get to the bottom of this.

And what will we do if we discover that one of the pictures is a fake? To be honest, we haven’t reached a consensus on that; however, Scripting Guy Jean Ross is currently standing by with a team of highly-trained commandos, ready to parachute into action on a moment’s notice. But, then again, Scripting Guy Jean Ross is always ready to parachute into action at a moment’s notice:

“Someone has a question about text files? My team and I will parachute in.”

“There’s a missing period in one of the Sesame Script articles? My team and I will parachute in.”

“Someone brought doughnuts and left them in the kitchen? My team and I will parachute in.”

Note. Shouldn’t someone talk to her about this erratic behavior? You bet they should. On the other hand, she’s continually surrounded by a team of highly-trained commandos. You go tell her she’s acting crazy!

Needless to say, we will keep you informed as more information becomes available. In the meantime, we would like to show you a script that can count the number of users in an Active Directory group. And don’t worry: this script comes with the Scripting Guys Certificate of Authenticity, ensuring that it is a real Scripting Guys script and not a fake.

If it doesn’t say Scripting Guys on it, you don’t know what’s inside.

Here’s the script:

Set objGroup = GetObject("LDAP://CN=Finance Employees,OU=Finance,DC=fabrikam,DC=com")

i = 0

Set objDictionary = CreateObject("Scripting.Dictionary")

For Each strUser in objGroup.Member
    i = i + 1

    Set objMember = GetObject("LDAP://" & strUser)
    strType = objMember.Class

    If Not objDictionary.Exists(strType) Then
        objDictionary.Add strType, "1"
    Else
        objDictionary.Item(strType) = objDictionary.Item(strType) + 1
    End If
Next

Wscript.Echo "Total members in the group: " & i
Wscript.Echo

For Each objType in objDictionary
    Wscript.Echo objType & ": " & objDictionary.Item(objType)
Next

Two things we should note before we go much further. First, the only way to determine the number of members in a group is to go ahead and count those members; ADSI doesn’t have a Count property that can instantly return the number of items in a collection for us. (Or at least it doesn’t have a Count property that actually works.)

Second, we could have written a simpler version of this script, one that looks like this:

Set objGroup = GetObject("LDAP://CN=Finance Employees,OU=Finance,DC=fabrikam,DC=com")

i = 0

For Each strUser in objGroup.Member
    i = i + 1
Next

Wscript.Echo "Total members in the group: " & i
Wscript.Echo

The preceding script does, indeed, echo back the number of members in a group. So why didn’t we just give you that script and call it good? Well, in light of the controversy surrounding the doctored photo, we decided to go the extra step and echo back not only the number of members in a group, but also a tally of the type of group member, like so:

Total members in the group: 840

group: 5
contact: 595
user: 240

That’s just our way of trying to make amends for all the heartache and heartbreak that has no doubt accompanied the doctored photo scandal. Use whichever script best suits your needs, and with our compliments.

As for the original script (the fancy one), that one kicks off by connecting to the group in question, in this case a group named Finance Employees, housed in the Finance OU of fabrikam.com:

Set objGroup = GetObject("LDAP://CN=Finance Employees,OU=Finance,DC=fabrikam,DC=com")

After we make a connection to the group account in Active Directory, we set the value of a counter variable named i to 1; we’ll use this variable to tally the number of members in the group. From there we then create an instance of the Scripting.Dictionary object, the object we’ll use to keep track of the different member types (like user, contact, and group):

Set objDictionary = CreateObject("Scripting.Dictionary")

That brings us to this line of code:

For Each strUser in objGroup.Member

As it turns out, Active Directory groups all have a multi-valued property named Member; this property contains a collection of all the members of that group. In order to get at the group membership all we have to do is set up a For Each loop to loop through all the values stored in the Member property.

Which, coincidentally enough, is exactly what we did in our script.

Inside the loop the first thing we do is increment the value of our counter variable. Each time we go through the loop we increment the value of i. Why? Well, suppose we have 37 members in the group. In that case, we’re going to run through the loop 37 times; when we exit the loop, i will be equal to 37. Which, now that we think about it, means that i will thus be able to tell us how many members are in the group.

Was that lucky or what?

As you may or may not know, individual group members are listed in the Member attribute by distinguished name (e.g., CN= Ken Myer, OU=Finance, dc=fabrikam, dc=com). Is that useful? You bet it is; that means we can bind to the individual member account using the following line of code:

Set objMember = GetObject("LDAP://" & strUser)

Why do we even want to bind to the individual member account? Well, in addition to keeping a simple tally of the number of members in the group, we want to determine the member type; the only way to do that is to bind to the account and retrieve the value of the Class property. That’s what we do here:

strType = objMember.Class

At this point we know that the group member is a user (or a contact or a group or whatever). But now what? Well, as we noted, we’re going to use our Dictionary object to keep a running tally of member types. With that in mind, our next step is to check and see whether this particular member type is already in the Dictionary:

If Not objDictionary.Exists(strType) Then

Suppose the preceding line of code comes back True; that means the member type is not in the Dictionary. (Notice the If Not syntax.) In that case, we use the following line of code to add the type to the Dictionary and set the value of the new item to 1:

objDictionary.Add strType, "1"

Got that? If not, you might want to take a look at the Microsoft Windows 2000 Scripting Guide for a more detailed look at the Dictionary object.

But what if the member type can be found in the Dictionary? That’s fine; if the member type already exists in the Dictionary then we simply use this line of code to increment the item value by 1:

objDictionary.Item(strType) = objDictionary.Item(strType) + 1

And then we loop around and repeat the process for the next member in the group.

After we’ve looped through the entire group membership we echo back the total number of members in the group (represented by our counter variable i), followed by a blank line:

Wscript.Echo "Total members in the group: " & i
Wscript.Echo

And then we set up a For Each loop to loop through all the items in the Dictionary, echoing back the member type and the item value:

For Each objType in objDictionary
    Wscript.Echo objType & ": " & objDictionary.Item(objType)
Next

And that, as you might expect, is that.

Oh, one more thing: if you’d like to count the number of members in a local group, use this script instead:

strComputer = "atl-fs-01"

i = 0

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")

For Each strMember in objGroup.Members
    i = i + 1
Next

Wscript.Echo "Total members in the group: " & i

We won’t bother explaining how this script works, at least not today. But that’s OK; we have other sources of information that explain how to work with local groups.

Which brings us back around to the burning question of the day: are the Scripting Guys upset that someone submitted a doctored photo for publication in the Script Center? Well, to be honest, when it was pointed out to us, we thought it was kind of funny. (And, truth be told, it’s possible that we might have used a doctored photo or two ourselves.) So no, we’re not upset: if you want to take the trouble to create a doctored photo of Dr. Scripto, well, who are we to stop you?

Besides, there are even unsubstantiated rumors that Dr. Scripto himself isn’t real, that he’s just a piece of clip art. Is there any truth to those rumors? We don’t know. But you can bet that Jean Ross and her Delta Scripting Force will find out.

Just as soon as they finish those doughnuts they liberated from the kitchen.


How Can I Delete All the Users in an Active Directory Group?

Hey, Scripting Guy! How can I delete all the users in an Active Directory group?

-- AA

Hey, AA. You know, that’s something we Scripting Guys never thought about: a script that could eliminate all the users in a particular group. The only problem we have with that is: where do we start? For example, maybe we start with the teenagers a few houses down, the same group that occasionally gets the uncontrollable urge to go outside and skateboard at 3:00 in the morning. (Although, to be fair, who among us doesn’t occasionally get the uncontrollable urge to skateboard at 3:00 in the morning?) Or maybe we start with that group of baseball players, the ones who clapped their hands, waved their arms, and yelled at the pitcher as he prepared to pitch. (Too bad the Scripting Son didn’t pitch that game; he would have known exactly what to do with that group.)

And then there’s all those groups here at work, like the one where all the people – What’s that? All you want to do is remove the group memberships for all the users in a specified Active Directory group? Oh, sure, no problem; we can do that.

What were we thinking about? Um, pretty much the same thing, deleting the group memberships for all the users in a specified Active Directory group.

Pretty much.

As it turns out, if all you want to do is delete a few Active Directory group memberships, well, that can be done using a script no more complicated than this:

Const ADS_PROPERTY_CLEAR = 1

Set objGroup = GetObject("LDAP://cn=Finance Users,ou=Finance,dc=fabrikam,dc=com")

objGroup.PutEx ADS_PROPERTY_CLEAR, "member", 0
objGroup.SetInfo

We agree: if only deleting other kinds of groups was that easy! As you can see, we start out by defining a constant named ADS_PROPERTY_CLEAR and setting the value to 1; we’ll use this constant to tell the script that we want to clear all the values of the group’s Member attribute. (Needless to say, the Member attribute is a multi-valued attribute that contains the list of group members.) After defining the constant we then use this line of code to connect to the group in question, in this case the Finance Users group located in fabrikam.com’s Finance OU:

Set objGroup = GetObject("LDAP://cn=Finance Users,ou=Finance,dc=fabrikam,dc=com")

Amazingly enough, we’re halfway done at this point. After binding to the group account we use the PutEx method to actually clear the group membership. (The PutEx method is an ADSI method designed to work with multi-valued attributes, attributes that can contain multiple values, like multiple user names.) We need to pass PutEx three parameters:

· ADS_PROPERTY_CLEAR, the constant that tells PutEx we want to delete all the values in the specified attribute. Other constants – and their corresponding values – would enable us to do things like add new members to the group or delete specified members from the group.

· Member, the multi-valued attribute we want to clear.

· 0, the new value being assigned to the attribute. Technically it doesn’t matter what value we specify here: when you perform an operation using ADS_PROPERTY_CLEAR ADSI ignores this third parameter. However, if you leave the parameter out you’ll get a “Wrong number of arguments” error. We put a 0 here simply as a reminder that, when the script finishes, the Finance Users group will have 0 members.

After calling the PutEx method we’re left with just one final task: we need to call the SetInfo method to write these changes (i.e., delete all the group memberships) back to Active Directory. That’s what we do with this line of code:

objGroup.SetInfo

Execute that last line and, just like that, all the members of the Finance Users group will be deleted.

Now, if we could just remove all the members of the people-who-honk-their-horns-before-the-light-even-turns-green group, well, then we’d really be on to something, wouldn’t we?


How Can I Determine Whether or Not a Group Has Any Members?

Hey, Scripting Guy! How can I check a computer and find out whether the Remote Desktop Users group has any members?

-- ET

Hey, ET. Somewhat surprisingly, ADSI doesn’t have any sort of NumberOfMembers property, a property that could tell you – at a glance – how many members are in a group. But that’s all right, because we can simply iterate through the group’s membership and count the members as we go. Here’s a script that binds to the Remote Desktop Users group on the computer atl-ws-01 and then counts the number of users in that group:

strComputer = "atl-ws-01"

Set objGroup = GetObject("WinNT://" & strComputer & "/Remote Desktop Users")
i = 0
For Each objUser in objGroup.Members
    i = i + 1
Next

Wscript.Echo "Number of users in group: " & i

If it’s not immediately clear to you, here’s how we count the number of members. We begin by setting a counter variable (i) to 0. We then use a For-Each loop to loop through the group membership. Each time we run through the loop, we increment the value of i by 1. Suppose we have 23 members in the group. In that case, we’ll run through the loop 23 times and – that’s right, at the end i will be equal to 23. We then echo the value of i (and, by extension, the number of members in the group).

But what if the group doesn’t have any members? In that case, we’ll never run through the For-Each loop, and the value of i will remain at 0. Thus we’ll end up echoing this message at the end of the script:

Number of users in group: 0

Of course, you didn’t necessarily want to know the exact number of members in the Remote Desktop Users group; you just wanted to know if the group had any members. So here’s a variation of the script. In this script, if we enter the For-Each loop (which we’ll enter only if the group has at least one member) we echo the fact that the group has at least one member and then quit. If we don’t enter the For-Each loop, then we echo the fact that the group doesn’t have any members. In other words:

strComputer = "atl-ws-01"

Set objGroup = GetObject("WinNT://" & strComputer & "/Remote Desktop Users")

For Each objUser in objGroup.Members
    Wscript.Echo "Remote Desktop Users has at least one member."
    Wscript.Quit
Next

Wscript.Echo "Remote Desktop Users does not have any members."


How Can I Determine Which Groups a User Belongs To?

Hey, Scripting Guy! In my logon script, how can I find out which Active Directory groups a user belongs to?

-- JB, Montpelier, VT

Hey, JB. This is pretty easy to do in a logon script:

On Error Resume Next
Set objADSysInfo = CreateObject("ADSystemInfo")
strUser = objADSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)
For Each strGroup in objUser.memberOf
    Set objGroup = GetObject("LDAP://" & strGroup)
    Wscript.Echo objGroup.CN
Next

So what’s going on here? Well, we begin by using the ADSystemInfo object to determine the distinguished name of the logged-on user; that will be a name similar to this:

CN=kenmyer, OU=Managers, DC=fabrikam, DC=com

As soon as we have the distinguished name, we can use the LDAP provider to bind to the user account in Active Directory. One of the properties of an Active Directory user account is memberOf, an array consisting of all the groups the user belongs to. Because memberOf is an array, we can use a For Each loop to list all the groups.

When we report back the group names, however, we do one last thing. By default groups are stored by distinguished name in the memberOf property; thus you get back things like this:

CN=Production Leads, OU=Managers, DC=fabrikam, DC=com

Distinguished names are great for binding to Active Directory, but less useful for answering questions like, “Does this user belong to the Production Leads group?” So we take one extra step and, after retrieving the distinguished name for a group, we then bind to the group account in Active Directory. By doing so we can retrieve the CN (common name) for the group, and thus report back group names like this:

Production Leads

A bit easier to deal with, to say the least.

Two things to keep in mind here. First, this script runs only on Windows 2000, Windows XP, and Windows 2003; that’s because the ADSystemInfo object isn’t supported on Windows NT 4.0 or Windows 98. Second, this script returns only the groups where the user is individually named as a member. So what, you ask? Well, suppose the user is a member of Group A, and Group A happens to be a member of Group B. This script can’t identify groups within groups; that requires a slightly more complicated bit of coding, something we’ll take up later.


How Can I Enumerate All the Universal Groups in Active Directory?

Hey, Scripting Guy! How can I enumerate all the universal groups in Active Directory?

-- MW

Hey, MW. You know, we’re glad you asked this question. Any time people talk about Active Directory then tend to talk about groups as if Active Directory had only one type of group. It doesn’t. Instead, there are six types of Active Directory groups: global, domain local, and universal security groups; and global, domain local, and universal distribution groups. And it’s important to be able to distinguish between the various types: after all, your ability to perform certain tasks, such as assigning security permissions or adding users to groups, often hinges on the group type.

So how do you list all the universal groups in Active Directory? The best way is by using ADO to conduct a search. The tricky part there is understanding how to search for different group types. Each group object has an attribute named groupType, but group types are not stored by name. Consequently, a query like this, which purports to search for all universal security groups, won’t work:

"SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='group'" & _
"AND groupType = 'universal security'"

Instead, group types are assigned numbers, and those numbers are stored in the groupType attribute:

Group Type | Value
Global group | 2
Domain local group | 4
Universal group | 8
Security group | -2147483648

To search for all the universal groups, you search for groups that have a groupType value of 8. If you want to search for only universal security groups, then you need to add the value for universal groups to the value for security groups. In other words, 8 + -2147483648, which happens to equal -2147483640. To return a list of all the universal security groups in a domain, you use this query:

"SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='group'" & _
        "AND groupType = -2147483640"

After you understand how to construct the query, the rest of the script is boilerplate: it resembles every other Active Directory search script you’ve ever written. We won’t discuss the details of searching Active Directory today; that goes a bit beyond the scope of this column. But if you’d like to learn more about searching Active Directory, you might take a look at our two-part Tales from the Script series (from April and May, 2005) that covers this very topic in-depth.

Here’s a completed script that returns a list of all the universal security groups found in the fabrikam.com domain:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='group'" & _
        "AND groupType = -2147483640"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop


How Can I Get a List of All the Groups Whose Name Starts with G-S-Group?

Hey, Scripting Guy! How can I use wildcards to get a list of all the groups whose name starts with G-S-Group?

-- JD

Hey, JD. It’s the day before Thanksgiving, and one of the Scripting Guys is headed out of town as soon as he finishes his Hey, Scripting Guy! column. And what a coincidence: he happened to pick a question about searching Active Directory, the kind of column he could write in his sleep (and probably has).

The reason Active Directory search columns are so easy to write is the fact that we cheat: we typically don’t explain the details behind such searches. Active Directory searches are actually pretty easy; after all, most of the code is boilerplate that doesn’t need to change from script-to-script. However, there is a considerable amount of boilerplate code, and that requires explanation that goes beyond the scope of this little column. Therefore, we typically refer people to our two-part Tales from the Script series Dude, Where’s My Printer? if they need more information about writing scripts to search Active Directory.

Note. You want to know how hard it is to be a Scripting Guy? Well, let’s see. This particular Scripting Guy writes a column that simply refers people to previously-written material. And then he celebrates Thanksgiving by making his mom do all the cooking. A very tough life indeed.

But we won’t be totally lazy; at least we’ll show you the code and explain how the query works. Here’s a script that returns all the groups whose Name starts with G-S-Group:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name FROM 'LDAP://DC=fabrikam,DC=com' WHERE
objectCategory='group' " & _
"AND Name='G-S-Group*'"

Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Wscript.Echo objRecordSet.Fields("Name").Value
objRecordSet.MoveNext
Loop

The part we’re interested in is the SQL query that returns just those groups that meet our criteria (names starting with G-S-Group):

"SELECT Name FROM 'LDAP://DC=fabrikam,DC=com' WHERE objectCategory='group' " & _
        "AND Name='G-S-Group*'"

As you can see, we need to specify two things in the Where clause: 1) the objectCategory must be equal to group (that way, we make sure we get back only groups and not users, OUs, printers, or anything else found in Active Directory); and 2) the Name attribute must begin with the letters G-S-Group. To specify the latter, we set the search value of Name to the desired string – G-S-Group – followed by an asterisk (*), the asterisk being short for “anything.” What we’re saying here is pretty straightforward: “Show me all the groups whose name starts with G-S-Group; I don’t care what – if anything – comes after that.”

You can’t get much more straightforward than that.

If we wanted to search for groups whose Name ended in G-S-Group we’d put the asterisk before the string:

Name='*G-S-Group'

That’s read as, “Show me all the groups whose name ends with G-S-Group; I don’t care what – if anything – comes before that.” Or we could search for groups that have the string G-S-Group somewhere (anywhere) in their name. To do that, we put asterisks before and after the string:

Name='*G-S-Group*'

All part of the fun – and the utility – of using scripts to search Active Directory.

As for us, there’s a turkey out there somewhere with our name on it. See you all again on Monday.


How Can I List All the Groups in an OU?

Hey, Scripting Guy! How can I list all the groups in an OU?

-- MK

Hey, MK. You know, sometimes people ask us a question and then immediately apologize for having asked it, believing that the question is “way too easy for you guys.” In turn, that seems to imply that the Scripting Guys aren’t really guys at all but simply great masses of intellectual energy that sit around all day contemplating the mysteries of the cosmos. Yes, just like the old Star Trek episode where the beings from some planet had become nothing but brains, having outgrown the need for their bodies. (Some of us Scripting Guys would like to get rid of our bodies, too, but that’s a different story.)

If the truth be told, though, the Scripting Guys like easy questions. For one thing, they’re easy; even great masses of intellectual energy deserve a break once in a while. More important, we know these same questions are shared by many other readers of this column. (Hey, as great masses of intellectual energy we know everything.) So don’t be afraid to ask easy questions; we encourage them. And if you have hard questions, well, maybe you could track down those brain guys from Star Trek. Sure, we could answer those questions ourselves, but seeing as how Star Trek was cancelled over 35 years ago those brain guys could probably use the work.

So how do you get a list of all the groups in an OU? Like this:

Set objOU = GetObject("LDAP://ou=Finance,dc=fabrikam,dc=com")
objOU.Filter = Array("Group")

For Each objGroup in objOU
    Wscript.Echo objGroup.Name
Next

We begin by binding to the Finance OU in Active Directory. This, by the way, brings up another question we get asked all the time: how do you bind to an OU that’s inside another OU? For example, suppose the Finance OU was in an OU named NorthAmerica? How would you bind to the Finance OU then?

Wow; that’s two easy questions in one day. To bind to a sub-OU all you have to do is specify the complete path to that OU; in other words:

Set objOU = GetObject("LDAP://ou=Finance,ou=NorthAmerica,dc=fabrikam,dc=com")

See how that works? We bind to the Finance OU, which is in the NorthAmerica OU, which is in fabrikam.com. Etc. etc.

Having made a connection to the OU we then apply a Filter, which in this case limits the returned data to group accounts:

objOU.Filter = Array("Group")

Note that values passed to the filter must be in the form of an array. That might seem silly – an array of just one object? – but this allows us to filter on multiple object types. For example, suppose we wanted to return a list of groups and computers. In that case, we would use code similar to this:

objOU.Filter = Array("Group", "Computer")

Finally, we set up a For Each loop and echo back the name of each item in the collection; thanks to the filter, the only things in the collection will be group accounts.

Now, if you’ll excuse us, we have to go back to figuring out why sushi – fish that hasn’t been cooked – costs way more than the exact same fish that has been cooked. (And, yes, we were going to try to figure out why people would eat raw fish in the first place, but there are some things that are beyond the reach of even great masses of intellectual energy.)


How Can I List the Members of a Group in Alphabetical Order?

Hey, Scripting Guy! I have a script that returns the names of all the users in an Active Directory group. How can I sort those names in alphabetical order?

-- JW

Hey, JW. Ah, yes, sorting data: the bane of script writers everywhere. Unfortunately, VBScript doesn’t have a built-in sorting mechanism, and neither does ADSI. Consequently, most people believe they have no choice but to live with data sorted by whatever order ADSI or WMI or imposes on them. So is that the case? Are you truly at the mercy of ADSI when it comes to returning group membership in alphabetic order? Hey, you know what they say: where there’s a will, there’s a way.

There are actually a couple different ways you can sort data, and if you’d like to know more we encourage you to tune in to Scripting Week 2, a series of 10 webcasts to be held in January, 2005. (The schedule hasn’t officially been finalized, but details will be available in the Script Center very soon.) Because you’re only dealing with a single item – user names – we’ll show you how to use a simple bubble sort to return these names in alphabetical order. If you need to deal with – and sort – a bunch items (user names, email addresses, profile paths, etc.) you don’t want to use a bubble sort; that would be way too complicated. But for sorting a single list like this, the bubble sort works pretty well, and requires only a few lines of code.

Here’s what our script looks like:

Dim arrNames()
intSize = 0

Set objGroup = GetObject("LDAP://CN=Accountants,OU=Finance,DC=fabrikam,DC=com")

For Each strUser in objGroup.Member
    Set objUser =  GetObject("LDAP://" & strUser)
    ReDim Preserve arrNames(intSize)
    arrNames(intSize) = objUser.CN
    intSize = intSize + 1
Next

For i = (UBound(arrNames) - 1) to 0 Step -1
    For j= 0 to i
        If UCase(arrNames(j)) > UCase(arrNames(j+1)) Then
            strHolder = arrNames(j+1)
            arrNames(j+1) = arrNames(j)
            arrNames(j) = strHolder
        End If
    Next
Next

For Each strName in arrNames
    Wscript.Echo strName
Next

We begin by creating a dynamic array named arrNames (for more information about dynamic arrays, see this portion of the Microsoft Windows 2000 Scripting Guide). Why do we do this? Well, we’ve already determined that ADSI doesn’t have a built-in sorting mechanism; therefore, we can’t sort the data as it comes back. Instead, we have to grab the data, temporarily store it in an array, and then sort that array.

Next we use this line of code to bind to the Accountants group in our hypothetical Active Directory:

Set objGroup = GetObject("LDAP://CN=Accountants,OU=Finance,DC=fabrikam,DC=com")

When we bind to a group, one of the properties we get back is the Member property, which just happens to hold the membership list for the group. If we just wanted to echo back the distinguished name of each group member, and if we didn’t care about sorting the membership list, we could use code like this:

For Each objMember in objGroup.Member
    Wscript.Echo objMember
Next

But that’s not what we want, is it? First of all, we probably don’t want distinguished names like CN=Ken Myer, OU=Finance, DC=fabrikam, DC=com. Instead, we probably want plain old common names (CNs), names like Ken Myer. Consequently, inside our For-Each loop we bind to each user account, the better to retrieve the CN for each user. We use this code to bind to the individual user accounts:

Set objUser =  GetObject("LDAP://" & strUser)

Second, we don’t want to echo the user names right now; instead, we want to stash those names in our array and then sort them before we actually display them inscreen. That’s what this code does; it grabs each user’s CN and creates a spot to store that CN within the array arrNames:

ReDim Preserve arrNames(intSize)
arrNames(intSize) = objUser.CN
intSize = intSize + 1

(Yeah, we know: if you aren’t used to working with arrays, this might look more like gobbledygook than it does scripting code. If you need an explanation, check out the Microsoft Windows 2000 Scripting Guide.)

Eventually, arrNames will contain the CNs for each user in the Accountants group. What we need to do then is sort that array using a bubble sort. Explaining the ins and outs of the bubble sort is more than we can handle in this column; we’ll explain it a bit better during Scripting Week 2. For now, we’ll just say that the bubble sort methodically compares each item in the collection against every other item, slowly by surely determining the alphabetical order. For example, suppose our array looked like this:

Sam
Mary
Abigail

The bubble sort would begin by comparing Sam and Mary. Because, alphabetically-speaking, Mary comes before Sam, the bubble sort switches their places in the array. The array then looks like this:

Mary
Sam
Abigail

Next the sort compares Sam and Abigail and, again, swaps their places in the array. Now the array looks like this:

Mary
Abigail
Sam

We now know that Sam is the very last item in the array. It has to be; we’ve compared Sam with every other item. Therefore, in this simple example, the bubble sort is finished with Sam, and only has to compare Mary with Abigail. The script compares the two, swaps their places, and gives us this final, sorted list:

Abigail
Mary
Sam

That’s the basic idea anyway and, admittedly, the actual code looks a bit more complicated:

For i = (UBound(arrNames) - 1) to 0 Step -1
    For j= 0 to i
        If UCase(arrNames(j)) > UCase(arrNames(j+1)) Then
            strHolder = arrNames(j+1)
            arrNames(j+1) = arrNames(j)
            arrNames(j) = strHolder
        End If
    Next
Next

If you take the time to trace what’s happening, though, you’ll see that the code follows the process we described above; it’s not as bad as it looks. The one tricky thing to note here is that we must convert each name to uppercase (using the UCase function) before making our comparisons. That’s because VBScript makes comparisons using the ASCII values of individual letters. In ASCII, uppercase letters always come before lowercase letters; thus Zachary would come before anne. Converting everything to uppercase ensures that ANNE comes before ZACHARY.

Finally, we use another For-Each loop to cycle through the array arrNames and echo the user names in alphabetical order.

As we noted the bubble sort is not the only way to sort data, and it has its limitations: it’s best used when you have only a single field (like user name) to worry about, and can be slow if you have hundreds of items in your list. If the bubble sort won’t handle your sorting needs, you can either way for Scripting Week 2, or take a look at the brief discussion of Disconnected Recordsets found in the Scripting Guide.


How Can I Rename an Active Directory Group?

Hey, Scripting Guy! How can I rename an Active Directory group?

-- CL

Hey, CL. This is actually pretty easy; as you’ll see, it only takes two lines of code. It’s not a hard problem, it’s just a little bit tricky. That’s because ADSI (at least when it comes to dealing with Active Directory) does not have a Rename method. Instead, you need to use the MoveHere method to “move” the account from its current ADsPath to a new ADsPath. The trick is that the new path differs in only one respect: the CN (common name). Giving the object a new CN effectively renames it.

Confused? We don’t blame you. So let’s give you an example. Suppose we have a group with an ADsPath similar to this (remember, the ADsPath is the unique path that allows us to locate the object in Active Directory):

LDAP://cn=FinAdmins, ou=Finance, dc=fabrikam, dc=com

Notice that the CN for the group is FinAdmins. We’d like the CN for the group to be Finance Managers; in other words, we’d like the ADsPath for the object to look like this:

LDAP://cn=Finance Managers, ou=Finance, dc=fabrikam, dc=com

We know what you’re thinking, but Active Directory won’t let us directly change the value of the CN attribute; that is, we can’t use code like this to change the CN:

objGroup.CN = "Finance Managers"
objGroup.SetInfo

But that’s all right; we’ll just “move” the group. The group started out with this ADsPath:

LDAP://cn=FinAdmins, ou=Finance, dc=fabrikam, dc=com

Now we’re going to move it to this ADsPath:

LDAP://cn=Finance Managers, ou=Finance, dc=fabrikam, dc=com

The group will still be in the Finance OU, and it will still have all its existing properties and members; it’s the same group, it just has a new ADsPath, and a new CN. Yes, it’s a little weird, but, hey, it works. And it’s no different than using the Move command to rename a file. If you type this from the command prompt, it will rename the file C:\Logs\New.log to C:\Logs\Old.log:

move c:\logs\new.log c:\logs\old.log

Same idea.

So what does the code actually look like to rename the group? It looks an awful lot like this:

Set objDomain = GetObject("LDAP://ou=finance, dc=fabrikam, dc=com")
objDomain.MoveHere _
     "LDAP://cn=FinAdmins,ou=Finance,dc=fabrikam,dc=com", "cn=Finance Managers"

Like we said, pretty easy. We bind to the Finance OU (the container where the group account resides), and then call the MoveHere method. We need to pass MoveHere two parameters: the ADsPath to the object we want to rename (LDAP://cn=FinAdmins,ou=Finance,dc=fabrikam,dc=com) , and then new CN for the object (cn=Finance Managers). That’s it. Run the script, and the group will be renamed.


How Can I Return Information For Each Member in a Group?

Hey, Scripting Guy! How can I write a script that, for each member in a group, returns the member’s name, department, address, title, and description?

-- TL-V

Hey, TL-V. You would have to mention the word “script,” wouldn’t you? Usually that wouldn’t bother us too much, but in the past 9 days or so the Scripting Guy who writes this column has looked at, tested, and recorded scores for 2,000 or so scripts, all as part of the 2007 Winter Scripting Games. Likewise, the Scripting Guy who doesn’t write this column has looked at, tested, and recorded scores for 2,000 or so scripts herself. Plus we still have several hundred more scripts that haven’t been looked at, tested, or had their scores recorded. To tell you the truth, we’re all scripted out, TL-V; if we ever see another script again it’ll be too soon.

But don’t worry. After all, there are plenty of places you can turn to for help; it’s not like we’re the only daily scripting column here on TechNet. Why there’s … and there’s … um ….

Hmmm ….

OK, fine, we’ll help you out, just this once. But if anyone asks where you got this script, whatever you do don’t tell them. The last thing we Scripting Guys need is a bunch of people asking us questions about scripting.

Note. OK, forget that last part. The Scripting Editor, who recently bought a new car, just reminded us that we do need a bunch of people asking us questions about scripting. We think she said something about job security, but, if you want to know the truth, we weren’t paying that much attention; she does ramble on and on, you know. And if you’ve ever read Hey, Scripting Guy! then you know how much we dislike people who ramble on and on without ever getting to the point.

Which reminds us of a funny story involving the Scripting Son and a baseball game ….

At any rate, here’s a script that can return information about all the members of a group:

Set objGroup = GetObject("LDAP://CN=Managers,OU=Finance,DC=fabrikam,DC=com")

For Each objUser in objGroup.Members
    Wscript.Echo "Name: " & objUser.DisplayName
    Wscript.Echo "Department: " & objUser.department
    Wscript.Echo "Street address: " & objUser.streetAddress
    Wscript.Echo "Title: " & objUser.title
    Wscript.Echo "Description: " & objUser.description
    Wscript.Echo
Next

As you can see, there’s not much to this script. We start out by binding to the desired group in Active Directory; in our sample script, that’s the Managers group, which resides in the Finance OU of fabrikam.com:

Set objGroup = GetObject("LDAP://CN=Managers,OU=Finance,DC=fabrikam,DC=com")

As long as we’re on the subject, one question we get asked over and over again is this: how can I bind to a group that resides in a child OU of another OU?. In other words, how can I bind to a group that resides in the Accounting OU, which, in turn, resides in the Finance OU? We’ve addressed that question before, but it’s worth repeating here: you simply include both OUs in your binding string. Something like this:

Set objGroup = GetObject("LDAP://CN=Managers,OU=Accounting,OU=Finance,DC=fabrikam,DC=com")

The trick here is to always work from back to front when creating your binding string. The object’s CN always comes first, followed by the child OU, the parent OU, and then the domain information. Suppose the account lived in the Payables OU, which was a child OU of Accounting, which, of course, is a child of the Finance OU. That’s fine: start with Payables, tack on Accounting, then continue on through Finance:

Set objGroup = GetObject("LDAP://CN=Managers,OU=Payables,OU=Accounting,OU=Finance,DC=fabrikam,DC=com")

OK, now back to our regularly-scheduled program. As it turns out, one of the properties of an Active Directory group is the Members attribute; as the name implies, this attribute contains information about all the members in the group. Whoa, wait a second: don’t just try echoing the value of the Members attribute. Why not? Because Members happens to be a multi-valued attribute, an attribute that can contain more than one value. (Which makes sense; after all, groups typically contain more than one member.) Any time you’re dealing with a multi-valued attribute you need to set up a For Each loop in order to walk through all the values in that attribute (in this case, all the members of the group). That’s why we have this line of code:

For Each objUser in objGroup.Members

And then, once we’re inside the loop, we can echo back the desired information for each group member:

Wscript.Echo "Name: " & objUser.DisplayName
Wscript.Echo "Department: " & objUser.department
Wscript.Echo "Street address: " & objUser.streetAddress
Wscript.Echo "Title: " & objUser.title
Wscript.Echo "Description: " & objUser.description

And that should do it, TL-V. When all is said and done you’ll get back information similar to this for each user listed as a group member:

Name: Ken Myer
Department: Accounts Receivable
Street address: Building 8, Room 435
Title: Senior Account Manager
Description: Accounts Receivable manager

You know, that’s a good point: if the Scripting Guys already have several hundred unscored Scripting Games entries lying around, well, what difference would a few more make? In other words, if you haven’t entered the Scripting Games yet there’s still time to complete events 9 and/or 10. Submit a script for at least one of those events and you’ll be eligible to win a Dr. Scripto Bobblehead doll or a copy of the book Windows PowerShell in Action. More important, you’ll be giving the Scripting Guys an opportunity to do what they love best.

Oh, wait, never mind. Instead, you’ll be giving us a chance to look at, test, and record scores for scripts, won’t you?


How Can I Tell Whether a Group is a Security Group or a Distribution Group?

Hey, Scripting Guy! Is there any way to tell whether an Active Directory group is a security group or a distribution group?

-- AW

Hey, AW. As a matter of fact, there is; this script will tell you what type of group you’re dealing with:

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers, ou=Finance, dc=Fabrikam, dc=com")
Wscript.Echo objGroup.groupType

Pretty easy, huh?

Well, OK, maybe not. The preceding script works just fine, but it reports back a group type like -2147483640 or -2147483646 or maybe even 4. What the heck is going on?

As it turns out, the group type is not stored in Active Directory as a string value; that is, if you echo the value of the groupType attribute you’re not going to get back something like Global distribution group. Instead, you’re going to back one of the following numbers:

Value | GroupType
2 | Global distribution group
4 | Domain local distribution group
8 | Universal distribution group
-2147483646 | Global security group
-2147483644 | Domain local security group
-2147483640 | Universal security group

In case you’re interested, the values 2, 4, and 8 identify – respectively – global, domain local, and universal groups. The value -2147483648 identifies security groups. To determine the group type you add the first number (2, 4, or 8) to the second number (-2147483648 if the group is a security group, 0 if it’s a distribution group). A domain local distribution group has a value of 4 (4 + 0); a domain local security group has a value of -2147483644 (4 + -2147483648).

But you don’t need to worry about where these numbers come from; all you need to know is which number matches up with which groupType. With that information, you can add a Select Case statement to your script and precisely identify the group type:

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers, ou=Finance, dc=Fabrikam, dc=com")
Select Case objGroup.GroupType
    Case 2
        Wscript.Echo "This is a global distribution group."
    Case 4
        Wscript.Echo "This is a domain local distribution group."
    Case 8
        Wscript.Echo "This is a universal distribution group."
    Case -2147483646
        Wscript.Echo "This is a global security group."
    Case -2147483644
        Wscript.Echo "This is a domain local security group."
    Case -2147483640
        Wscript.Echo "This is a universal security group."
End Select

If all you care about is whether the group is a security group or a distribution group, then you could simply check to see if the groupType value is less than 0. If it is, then the group has to be a security group. Here’s a script that does that very thing:

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers, ou=Finance, dc=Fabrikam, dc=com")
If objGroup.groupType < 0 Then
    Wscript.Echo "This is a security group."
Else
    Wscript.Echo "This is a distribution group."
End If


How Can I Tell Whether a Group Member is a User, a Computer, or Another Group?

Hey, Scripting Guy! How can I tell whether the member of a group is a user, a computer, or another group?

-- ON

Hey, ON. Interesting question and – to tell you the truth – one we’d never really thought about. It’s easy enough to list all the members of a group; for example, here’s a script that reports back all the members of the Finance Managers group:

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers, ou=Finance, dc=fabrikam, dc=com")

For Each strUser on objGroup.Member
        Wscript.Echo strUser
Next

When you run this script you’ll get back the value of the distinguishedName (DN) attribute for each member of the group; that output will look something like this:

cn=atl-ws-01, ou=Finance, dc=fabrikam, dc=com
cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com
cn=North American Finance Users, ou=Finance, dc=fabrikam, dc=com
cn=Pilar Ackerman, ou=Finance, dc=fabrikam, dc=com

So what’s wrong with that? Well, nothing, except that – depending on your naming conventions – it might be difficult to look at the list and determine whether a group member is a user, a computer, or even another group. Likewise, your script might be designed to take specific action based on the item type; for example, if a group member happens to be another group you might want to use a recursive function to connect to and list the members of that group. But you can’t do that unless you can first determine whether or not the member actually is another group.

So how can we distinguish users from computers and groups from whatever? As it turns out all Active Directory objects have an attribute named Class. (Yes, in Active Directory even the Scripting Guys have Class!) The Class attribute can tell you what kind of object you’re dealing with: a user, a computer, a group, whatever. To tell the difference all you have to do is examine the value of the Class attribute.

Of course, there is one tiny catch. When you enumerate group members the only attribute you get back is the member’s distinguished name; you don’t get back the Class attribute. That means you can’t just enumerate group members and their Class types. Instead, you need to bind to each individual group member’s Active Directory account and then echo back the value of the Class attribute. But don’t worry; that’s very easy.

Let’s take a look at a script that reports back group membership and Class types for the Finance Managers group:

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers, ou=Finance, dc=fabrikam, dc=com")

For Each strUser on objGroup.Member
    Set objMember = GetObject("LDAP://" & strUser)
    Wscript.Echo objMember.CN & ", " & objMember.Class
Next

What did we tell you: just a few lines of code and we’re done. We begin by binding to the Finance Managers group in Active Directory. After making the connection we use a simple For Each loop to loop through all the values in the multi-valued attribute Member. Because Member just happens to contain the distinguished names of all the members of the group, looping through these values will…well, give us the distinguished names of all the members of the group.

And before you ask, yes, the attribute name probably should be Members, with an s on the end. But it’s not; go figure.

Now, if all we wanted was the DN for each group member we’d be done. However, we need to get the value of the Class attribute as well. Therefore, inside our For Each loop we bind to the Active Directory account of each group member. That’s what we do with this line of code:

Set objMember = GetObject("LDAP://" & strUser)

To bind to an Active Directory object you need to specify the object’s ADsPath. The ADsPath consists of the ADSI provider name (for Active Directory that will always be LDAP://) followed by the object’s distinguished name. We already have the DN; that’s stored in the variable strUser. Consequently, all we have to do is concatenate LDAP:// with the value of strUser, and pass that value to the GetObject method. In turn, GetObject will bind us to the Active Directory account.

Make sense? Good.

After we’re connected to the account we can echo back the values of any attributes belonging to that account. In our sample script we simply echo back the value of the CN and Class attributes (with a comma to separate them); that gives us output similar to this:

atl-ws-01, computer
Ken Myer, user
North American Finance Users, group
Pilar Ackerman, user

There you have it: output that distinguishes between classes such as users, computers, and groups. No one ever believes us when we say a scripting task is easy, but this time we have proof!


How Can I Temporarily Add a Group to Another Active Directory Group?

Hey, Scripting Guy! I’d like to add an Active Directory group to a second group, but only for an hour; after an hour, I’d like remove that group from the second group. Can I do that with a script?

-- JW

Hey, JW. Well, this is definitely one of the more interesting questions we’ve received. As far as we know, there’s no way to put a time limit on group membership; in other words, there’s no Active Directory property that says, “Yes, you can be a member of this group, but only for so long.” Consequently, we had to look for a workaround.

This is what we came up with. The following script will add a group (Accountants) to a second group (Finance Managers). The script will pause for one hour and then remove the Accountants group from the Finance Managers group. Here’s what the code looks like:

Set objGroup = GetObject("LDAP://cn=Finance
Managers, ou=Finance, dc=fabrikam, dc=com")
Set objTempGroup =  GetObject("LDAP://cn=Accountants, ou=Finance,
dc=fabrikam, dc=com")

objGroup.Add(objTempGroup.ADsPath)

Wscript.Sleep 3600000

objGroup.Remove(objTempGroup.ADsPath)

And here’s how the thing works. We begin by binding to the Finance Managers group in Active Directory and assigning that group to an object reference named objGroup. We then create a second object reference (objTempGroup) and bind to the Accountants group. After we’ve made these two connections we can then add the Accountants group to the Finance Managers group using this line of code:

objGroup.Add(objTempGroup.ADsPath)

Got all that? We just call the Add method and pass that method the ADsPath of the member being added (in this case, the Accountants group).

At this point the Accountants group is now a member of the Finance Managers group. Now all we have to do is wait an hour and then remove the group.

That, of course, is the tricky part. What we decided to do was simply pause the script for an hour; we can do that using this line of code, which calls the Wscript.Sleep method and instructs the script to wait 3,600,000 milliseconds before resuming:

Wscript.Sleep 3600000

If you’re wondering, “Why 3,600,000 milliseconds?” well, Wscript.Sleep accepts values in millisecond increments. One second equals 1,000 milliseconds. One minute thus equals 60,000 milliseconds (60 x 1,000), and one hour equals 3,600,000 milliseconds (60 x 60,000).

After the hour has passed, the script resumes with the next line of code, a line which removes the Accountants group from the Finance Managers group:

objGroup.Remove(objTempGroup.ADsPath)

This script works just fine; the only problem is that the script has to run – without interruption – for an hour. Should the script end prematurely (because someone terminates the process, or someone closes the command window the script is running in, or someone reboots the computer, or …) the Accountants group will never be removed from the Finance Managers group. Because of that, a better approach might be to use two scripts – one that adds the group and another that removes the group – and run them both as scheduled tasks. That way you have more assurance that everything will go off as planned.

In case you’re wondering, a script that’s paused like this uses no CPU time; it just sits there patiently and waits for the alarm to ring and tell it to get back to work. And you don’t have to worry about the script “forgetting” to wake up. Just for the heck of it, we ran a script that included a 15-hour pause. Fifteen hours later the script resumed as expected. In other words, this approach works just fine, provided, of course, that there’s no interference from outside factors (such as the computer rebooting).


How Can I Determine if an OU is Blocking Group Policy Inheritance?

Hey, Scripting Guy! How can I determine if an OU has the Group Policy property Block Policy Inheritance box checked?

-- PP

Hey, PP. You know, sometimes you wait all your life for a situation to arise, and then it never arises. For example, one of the Scripting Guys happens to know all nine verses to the song My Darlin’ Clementine. All his life he’s waited for someone to say, “All nine verses to My Darlin’ Clementine? I got a million dollars that says no one knows all nine verses to My Darlin’ Clementine.” For some strange reason, that’s never happened.

For the longest time it seemed like the same thing would happen with Group Policy inheritance. This same Scripting Guy – who seems to specialize in knowledge that few people care about – also happens to know how to tell whether or not Group Policy inheritance is enabled on an Active Directory OU. All his life he’s waited for someone to say, “Know how to tell whether Group Policy inheritance is enabled on an OU? I got a million dollars that says no one knows how to tell whether or not Group Policy inheritance is blocked on an OU.” And now, at last, the moment has come.

Of course, you didn’t offer a million dollars for the answer. But what the heck:

Set objOU = GetObject("LDAP://ou=Finance,dc=fabrikam,dc=com")

If objOU.gpOptions = 1 Then
    Wscript.Echo "Block policy inheritance is enabled."
Else
    Wscript.Echo "Block policy inheritance is not enabled."
End If

Believe it or not, that’s the entire script: this isn’t a hard thing to do, it’s just that very few people know how to do it.

The script begins by binding to the OU in Active Directory. In our sample script, that means binding to the Finance OU in fabrikam.com:

Set objOU = GetObject("LDAP://ou=Finance,dc=fabrikam,dc=com")

After making the connection all we need to do is check the value of the gpOptions attribute. If gpOptions is equal to 1 that means that Group Policy inheritance has been blocked on the OU. If gpOptions is equal to anything else (the other two possible values are 0 and Null) then Group Policy inheritance has not been blocked. We simply set up an If Then statement to examine the value of gpOptions and then echo the appropriate message. And that’s it.

Now, who wants to make an offer on all nine verses of My Darlin’ Clementine? That’s OK; take your time and think it over. We’ve waited this long, we can wait some more.

Time’s up: now who wants to make an offer on all nine verses of My Darlin’ Clementine?


How Can I Determine the OU the Local Computer Belongs To?

Hey, Scripting Guy! In a logon script, which attribute do I use to determine the OU that the local computer belongs to?

-- JP

Hey, JP. Most people are familiar with Cupid, the Roman god of love. In fact, you’d be hard-pressed to find anyone who doesn’t know that Cupid carries with him a quiver of gold-tipped arrows. Get struck by one of Cupid’s arrows, and you’ll instantly fall in love with whomever Cupid dictates.

Ah, but here’s something that most people don’t know: in addition to his gold-tipped arrows Cupid also carries a few lead-tipped arrows. What happens if you get struck by one of those arrows? You got it: you’ll instantly hate whomever Cupid dictates. Something to keep in mind the next time you see Cupid buzzing around your window.

So why are we wasting valuable Web space talking about Roman mythology? That’s easy: we Scripting Guys have taken a sacred vow to tell people things that they don’t already know. Most people don’t know that Cupid can cause you to hate others just as easily as he can cause you to love others; therefore, it’s our duty to tell you this. Likewise, it’s our duty to tell you that– for some reason – there is no Active Directory attribute that tells you which OU a computer belongs to. If you were hoping to use a line of code like this to determine a computer’s OU, well, sorry but you’re out of luck:

Wscript.Echo objComputer.OU

But wait; don’t go just yet. After all, the Scripting Guys have taken a lot of sacred vows; let’s see if we can find another one that might prove useful. Let’s see: “Always put the garbage out the night before the garbage gets picked up.” Good advice, but it probably doesn’t apply here.

How about this one: “Always make fun of the Oakland Raiders even when there’s absolutely no reason to mention the Oakland Raiders” Actually, we’d never do that; Oakland Raider fans are miserable enough as it is.

Oh, wait, here we go: “Whenever there’s no obvious way to do something try to find a way to do it anyway.”

Based on that sacred vow:

Set objSysInfo = CreateObject("ADSystemInfo")
strComputer = objSysInfo.ComputerName

Set objComputer = GetObject("LDAP://"
& strComputer)

arrOUs = Split(objComputer.Parent,
",")
arrMainOU = Split(arrOUs(0),
"=")

Wscript.Echo arrMainOU(1)

Yes, this is an odd-looking script; for one thing, who’d have ever guessed that you could use that many Split functions in such a short snippet of code? So let’s see if we can explain how this all works.

To begin with, we create an instance of the ADSystemInfo object, an object that can return all sorts of useful information about the local computer and the logged-on user (for a sample script that shows all the information that can be returned using ADSystemInfo click here). We then use this line of code to store the value of the ComputerName property in a variable named strComputer:

strComputer = objSysInfo.ComputerName

As it turns out, the ComputerName property actually returns the distinguished name of the computer; that means you’re going to get back a value similar to this:

CN=ATL-WS-01,OU=Accounting,OU=Finance,DC=fabrikam,DC=com

As you can see, the computer’s OU – Accounting – happens to be embedded within the distinguished name. All we have to do now is figure out how to extract the OU name.

To be honest, there are several different ways to go about extracting the OU name. After giving this a tiny bit of thought we decided to start the process by using this line of code to connect to the computer account in Active Directory:

Set objComputer = GetObject("LDAP://" & strComputer)

That, in turn, brings us to this portion of the script:

arrOUs = Split(objComputer.Parent, ",")

What the heck is going on here? What we’re doing is using the Split function to split the value of the computer’s Parent attribute into an array. What’s the Parent attribute? In this case, it’s going to be the ADsPath of the container in which the computer object resides. In other words, it’s equal to this:

LDAP://OU=Accounting,OU=Finance,DC=fabrikam,DC=com

When we split the value of the Parent attribute on the comma, we end up with an array (named arrOUs) that contains the following elements:

LDAP://OU=Accounting
OU=Finance
DC=fabrikam
DC=com

As you can see, we’re getting closer; in fact, the first element in our array (element 0) is exactly what we need, assuming we can get rid of the LDAP://OU=, that is. So then why don’t we get rid of the LDAP://OU=? The following line of code takes element 0 in the array arrOUs – LDAP://OU=Accounting – and splits that value on the equals sign (=):

arrMainOU = Split(arrOUs(0), "=")

The net result? Another array (arrMainOU) containing these two elements:

LDAP://OU
Accounting

All we have to do now is echo back the second item in the array (element 1) and we’re done:

Wscript.Echo arrMainOU(1)

Like we said, it’s a roundabout way of getting the computer OU but, for better or worse, there’s nothing but a roundabout way of getting the computer OU. But hey, as long as it works, right?

Now, before we go, is there anything else that the Scripting Guys know that you need to know as well? Maybe, say, the story of Cupid and Psyche, complete with deranged man-killing sheep, talking towers, and a box full of eternal sleep?

Oh. Well, OK; guess we’ll save that story for a future column. Hey, what are you guys doing tomorrow?


How Can I Enumerate All the Objects in an Active Directory OU?

Hey, Scripting Guy! How can I enumerate all the objects in an Active Directory OU?

-- RB

Hey, RB. By the way, thanks: it’s always nice to get an easy question every once in awhile! Enumerating all the objects in an OU is almost embarrassingly-simple: by default, any time you bind to an OU using ADSI you automatically get back a collection of all the objects in that OU. To enumerate those objects, all you have to do is create a For Each loop that walks through the collection.

Don’t believe us? Well, here’s a script that binds to the Servers OU in a domain named fabrikam.com. After making the connection, the script enters a For Each loop that displays the CN (common name) of every item in that collection, and thus every object in the OU:

Set colItems = GetObject _
    ("LDAP://ou=Servers, dc=fabrikam, dc=com")

For Each objItem in colItems
    Wscript.Echo objItem.CN
Next

It really is that simple. To adapt this script for use in your domain, just change the binding string accordingly. For example, suppose you want to connect to the Finance OU in contoso.com. Your script would look like this:

Set colItems = GetObject _
    ("LDAP://ou=Finance, dc=contoso, dc=com")

For Each objItem in colItems
    Wscript.Echo objItem.CN
Next

Two things to keep in mind here. First, remember that neither Users nor Computers (the default locations for user and computer accounts) are actually OUs; technically, these two entities are known as “containers.” That means you can’t bind to either of these containers using a binding string like thus:

ou=Users, dc=fabrikam, dc=com

That’s not going to work. Instead, you’ll have to reference the CN, like so:

cn=Users, dc=fabrikam, dc=com

Second, there will often be times when you want to enumerate only a subset of items found in an OU; for example, you might want to get back a list of just the user accounts or just the computer accounts. To do that, bind to the OU, then add a filter. For example, this script returns only a list of the computer objects found in the Servers OU. How do we know that it returns only computer objects? Note the Filter, which specifies just one item: Computer.

Set colItems = GetObject _
    ("LDAP://ou=Servers, dc=fabrikam, dc=com")
colItems.Filter = Array("Computer")

For Each objItem in colItems
    Wscript.Echo objItem.CN
Next

Notice, too, that items included in the Filter have to be passed as an array, even if (as is the case here) you’re only filtering on one thing. Because items are passed as an array, this means you can filter on multiple items. Need a script that returns both user and computer accounts? All you had to do was ask:

Set colItems = GetObject _
    ("LDAP://ou=Servers, dc=fabrikam, dc=com")
colItems.Filter = Array(“User”, "Computer")

For Each objItem in colItems
    Wscript.Echo objItem.CN
Next


How Can I List All the Sub-OUs in an OU?

Hey, Scripting Guy! How can I list all the sub-OUs in an OU? That includes any OUs that might be in those sub-OUs.

-- JE

Hey, JE. If you’re the least bit familiar with this column then you know that we Scripting Guys love to answer questions that involve searching Active Directory. That’s because we never bother to write detailed explanations of how the scripts we show you work; instead, we just refer you to our two-part Tales from the Script series Dude, Where’s My Printer? If you’ve been playing along at home and if you picked January 13th as the first day in the year 2006 that the Scripting Guys would tackle a question involving Active Directory search, well, congratulations: you’re a winner.

Note. So is it really true that explaining how an Active Directory search script works would exceed the scope of this column? Man, who told you that? Oh, right: we did. Um, yes, yes, explaining how an Active Directory search script works does exceed the scope of this column; it has nothing to do with the Scripting Guys being lazy. No, nothing at all …. (Editor’s Note: Editing for accuracy: It has everything to do with the Scripting Guys being lazy.)

Now, to be fair, we don’t have to use an Active Directory search to retrieve a list of all the sub-OUs (and sub-sub-OUs, etc.) within a specific OU; we could achieve the same results using a recursive function. In general, though, recursive functions are not only difficult to write but also difficult to visualize and understand. We’re simple guys, and we find Active Directory search scripts much simpler to deal with. Consequently, that’s the route we took:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://ou=finance,dc=fabrikam,dc=com' WHERE " & _
        "objectCategory='organizationalUnit'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("ADsPath").Value
    objRecordSet.MoveNext
Loop

This might look a bit intimidating to newcomers, but most of this script is boilerplate; it’s the same code you’ll find in almost every Active Directory search script. Because of that we’re going to focus on just one small piece of the puzzle, the SQL statement we use to query Active Directory. If you have no idea what an ADODB.Connection object is used for then we recommend you take a look at our two-part Tales from the Script series Dude, Where’s My Printer?

Oh, right: we did mention that already, didn’t we?

Here’s the code of interest:

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://ou=finance,dc=fabrikam,dc=com' WHERE " & _
        "objectCategory='organizationalUnit'"

What we’re doing here is selecting the ADsPath attribute for all organizational units found in the Finance OU in fabrikam.com. How do we know that we’re getting back the organizational units found in the Finance OU in fabrikam.com (and only those organizational units)? Two reasons: First, we know we’re going to get back only OUs because our Where clause limits us to items that have an objectCategory equal to organizationalUnit. That’s going to filter out user accounts, groups, print queues, and anything else that might be found in Active Directory.

That makes sense. But how do we know we’ll get back only those OUs that can be found in the Finance OU? Because, in this script, we don’t start our search in the Active Directory root; instead we start it in the Finance OU itself: LDAP://ou=finance, dc=fabrikam, dc=com. That means we’re going to search only the Finance OU and any other containers found in that OU; we aren’t even going to take a peek anywhere else. In turn, that means anything we get back from this script has to be within the Finance OU. Suppose we wanted to find all the OUs located in the West Coast OU, which happens to be in the USA OU? Then we’d target our query like so:

objCommand.CommandText = _
    "SELECT AdsPath FROM 'LDAP://ou=West Coast,ou=USA,dc=fabrikam,dc=com' WHERE " & _
        "objectCategory='organizationalUnit'"

Pretty straightforward.

So when will the Scripting Guys tackle another Active Directory search question? Well, we thought about starting an office pool and taking bets on the next date, but then someone pointed out that we could cheat and just wait until the date we picked before answering another Active Directory search question. The Scripting Guys cheat? Say it isn’t so! ((Although, knowing the Scripting Guys, we’d probably lose the office pool even if we did cheat. Which helps explain why we all live in Redmond, WA rather than, say, Monte Carlo.)


How Can I Retrieve a List of OUs that have a Specific Word in Their Name?

Hey, Scripting Guy! We have a number of OUs in our domain that have the word Servers in the OU name. How can I retrieve a list of those OUs?

-- AM

Hey, AM. One of the Scripting Guys has a 15-year-old son who, whenever he hears his father tell a story about the good old days, rolls his eyes and says, “You’re just one of those old guys still living in the past.” (Of course, in the same breath this Scripting Son will say, “Remember when I was 10 and hit that home run against the Rockies?” but somehow that’s different.)

We mention this because your question takes us back to the good days, back to the glory years of the Hey, Scripting Guy! column. Back then it seemed like every other question we took had the same answer: you can find what you’re looking for by searching Active Directory. Did we do that because searching Active Directory is an incredibly powerful tool for system administrators, or did we do that because we were lazy and just liked giving the same answer over and over again? Well, we are lazy, but the truth is, ADO searches of Active Directory are enormously useful to system administrators. If you need a list of user accounts or a list of groups or a list of computers or a list of phone numbers or a list of just about anything, there’s a good chance you’ll find that information stored in Active Directory.

So, roll your eyes and sigh if you must, but here’s the answer to your question: you can find what you’re looking for by searching Active Directory. In fact, you can retrieve a list of OUs that have the word “Servers” in their name by using a script similar to this:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' " & _
        "WHERE objectCategory='organizationalUnit' AND Name = '*Servers*'"

Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("ADsPath").Value
    objRecordSet.MoveNext
Loop

We won’t go over this script in detail today; if you don’t have much experience with searching Active Directory you might take a look at our two-part series Dude, Where’s My Printer?, a series that explains the ADODB.Connection object and the Searchscope and all the other crazy-looking code found in this script. Instead we’ll just focus on the SQL query that actually returns the list of OUs for us:

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' " & _
        "WHERE objectCategory='organizationalUnit' AND Name = '*Servers*'"

What we’re doing here is searching the fabrikam.com domain and retrieving the ADsPath attribute. (Obviously we could have picked any other attribute, or even multiple attributes. We chose ADsPath because once you know the ADsPath for an object you can easily bind to that object and retrieve values for all its attributes.) That part’s pretty straightforward; what we’re really interested in is the WHERE clause:

WHERE objectCategory='organizationalUnit' AND Name = '*Servers*'

We use the WHERE clause to specify the criteria for our search, and in this case we’re looking for two things:

· An objectCategory equal to organizationalUnit. As you might have guessed, an object with an objectCategory equal to organizationalUnit will be an OU. This part of our query weeds out users, computers, groups, sites, subnets, and any other kind of object stored in Active Directory.

· OUs that have the word Servers somewhere in their Name attribute. Note the use of the wildcard character (the asterisk). This syntax *Servers* basically says, “We’re looking for the word Servers anywhere in the Name attribute. We don’t care what, if anything, comes before the word Servers (the first asterisk), and we don’t care what, if anything, comes after the word Servers (the second asterisk). We want any OU that has the word Servers somewhere in its Name.”

We could, of course, modify this syntax. What if we wanted to search only for Names that started with Servers (Servers-1, Servers-2, etc.)? Then we could search for this: Servers*. As you can see, in this case our Name has to start with the word Servers; we don’t care what comes after that. If we wanted to search for Names that ended with the word Servers (Asian Servers, North America Servers, etc.) we’d use this syntax: *Servers. It’s really no different than using wildcard characters to search for files or folders, something you’ve probably done a million times.

After defining the query we call the Execute method; this actually runs the query and returns a recordset containing all the OUs that have the word Servers somewhere in the Name. We then loop through the recordset and echo the ADsPath for each of these OUs. That’s all there is to it.

Incidentally, it’s usually not recommended that you search for text in the middle of a string (*Servers*); that’s because a search like that can be resource-intensive and take awhile for the domain controller to process. We can get away with it here because we’re assuming you don’t have millions of OUs to check; typically domains have far fewer OUs than they do, say, users or groups. Therefore we don’t expect this search to cause any problems.

Does that answer your question? Good. And, although you didn’t ask, we’re sure you’d like to hear about the time we almost-single-handedly beat Hanford High School, at that time undefeated and the number 1 ranked baseball team in the state of – oh, OK. But if the Scripting Son wants to talk about his home run from this season, you better not listen to him, either. (Although that was a nice hit. It reminded Scripting Dad of that one day in Richland when he….)


How Can I Tell If an OU Has Any User Accounts In It?

Hey, Scripting Guy! How can I tell if an OU has any user accounts in it?

-- RL

Hey, RL. As a matter of fact, there is a way to determine whether or not an OU contains any user accounts: all you have to do is search just that OU for nothing but user accounts. When you perform a search, any “hits” (in this case, any user accounts that are found) come back as part of a recordset. When you get the recordset back, all you have to do is check the value of the RecordCount property. If the value is 0, then there aren’t any user accounts in the OU; if the value is anything other than 0, then that’s how many user accounts were found. In other words, if the RecordCount is 17, then there are 17 user accounts in that OU. Child’s play.

Here’s what the script looks like:

On Error Resume Next

Const ADS_SCOPE_ONELEVEL = 1

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_ONELEVEL

objCommand.CommandText = _
"SELECT Name FROM
'LDAP://OU=finance,dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

Wscript.Echo "Number of user accounts: " &
objRecordSet.RecordCount

We can’t provide a detailed explanation of how an Active Directory search script works in this column; if you’re interested in knowing more about searching Active Directory, you might want to look at a Webcast we did on this topic a few months ago. However, there are three things that we will point out.

First, note that we begin by defining a constant named ADS_SCOPE_ONELEVEL and setting it to 1. This constant tells the script to search only the target container; if there are any OUs nested within this OU, they won’t get searched. If you want to search child OUs as well, then use the constant ADS_SCOPE_SUBTREE and set the value to 2.

Second, make sure that you start the search in the target OU. Notice that our CommandText says SELECT Name FROM 'LDAP://OU=finance,dc=fabrikam,dc=com'. We don’t want to start in the domain root (dc=fabrikam,dc=com), we want to start in the Finance OU.

Finally, we’re searching only for user accounts; hence the clause WHERE objectCategory='user'. What if you wanted to know if there were any computer accounts in the OU? Then just change the WHERE clause to this:

WHERE objectCategory='computer'

What if you just wanted to know if anything was in the OU? (For example, perhaps you want to delete the OU, but only if it’s empty.) In that case, you wouldn’t use a WHERE clause at all, but would instead use this CommandText:

SELECT Name FROM 'LDAP://OU=finance,dc=fabrikam,dc=com'

Active Directory searching is extremely powerful and extremely useful, and we’d like to see people make more and better use of it. If that’s the case, then shouldn’t we Scripting Guys be doing something to make it easier for people to get started with Active Directory searching? Funny you should mention that; we just might have something for you in a week or two. Stay tuned!


How Can I List All the Sites in Active Directory as well as All the Servers in Those Sites?

Hey, Scripting Guy! How can I list all the sites in Active Directory as well as all the servers in those sites?

-- DW

Hey, DW. You know, you’re putting us to the test here. Ask us a question about users and groups and we’re fine; we at least kind of understand what users and groups are all about. Ask about computer accounts or OUs; hey, no problem. When it comes to things like sites and site links and stuff like that, however, well, then we start to worry; after all, we’re never really convinced that we even know what those things are, let alone how you can script them.

But would the Scripting Guys ever let a total lack of knowledge and understanding stop them? Of course not. Here’s a script that lists all of the sites in Active Directory, along with the servers found in each of those sites:

On Error Resume Next

Set objRootDSE = GetObject("LDAP://RootDSE")
strConfigurationNC = objRootDSE.Get("configurationNamingContext")

strSitesContainer = "LDAP://cn=Sites," & strConfigurationNC
Set objSitesContainer = GetObject(strSitesContainer)
objSitesContainer.Filter = Array("site")

For Each objSite In objSitesContainer
    Wscript.Echo objSite.CN
    strSiteName = objSite.Name
    strServerPath = "LDAP://cn=Servers," & strSiteName & ",cn=Sites," & _
        strConfigurationNC
    Set colServers = GetObject(strServerPath)

    For Each objServer In colServers
        WScript.Echo vbTab & objServer.CN
    Next
    Wscript.Echo
Next

So what are we doing here? (Actually, that wasn’t a rhetorical question, we were hoping someone would explain it all to us!) We begin by using these two lines of code to bind to the Active Directory root and then to the Configuration container (configuringNamingContext):

Set objRootDSE = GetObject("LDAP://RootDSE")
strConfigurationNC = objRootDSE.Get("configurationNamingContext")

Why do we start out like this? Well, the Configuration container holds information about the physical structure and layout of Active Directory; that’s where we’ll find information about sites and about the servers assigned to each site. By binding to the Active Directory root (rootDSE) and then using the Get method to retrieve the configurationNamingContext we can get the Active Directory path to the Configuration container regardless of the name of our domain.

Note. What does that mean? For one thing, it means you can use this script as-is, without having to change fabrikam.com to the name of your domain; as you can see, we don’t even reference fabrikam.com (or any other domain name) anywhere in the script.

Our next step is to retrieve a collection of all the Active Directory sites. That’s what we do here:

strSitesContainer = "LDAP://cn=Sites," & strConfigurationNC
Set objSitesContainer = GetObject(strSitesContainer)
objSitesContainer.Filter = Array("site")

In the first line we construct the ADsPath to the Sites container, combining LDAP://cn=Sites and the configurationNamingContext. We use the GetObject method to bind to the Container, then apply a Filter that limits items in the collection to Site objects. The net result: objSitesContainer will now contain a collection of all our Active Directory sites.

OK, so maybe this isn’t as difficult as we thought it would be. Next we set up a For Each loop to walk through the collection of sites. Inside that loop we echo the CN for the first site in the collection, then store the site Name (which will be something like CN=Default-First-Site-Name) in the variable strSiteName:

Wscript.Echo objSite.CN
strSiteName = objSite.Name

As it turns out, each Active Directory site has a Servers container, and each Servers container holds a list of the servers assigned to that site. In order to bind to that Servers container for our first site we use this line of code to construct an ADsPath, a path we store in the variable strServerPath:

strServerPath = "LDAP://cn=Servers," & strSiteName & ",cn=Sites," & _
    strConfigurationNC

Once we have the ADsPath we can then bind to the Servers container for the first site using this line of code:

Set colServers = GetObject(strServerPath)

We’re on a roll now. Next we set up a second For Each loop that walks through the collection of items found in the Servers container. For each server in the collection we do nothing more than echo back the server CN:

For Each objServer In colServers
    WScript.Echo vbTab & objServer.CN
Next

Oh, OK: we also put a tab character (vbTab) in front of the server name. You’ll see why we do that when you take a look at the sample output.

And that’s it: we’ve now echoed back the name of the first site as well as all the servers assigned to that site. We then loop around and repeat the process with the second site in the collection. This continues until we’ve worked with all the sites and echoed back the names of all the servers in those sites. When we’re all done we should have output similar to this:

Default-First-Site-Name
    atl-dc-01
    atl-dc-02

European-Site
    lon-dc-01
    lon-dc-02
    par-dc-01
    bru-dc-01
    bru-dc-02

Not bad, huh? And just imagine what a cool script we could have written had we actually known what we were doing!


Can I Get a List of All My User Accounts?

Hey, Scripting Guy! Is there any way to get a list of all the user accounts in Active Directory?

-- CB, Fremont, CA

Hey, CB. By far the quickest and easiest way to do this is to search Active Directory for all your user accounts. Here’s a sample script that searches the fabrikam.com domain; to search your own domain, just edit the LDAP connection string (‘LDAP://dc=fabrikam,dc=com’) as needed. For example, if your domain is named contoso.com, you’d change the connection string to ‘LDAP://dc=contoso,dc=com’.

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

There’s too much going on in this script to explain it all here; if you’re interested in how and why this works, and what else you can search for in Active Directory, you might want to take a look at this Tales from the Script column.


How Can I Add an Email Address to the proxyAddresses Attribute?

Hey, Scripting Guy! How can I add an email address to the proxyAddresses attribute without overwriting all the existing attributes?

-- DH

Hey, DH. For those of you who aren’t too familiar with the proxyAddresses attribute (and, alas, that includes the Scripting Guys) proxyAddresses is an attribute added to Active Directory by Microsoft Exchange; it’s simply an alias by which an Exchange user can be recognized by a non-Exchange mail system. If you echo back the proxy addresses for a given user you’re likely to see something similar to this:

NUMERIC:4257050743
sip:kenmyer@fabrikam.com
X500:/o=fabrikam/ou=APPS-WGA/cn=Xenix_Users/cn=kenmyer
X500:/o=fabrikam/ou=First Administrative Group/cn=Recipients/cn=kenmyer
x400:c=US;a=MCI;p=fab;o=northamerica;s=Myer;g=Ken;
X500:/O=fabrikam/OU=northamerica/cn=Recipients/cn=kenmyer
X400:c=US;a=finance;p=fabrikam;o=fabrikam;s=Myer;g=Ken;
x500:/o=fabrikam/ou=northamerica/cn=Recipients/cn=556899
smtp:kenmyer@fabrikam.com
SMTP:kenmyer@finance.fabrikam.com

If you’re thinking, “Wow, that’s s lot of stuff for one little attribute to have,” well, that’s because proxyAddresses happens to be a multi-valued attribute, an attribute that can contain more than one value. And that’s why this particular question is so tricky. Suppose we wanted to add a new proxy address – kenmyer@northamerica.fabrikam.com – to the list of proxyAddresses. We don’t want to delete the existing addresses; we just want to add an additional one.

Hey, no problem; after all, everyone knows how to modify an Active Directory attribute, right? And so you try a script similar to this:

Set objUser = GetObject _
    ("LDAP://atl-dc-1/CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com")
objUser.proxyAddresses = kenmyer@northamerica.fabrikam.com
objUser.SetInfo

And guess what you get back the next time you echo Ken Myer’s proxy addresses:

kenmyer@northamerica.fabrikam.com

Uh-oh: we deleted all the old proxy addresses and replaced them with the new address. Not what we wanted to do at all.

So how can we simply tack an additional value onto the end of a multi-valued attribute? Why, we just need to use the PutEx method:

Const ADS_PROPERTY_APPEND = 3

Set objUser = GetObject _
    ("LDAP://cn=KenMyer,ou=Finance,dc=fabrikam,dc=com")

objUser.PutEx ADS_PROPERTY_APPEND, "proxyAddresses", _
    Array("kenmyer@northamerica.fabrikam.com ")

PutEx is an ADSI method designed for working with multi-valued attributes; in fact, PutEx offers four different options for working with multi-valued attributes:

Constant | Value | Description
ADS_PROPERTY_CLEAR | 1 | Clears the value (or values) from the specified attribute.
ADS_PROPERTY_UPDATE | 2 | Replaces the value in the specified attribute with new values.
ADS_PROPERTY_APPEND | 3 | Appends a new value (or values) to the specified attribute.
ADS_PROPERTY_DELETE | 4 | Deletes the value (or values) from the specified attribute.

We won’t spend a lot of time detailing how PutEx works; for more information you might check out the Modifying Multivalued Attributes section in the Microsoft Windows 2000 Scripting Guide.

Now, back to our script. We want to append a new value to proxyAddresses; consequently in the first line of the script we define a constant named ADS_PROPERTY_APPEND and set the value to 3:

Const ADS_PROPERTY_APPEND = 3

We bind to the Ken Myer user account and then call the PutEx method, passing three parameters:

· The constant ADS_PROPERTY_APPEND, which tells the script to append the new value to any existing values.

· The name of the attribute we want to work with (proxyAddresses).

· The value we want to append (in other words, the new proxy address).

Note that even though we’re adding only a single value we need to pass that value as an array; hence our actual code looks like this:

objUser.PutEx ADS_PROPERTY_APPEND, "proxyAddresses", _
    Array("kenmyer@northamerica.fabrikam.com")

And what do we get when we echo the proxy addresses for Ken Myer:

kenmyer@northamerica.fabrikam.com
NUMERIC:4257050743
sip:kenmyer@fabrikam.com
X500:/o=fabrikam/ou=APPS-WGA/cn=Xenix_Users/cn=kenmyer
X500:/o=fabrikam/ou=First Administrative Group/cn=Recipients/cn=kenmyer
x400:c=US;a=MCI;p=fab;o=northamerica;s=Myer;g=Ken;
X500:/O=fabrikam/OU=northamerica/cn=Recipients/cn=kenmyer
X400:c=US;a=finance;p=fabrikam;o=fabrikam;s=Myer;g=Ken;
x500:/o=fabrikam/ou=northamerica/cn=Recipients/cn=556899
smtp:kenmyer@fabrikam.com
SMTP:kenmyer@finance.fabrikam.com

Cool, huh?

Incidentally, there’s no guarantee that the new proxy address will show up first in the list of addresses; when it comes to multi-valued attributes the order of the individual attributes is not guaranteed. But that doesn’t matter just as long as kenmyer@northamerica.fabrikam.com shows up somewhere in the list.


How Can I Assign a New Home Drive to a User Who Doesn’t Have One?

Hey, Scripting Guy! How can I check to see if a user has a home drive and home directory set and, if they don’t, assign them a new one?

-- AM

Hey, AM. No, no excuses: the Seahawks had plenty of opportunities to win the Super Bowl yesterday, but they failed to take advantage of it. At times it seemed like they were playing not to lose instead of playing to win, and that’s always a recipe for disaster. Oh, well; that’s the way the world goes round.

Of course, considering how far they had to travel to get there you’d think that the officials would have been interested in watching the game themselves. But apparently not. Matt Hasselbeck gets called for blocking below the waist while making a tackle? OK ….

No, that’s not an excuse: it’s just an … observation ….

But, hey, what’s done is done, and, besides, this is supposed to be a scripting column, not a sports column. With that in mind, how can we test for the existence of a home directory and, if necessary, assign a new home directory and home drive for the user? Well, this should do the trick:

On Error Resume Next

Set objUser = GetObject("LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam,
dc=com")

If IsEmpty(objUser.homeDirectory) or IsNull(objUser.homeDirectory) Then
strUser = objUser.sAMAccountName
strHomeDirectory = "\\atl-dc-01\users\" &
strUser
objUser.homeDirectory = strHomeDirectory
objUser.homeDrive = "Z:"
objUser.SetInfo
End If

In this script we begin by binding to the Ken Myer user account in Active Directory. That brings us to this line of code:

If IsEmpty(objUser.homeDirectory) or IsNull(objUser.homeDirectory) Then

As you probably know, Active Directory scripting can be a tad bit tricky from time-to-time. Suppose you look at a user account in Active Directory Users and Computers and see that the home directory box is blank. In that case, your first thought is likely to be, “Oh, I guess this user doesn’t have a home directory.” And you would be right.

If you run a script against this same account, however, you have to concern yourself with two things: is the home directory box blank because the attribute is null (that is, no value has been assigned to it), or is the home directory box blank because the attribute is empty (perhaps because another administrator ran a script that set the value to an empty string)?

Yes, we know: an empty attribute and a Null attribute would seem to be the same thing. For better or worse, though, they aren’t. Technically speaking, an empty attribute actually has a value: it’s just that the value is nothing. With a Null attribute, however, we’re saying that we have no idea whether or not this property has a value. It’s confusing, but it’s an important distinction. If the homeDirectory attribute is empty that means that the user doesn’t have a home directory assigned. However, in that case the following test will return False and lead us to believe that the user does have a home directory:

IsNull(objUser.homeDirectory)

Why does this return False? Well, because the attribute isn’t Null, it’s empty. And, like it or not, there’s a difference

That’s all right: let us know when your head stops spinning and then we’ll continue.

Better? To be honest, this isn’t worth worrying about, at least not right now. Instead, just play it safe and do what we did: check to see if the attribute value is empty or if it’s Null. You can do that by using both the IsEmpty and IsNull functions in your If-Then statement:

If IsEmpty(objUser.homeDirectory) or IsNull(objUser.homeDirectory) Then

This way you’re covered no matter what.

So suppose the user doesn’t have a home directory; what then? Well, then we execute this block of code:

strUser = objUser.sAMAccountName
strHomeDirectory = "\\atl-dc-01\users\" & strUser
objUser.homeDirectory = strHomeDirectory
objUser.homeDrive = "Z:"
objUser.SetInfo

Home directories are typically named after the user. (They don’t have to be, but that’s the standard adopted in many organizations.) Because we’re too lazy to come up with our standard, we’re going to use this same approach; that means we’re going to assign the user the home directory \\atl-dc-01\users\logonname, where logonname represents the user’s logon name (sAMAccountName). Granted, we could hard-code this name into the script. However, we decided to get a tiny bit fancy and have the script create the home directory name for us.

To do that we first assign the value of the user’s sAMAccountName (for example, kenmyer) to a variable named strUser. We then use this line of code to construct the UNC path to the directory:

strHomeDirectory = "\\atl-dc-01\users\" & strUser

If the user’s sAMAccountName really is kenmyer then the resulting path (and the value assigned to the variable strHomeDirectory) will be this:

\\atl-dc-01\users\kenmyer

Once we have the complete path we can set the value of the homeDirectory attribute; after that we‘re free to set the value of the homeDrive attribute to Z:. (And, yes, you must include the colon following the drive letter.) And when all that’s done we can call SetInfo to write the changes back to the user account. Whatever you do, don’t leave out the call to SetInfo: if you do your changes will not be applied to Active Directory. Which in turn means the user still won’t have a home drive and directory.

So are we really putting away the sports references forever? Of course not: after all, the end of the Super Bowl simply means that baseball season is just around the corner. And this year we think that our very own Seattle Mariners are going to –

On second thought, we’ll get back to you about those sports references ….


How Can I Assign a New UPN to All My Users?

Hey, Scripting Guy! How can I assign a new UPN to all my users?

-- CH

Hey, CH. The UPN (or User Principal Name) provides an alternate way of logging on to a domain. Typically you log onto a domain by pressing Ctrl-Alt-Delete, typing in your user name, domain name, and password, and then pressing ENTER. With a UPN, you don’t enter separate user and names, instead you enter a user name similar to this:

kenmyer@fabrikam.com

We won’t bother discussing the concepts behind the UPN other than note that, among other things, it lets you log on to a computer even if your domain name doesn’t appear in the Log on to dropdown list.

So how can you change the UPN for all the users in a domain? Well, it’s a two-part process: first you need to get a list of all the users in the domain, and then you need to change the UPN for each one. Let’s start with the first step, just for the heck of it.

By far the best way to get a list of all the users in a domain is to do an Active Directory search. We’ve discussed the process behind searching Active Directory in the past, so we’ll just give you a sample script for now; if you’d like more information on searching Active Directory, you might want to take a look at this Tales From the Script column. For now, here’s a script that returns the ADsPath for all the users in the fabrikam.com domain:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("ADsPath").Value
    objRecordSet.MoveNext
Loop

The ADsPath, of course, provides the path to the user account in Active Directory; for example, the ADsPath for user Ken Myer might look like this:

LDAP://CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com

We’re retrieving the ADsPath because we need to bind to each individual user account in order to change the UPN for that account; AdsPath provides a direct route to the account, and – as well sees – enables us to bind to an account using a single line of code, and without any fancy string manipulation of any kind.

So what happens after we bind to a user account? Well, we need to do two things at that point: we need to assign the user a new UPN, and then we need to call the SetInfo method, which actually writes the new UPN to the Active Directory user account. In pseudo-code, the process looks like this:

objUser.userPrincipalName = New UPN we’re assigning the user
objUser.SetInfo

In real code, the process of retrieving all the user accounts in Active Directory, binding to each one individually, and then assigning each account a new UPN looks like this:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT AdsPath,samAccountName,userPrincipalName FROM " & _
        "'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    strUser = objRecordSet.Fields("ADsPath").Value
    strNewUPN = objRecordSet.Fields("samAccountName").Value & "@" & "contoso.com"
    Set objUser =  GetObject(strUser)
    objUser.userPrincipalName = strNewUPN
    objUser.SetInfo
    objRecordSet.MoveNext
Loop

After we retrieve the collection of user accounts, all the excitement takes place inside the Do Until loop. Inside that loop, we begin by assigning the ADsPath for user 1 to the variable strUser. Next, we construct a new UPN for the user. Typically, UPNs are composed of the user’s logon name (samAccountName) followed by the domain name. Because CH’s question was about changing existing UPNs, we’ll do something a little different here. We’ll pretend that your company has merged with another organization, and now you want users to use the new name (contoso.com) in their UPN. For example:

kenmyer@contoso.com

Therefore, we’re going to construct a new UPN that consists of the samAccountName, the @ sign, and contoso.com, and then stash that new UPN in a variable named strNewUPN. That’s what this line of code does:

strNewUPN = objRecordSet.Fields("samAccountName").Value & "@" & "contoso.com"

So far so good. Next, we bind to the individual user account and assign the new UPN to the user. All of that gets accomplished with just two lines of code:

Set objUser =  GetObject(strUser)
objUser.userPrincipalName = strNewUPN

We call SetInfo, and just like that our first user has a new UPN. We then loop around and assign a new UPN to the next user in the collection. This process continues automatically until all of our users have a brand-new UPN.

We don’t know how often you’ll need to change the UPN for all the users in a domain, but the basic idea behind this script can be used for lots of other purposes as well. For example, you might want to change the company name for all your users, or you might want to require all your users to change their password the next time they log on. The script we’ve shown you today can easily be modified to carry out any kind of task that involves modifying all the user accounts in a domain.


How Do I Bind to a User Account in a Sub-OU?

Hey, Scripting Guy! How do I bind to a user account when the user is in a sub-OU? Code like this doesn’t work: LDAP://CN=Ken Meyer, OU=NA\Human Resources, DC=fabrikam, dc=com.

-- RD

Hey, RD. The past few days we’ve done some lengthy answers to questions, so we decided to be a little lazy today (hey, it is Friday!) and take an easy one. In your Active Directory, you have an OU named NA, and inside that OU you have a sub-OU named Human Resources. You’re trying to get at a user account located in the Human Resources OU but, as you’ve discovered, this code won’t do it:

Set objUser = GetObject _
  ("LDAP://CN=Ken Meyer, OU=NA\Human Resources, DC=fabrikam, dc=com ")

Why not? Well, you’re trying to take a shortcut here and, in this case at least, ADSI doesn’t like you taking shortcuts. You’re specifying an OU named NA\Human Resources, obviously meaning, “The OU named Human Resources that’s found inside the NA OU.” That makes sense to us, but not to ADSI. Instead, ADSI requires you to separate each part of the distinguished name; no shortcuts. Thus you need to use a binding string like this:

Set objUser = GetObject _
  ("LDAP://CN=Ken Meyer, OU=Human Resources, OU=NA, DC=fabrikam, dc=com ")

Notice that ADSI requires you to work backwards when creating a binding string. You start with the actual user account itself (CN=Ken Meyer). Next you go to the OU in which the user account resides: OU= Human Resources. From there you go to the parent OU: OU=NA. What if the NA OU was actually a sub-OU of, say, Headquarters? No problem; in that case, your binding string would look like this:

Set objUser = GetObject _
  ("LDAP://CN=Ken Meyer, OU=Human Resources, OU=NA, " & _
        "OU=Headquarters, DC=fabrikam, dc=com ")

You then tack on the domain components (DC=fabrikam, DC=com), and you’re off and running.


How Can I Bind to a User Account Using Something Other Than the CN Attribute?

Hey, Scripting Guy! Can I bind to an Active Directory account using the user’s mailNickname or their sAMAccoutName, something other than the CN?

-- MB

Hey, MB. When you bind to an account in Active Directory, you need to pass GetObject the account’s ADsPath. That means you need to use a line of code similar to this:

Set objUser = GetObject(“LDAP://cn=ken myer, ou=finance, dc=fabrikam, dc=com”)

The ADsPath is actually composed of two pieces: the ADSI provider used to connect to Active Directory (LDAP://) and the distinguishedName attribute of the object you’re trying to bind to (cn=ken myer, ou=finance, dc=fabrikam, dc=com). As you can see, the common name (CN) is the first element in the distinguish name. And – for better or worse – the common name has to be the first element in the distinguished name; if it’s not, then it’s not a distinguished name.

For example, suppose you try binding to an account using the mailNickname instead:

Set objUser = GetObject(“LDAP://mailNickname=kenmyer, ou=finance, dc=fabrikam, dc=com”)

That binding string is doomed to fail: to bind directly to an object in Active Directory you need to know the object’s CN. (Actually there are some oddball exceptions; for example, you can bind to an object if you know the object’s GUID. But it’s probably safe to assume that you haven’t memorized all the GUIDs for all your users.)

But wait: don’t leave just yet. As you probably know, we Scripting Guys love to build up dramatic tension: for reasons known only to ourselves, we delight in telling people that something can’t be done, then turning right around and telling them how to do it anyway. And, to paraphrase Ronald Reagan, here we go again.

Let’s think about the situation for a moment. Why would you want to bind to an object using the mailNickname or the sAMAccountName? We’re guessing there’s one reason for that: you don’t know the user’s CN, but you do know their mailNickname or sAMAccountName. Well, why didn’t you say so? If all we have is the mailNickname we can’t directly bind to an Active Directory object. However, we can search Active Directory, find the user with that mailNickname, retrieve the user’s distinguished name (which, of course, includes the CN), and then bind to the account. The net effect is the same: we supply nothing but the mailNickname and yet we end up binding to the user account. Sweet.

Here’s a script that searches Active Directory for the user with a mailNickname of kenmyer. After finding the account the script retrieves the distinguishedName attribute for the user and then binds to the account:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND mailNickname = 'kenmyer'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    strDN = objRecordSet.Fields("distinguishedName").Value
    objRecordSet.MoveNext
Loop

Set objUser = GetObject("LDAP://" & strDN)
Wscript.Echo objUser.Name

We won’t explain the intricacies of searching Active Directory; if you’re looking for an explanation of what we’re doing here – and why – take a look at this month’s Tales from the Script column. For now we’ll just highlight a couple of key points.

To begin with, here’s the query that locates all the users with the mailNickname kenmyer (and because mailNicknames must be unique, there will be – at most – only one user that fits the criteria):

objCommand.CommandText = _
    "SELECT distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND mailNickname = 'kenmyer'"

Pretty simple, huh? All we’re doing here is returning the distinguishedName attribute for all the user accounts (WHERE objCategory = ‘user’) with the mailNickname kenmyer. When we execute this query we get back a recordset consisting of – at most – a single item. We then use this code to loop through the collection and store the user’s distinguished name in a variable named strDN:

Do Until objRecordSet.EOF
    strDN = objRecordSet.Fields("distinguishedName").Value
    objRecordSet.MoveNext
Loop

Now that we know the user’s distinguished name we can bind to the user account and then do whatever we want (in our simple example, all we do is echo back the value of the Name attribute):

Set objUser = GetObject("LDAP://" & strDN)
Wscript.Echo objUser.Name

Again, all we did was supply the mailNickname and yet we ended up binding to the user account. What if all we had was the sAMAccountName? In that case we’d modify our query and look for a user with a specified sAMAccountName rather than a specified mailNickname:

objCommand.CommandText = _
    "SELECT distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND sAMAccountName = 'kenmyer'"

Hey, no need to thank us. After all, finding out ways to work around the system is what we do best!


How Can I Bind to an Active Directory User Account Using Windows PowerShell?

Hey, Scripting Guy! I’ve been playing around with Windows PowerShell, and I find it interesting. However, I have a problem. I’d like to use PowerShell as a quick way to get information about an Active Directory user account, but I can’t figure out how to bind to a user account. Any suggestions?

-- BW

Hey, BW. As we noted a few days ago, Windows PowerShell is still in beta; although the technology works (and works well), there are still some features missing. Unfortunately, one of those features happens to be a cmdlet that makes it easy to bind to and work with Active Directory objects (similar to what the Get-WMIObject cmdlet does for WMI). When that cmdlet is available, working with Active Directory will be a breeze; until then, well, we’re afraid you’re out of luck. See you later, BW; we’re going outside to enjoy the sunshine.

Um, we’re back. The Scripting Editor has … suggested … that we actually try to help you with your problem instead of just brushing you off and heading outside. Fine; we’ll see if we can figure out a way to use Windows PowerShell to bind to an Active Directory user account. But we’d like to state, for the record, that we’re only doing this because we’re scared of the Scripting Editor. (We’re still amazed that no one has ever dropped a house on her, if you know what we mean.)

Editor’s Note: The Scripting Editor would normally edit out something like that, but she’s decided instead to introduce the Scripting Guys to some flying monkeys. (She thought of sending them some poppies, but they already spend most of their day sleeping.)

If you’ve written VBScript scripts that access Active Directory you’ve probably used code similar to this:

Set objUser = GetObject _
    ("LDAP://cn=kenmyer, ou=Finance, dc=fabrikam, dc=com")

What’s wrong with that line of code? Nothing; it works just fine, provided you’re using VBScript. If you’re using Windows PowerShell, however, it’s a different story; that’s because Windows PowerShell doesn’t have a cmdlet analogous to the GetObject method. If you need to use GetObject to connect to something, well, that’s a problem.

So does that mean we are out of luck? Not quite. One of the cool things about Windows PowerShell is that it provides direct access to the .NET Framework. The Scripting Guy who writes this column isn’t exactly an expert on the .NET Framework, but he does know one thing: the DirectoryServices.DirectoryEntry class enables you to bind to an object in Active Directory. Can we use this .NET Framework class to get information about the Ken Myer user account? We’re about to find out.

Right after we step outside for a moment. Don’t tell you-know-who, OK? (Although that probably won’t matter; no doubt her flying monkeys will tattle on us anyway.)

Editor’s Note: Never fear, the monkeys have brought the Scripting Guys back to their offices to finish this.

Now, where were we? Oh, that’s right: something about using Windows PowerShell to bind to an Active Directory user account. Hey, all you had to do was ask:

New-Object DirectoryServices.DirectoryEntry "LDAP://cn=kenmyer, ou=Finance, dc=fabrikam, dc=com"

As you can see, this is actually a pretty neat little trick: what we do is call the New-Object cmdlet followed by the type of .NET object we want to create (DirectoryServices.DirectoryEntry). And then we follow that by specifying the ADsPath of the object we want to bind to. It’s that easy.

Incidentally, you can also use the New-Object cmdlet to access COM objects. That can get a tiny bit complicated, so we won’t discuss the whys and wherefores today. But, just to whet your appetite a little, here’s a command that starts up a visible instance of Microsoft Excel (note the use of the –com parameter):

$A = New-Object –com Excel.Application; $A.Visible = $True

Cool.

By default the Active Directory command we showed you a moment ago returns only the user’s distinguished name. (Which isn’t as useful as it might sound; after all, seeing as how the distinguished name is part of the ADsPath we already know the user’s distinguished name.) But that’s OK; after we bind to the user account we can then use the Select-Object cmdlet to pick out the properties we’re interested in. Did you say you wanted to know the user’s Name and telephoneNumber? Okey-doke:

New-Object DirectoryServices.DirectoryEntry "LDAP://cn=kenmyer, ou=Finance, dc=fabrikam, dc=com" |
Select-Object name, telephoneNumber

Now you want to get back all the properties and their values? Then just use the wildcard character (the asterisk) when calling Select-Object:

New-Object DirectoryServices.DirectoryEntry "LDAP://cn=kenmyer, ou=Finance, dc=fabrikam, dc=com" |
Select-Object *

Not bad for a workaround, is it?

Ah, that’s a good question: how can you determine, in advance, which properties are available for this user account? Here’s a hint for you: bind to the object, then pass the object to the Get-Member cmdlet, which then reports back all the object properties and methods.

Here’s an even better hint; just do this:

New-Object DirectoryServices.DirectoryEntry "LDAP://cn=kenmyer, ou=Finance, dc=fabrikam, dc=com" | Get-Member

We should note that all we’ve done today is answer a very specific question: how can you bind to, and get back information from, an Active Directory user account? No doubt you have other questions about using Windows PowerShell to work with Active Directory (e.g., how can I create/delete/modify a user account?). To be honest, we don’t have the answers to many of those questions; we’re pretty new to Windows PowerShell ourselves. But as we begin to find the answers you’ll be the first to know. Promise.

And now we are going outside. After all, in Seattle the sun is like a solar eclipse or a pay raise: if you’re lucky, you might get to see it once in your life. We’ve given up on the pay raise, but we have no intention on missing the sunshine.


How Can I Cause a User’s Password to Expire?

Hey, Scripting Guy! How can I cause a user’s password to expire?

-- GB

Hey, GB. You know, one thing people dislike about politicians is that any time you ask them a question many politicians will give you an answer to a different question. Even worse, if you press them on that point they’ll tell you that there’s a good reason why they did that: after all, it’s for your own good.

What does that have to do with the Hey, Scripting Guy! column? Well, instead of answering the question you asked, we’re going to answer a different question. But don’t worry: it’s for your own good.

So why is this for your own good? Well, we’re assuming there’s only one reason why you’d want to expire a user’s password: you want the user to have to change that password the next time they log on. You wouldn’t expire a password in order to prevent a user from logging on; if you don’t want a user logging on then you should disable or delete the user account. We want to force a user to change their password the next time they log on, and there’s an easier way to do that than by changing the password expiration date. All you have to do is run this little script instead:

Set objUser = GetObject("LDAP://CN=myerken,OU=Finance,DC=Fabrikam,DC=com")

objUser.pwdLastSet = 0
objUser.SetInfo

That’s right: there really isn’t much to it, is there? We begin by binding to the user account in Active Directory; that’s what this line of code is for:

Set objUser = GetObject("LDAP://CN=myerken,OU=Finance,DC=Fabrikam,DC=com")

Having done that, we then set the value of the pwdLastSet attribute to 0. pwdLastSet is an attribute that stores the date and time that the password for a given account was last set. If pwdLastSet is equal to 0 the user will have no choice but to change their password the next time they log on. In other words, without having to mess around with dates and times we’ve essentially “expired” their password: their current password will have to be changed the next time they log on. We set pwdLastSet to 0, then call the SetInfo method to write the change back to Active Directory.

Incidentally, you can do the same sort of thing with local user accounts using a script like this:

strComputer = "atl-win2k-01"
Set objUser = GetObject("WinNT://" & strComputer & "/kenmyer")

objUser.PasswordExpired = 1
objUser.SetInfo

In this script, we bind to the Ken Myer account on the computer atl-win2k-01 and then set the value of the PasswordExpired attribute to 1. We call the SetInfo method and, voilà: the next time Ken Myer logs on to this computer he’ll have to change his password.

So there you have it: we answered a question, even though it might not have been the exact question we were asked. Hopefully this will help: the last time we tried giving answers that didn’t match the questions was on our SAT test. That one didn’t work out too well.


How Can I Change a User’s Password?

Hey, Scripting Guy! How can I change a user’s password using a script?

-- GO

Hey, GO. You didn’t indicate whether you wanted to change the password for a local user or for an Active Directory user. But that’s OK: the processes are so similar we’ll go ahead and show you how to do both. It’s like getting two Hey, Scripting Guy! columns for the price of one.

Regardless of whether you want to change a local user password or an Active Directory user password you need to go through a two-step process. First you bind to the user account in question, and then you use ADSI’s SetPassword method to assign the user a new password. That’s it: two steps and you’re done.

To prove it, let’s start by changing the password for a local user. In the following script, we bind to the kenmyer user account on the computer atl-ws-01 and assign Ken the password i5a2sj*!:

Set objUser = GetObject("WinNT://atl-ws-01/kenmyer")
objUser.SetPassword("i5A2sj*!")

That’s the whole script right there: bind to the user account, call the SetPassword method, passing SetPassword the user’s new password. The only thing to watch out for is the way you format the provider name. It has to be WinNT, with the W and the NT in uppercase letters. Write that out in any other way – for example, winnt – and the script will fail. This is one of the very rare times in which case-sensitivity is important in VBScript. Other than that, there’s nothing to it.

Of course, you might be thinking, “Yeah, they start out with a local user script because local user accounts are so simple compared to Active Directory user accounts. Just wait until they try to change the password for an Active Directory user account.” Well, the waiting is over; here’s a script that changes the password for the kenmyer user account in the domain fabrikam.com:

Set objUser = GetObject("LDAP://cn=KenMyer,ou=Finance,dc=fabrikam,dc=com")
objUser.SetPassword("i5A2sj*!")

That’s right: it’s remarkably similar to the script for changing a local user password. The only difference is that we use the LDAP provider to bind to the user account (the LDAP provider is used when working with Active Directory and the WinNT provider is used when working with local accounts and Windows NT 4.0 domains). And, of course, the path to the actual account will vary depending on whether the account is stored locally or in Active Directory. But other than that the two scripts are identical.

We should mention that you can change any user account with these scripts, including the local Administrator account. Just replace kenmyer with Administrator:

Set objUser = GetObject("WinNT://atl-ws-01/Administrator")
objUser.SetPassword("i5A2sj*!")

In fact, as long as we’re at it, let’s give you three columns for the price of one. A question we get asked all the time is this: How do I change the local Administrator password for all the computers in an OU? Well, here’s your answer:

Set objOU = GetObject("LDAP://OU=Finance, DC=fabrikam, DC=com")
objOU.Filter = Array("Computer")

For Each objItem in objOU
    strComputer = objItem.CN
    Set objUser = GetObject("WinNT://" & strComputer & "/Administrator")
    objUser.SetPassword("i5A2sj*!")
Next

So what are we doing in this script? Well, we’re binding to the Finance OU in fabrikam.com. We’re then applying a filter to the collection we get back to make sure that we’re dealing only with computer accounts. After we’ve applied the filter, we loop through the collection of computer accounts. We grab the CN (essentially the NetBIOS name) for the first computer and store the name in the variable strComputer. We then connect to the Administrator account on that machine and change the password. The script loops around and repeats the process for the second computer in the collection, and then continues looping around and changing passwords until it’s hit every computer in the OU.

We know. But we won’t tell your boss how easy this is. Let him or her think you’re a real genius when you tell them you’ve discovered a way to automatically change all the local Administrator passwords on all the computers in an OU. The fact that it really takes just a few simple lines of code will be our little secret. (Trust us: we’ve never told our bosses how easy this is, either!)


How Can I Change a User’s Primary Group?

Hey, Scripting Guy! How can I change a user’s primary group?

-- AS

Hey, AS. You know, one of the Scripting Guys knows how to make an incredibly rich rocky road brownie, a brownie that’s topped with melted marshmallows and walnuts and chunks of dark chocolate and all sorts of other good stuff. Now, some people might ask whether or not, in the interests of promoting a healthy lifestyle, this Scripting Guy should make such a decadent dessert. But you know what? The Scripting Guys are doers, not thinkers: we’ll continue making brownies, and we’ll let others debate whether or not that’s really what we should be doing.

Hold on a second: we need to cut ourselves another brownie. Just a small one this time ….

The same thing is true of changing a user’s primary group: we know how to do it, but we don’t know whether or not you should do it. According to what little we know about Active Directory, the primary group is used only if you have to deal with Macintosh clients or POSIX-compliant applications; otherwise, you can just leave the primary group alone. (By default, the primary group for each user is Domain Users.) That means, for most people, you can probably file today’s column under Things I Don’t Need to Worry About, At Least Not Today. Most of you will be able to lead long and happy lives without ever changing a user’s primary group. (Yes, hard to believe. But true.)

But suppose you do need to change a user’s primary group. In that case, here’s a script that will take care of everything for you:

Set objUser = GetObject _
    ("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

Set objGroup = GetObject _
    ("LDAP://cn=Finance Managers,ou=Finance,dc=fabrikam,dc=com")

objGroup.GetInfoEx Array("primaryGroupToken"), 0

objUser.primaryGroupID = objGroup.primaryGroupToken
objUser.SetInfo

The script begins by binding to the user account you want to modify; in this sample script, we bind to the Ken Myer account in the Finance OU of fabrikam.com. We then create a second object reference, this one to the group that will become the user’s new primary group (Finance Managers). Note that this script assumes that the user is already a member of the group Finance Managers. If that’s not the case then you will need to add the user to the group before Finance Managers can be reassigned as the primary group.

Next we use the GetInfoEx method to explicitly retrieve the value of the primaryGroupToken attribute for Finance Managers. The primaryGroupToken is a computed attribute used when retrieving the membership list for certain groups. Because this is a computed attribute – that is, the value is generated on-the-fly rather than being stored as-is – we use GetInfoEx to force this computation and return the value. The GetInfoEx code looks like this:

objGroup.GetInfoEx Array("primaryGroupToken"), 0

While we’re on the subject, we should point out a few quirks regarding GetInfoEx. First, GetInfoEx always retrieves data as though it was stored as an array; hence the need to specify an array consisting of a single item (primaryGroupToken). The 0 tacked on the end of the call is meaningless but required: if you leave it off or if you substitute a different value, the script will fail.

To be honest, we don’t know why; that’s just the way it works.

The rest is easy. We set the value of the primaryGroupID to the primaryGroupTaken value we just retrieved, and then call the SetInfo method to write those changes to the user account in Active Directory:

objUser.primaryGroupID = objGroup.primaryGroupToken
objUser.SetInfo

That should do the trick. As we noted, by default all users are assigned Domain Users as their primary group. If you ever want to switch Ken Myer back to this default then simply run this script, which changes the primary group back to Domain Users:

Set objUser = GetObject _
    ("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

Set objGroup = GetObject _
    ("LDAP://cn=Domain Users,cn=Users,dc=fabrikam,dc=com")

objGroup.GetInfoEx Array("primaryGroupToken"), 0

objUser.primaryGroupID = objGroup.primaryGroupToken
objUser.SetInfo

That’s all you need to do. As for us, we’re going to cut ourselves another brownie. Just a small one this time ….


How Can I Change a User’s sAMAccountName?

Hey, Scripting Guy! How can I change the sAMAccountName from KMyer to Ken.Myer?

-- CP

Hey, CP. You know, William Shakespeare once said, “A rose by any other name would smell as sweet.” What does that have to do with your question abut changing the sAMAccountName for a user? Nothing really; it’s just that every now and then we like people to think that we’re well-rounded, culturally-literate guys who do something besides watch baseball and The Simpsons. The truth is we’re not culturally-literate guys who do something besides watch baseball and The Simpsons; we just don’t want anyone to know that. (Editor’s Note: That’s not true of all the Scripting Guys. Some of us watch football too.)

If Shakespeare was alive today (he is dead, isn’t he?) he’d probably answer that question by saying something like, “Leave me alone, Scripting Guys. I’m tired of answering scripting questions for you; I’ve got a play to write.” But that’s OK; fortunately, this is a question that we can answer ourselves, without any help from Mr. Shakespeare:

Set objUser = GetObject("cn=Ken Myer,
ou=Finance, dc=Fabrikam, dc=com")

objUser.sAMAccountName = "Ken.Myer"
objUser.userPrincipalName = "Ken.Myer"
objUser.SetInfo

Granted, it’s not iambic pentameter (or maybe it is; we aren’t totally sure what iambic pentameter looks like) but there’s still a certain beauty to the code, don’t you think? The script starts off by binding to the Ken Myer user account in Active Directory. We then use this line of code to change the sAMAccountName to Ken.Myer:

objUser.sAMAccountName = "Ken.Myer"

Now, we’re not saying that the Scripting Guys are better than Shakespeare; all we’re saying is that if you attend one of Shakespeare’s plays he doesn’t give you a bonus play absolutely free. The Scripting Guys, however, give you bonus code absolutely free. Although the following line of code is optional, many organizations like to keep their UPNs (user principal names) in sync with their sAMAccountNames; that way users have to remember only one name – Ken.Myer – as opposed to two: Ken.Myer and kmyer@fabrikam.com. Therefore, we tossed in this line of code to change the UPN as well:

objUser.userPrincipalName = "Ken.Myer"

Last, but definitely not least, we call the SetInfo method to write the changes back to Active Directory:

objUser.SetInfo

Whatever you do, don’t leave out this line of code. When you bind to a user account in Active Directory, a copy of that account is created in memory on your machine. As you make changes to the account properties, those changes are made on this copy; the original account remains in Active Directory, unchanged. The only way to update the actual user account, the one in Active Directory, is to call the SetInfo method, which is roughly equivalent to clicking the Save button in, say, Microsoft Word.

So there you have it, CP. As William Shakespeare once said, “See you tomorrow.”

Note. Hey, he probably said that at some point in his life.


How Can I Change the Location of a User’s Home Drive in Active Directory?

Hey, Scripting Guy! How can I change the location of a user’s home drive in Active Directory?

-- DS

Hey, DS. Did you ever read the story The Tell-Tale Heart by Edgar Allan Poe? In that story the narrator commits the perfect crime: he kills an old man and then dismembers and hides the body, taking such care that no one could possibly know that a murder had taken place. In the end, however, the murderer is done in by his own guilt. Believing that he still could hear the dead man’s heart beating from beneath the floorboards, he confesses to the crime: “I admit the deed!--tear up the planks!--here, here!--it is the beating of his hideous heart!"

To tell you the truth, DS, your question is sort of the tell-tale heart of the scripting world: it brings back terrible memories of one of the first scripts we posted in the Script Center, a script that changed user profile information in Active Directory. As luck would have it, somewhere along the way we dropped a colon from the drive letter (making it drive X rather than drive X:). That doesn’t seem like that big of a deal: after all, just edit the script and put the colon back. Unfortunately, though, back in those days we didn’t have the right to log onto TechNet and fix little problems like that. Consequently, we had to wait months before someone else could fix the problem for us, a period of time in which it seemed like every single person who visited the Script Center would write to us and say, “Hey, I tried that user profile script but it didn’t work.”

OK, look: “We admit the deed!--tear up the Script Center!--here, here!--it is the beating of the missing colon!"

All right, that might be carrying this a bit too far. But it still bugged us that we had posted a script that didn’t work and then couldn’t get the problem corrected.

By contrast, here is a script that should work. This script changes both the location of the home drive (the homeDirectory attribute) as well as the drive letter for the home drive (homeDrive). If all you want to do is remap the drive (that is, make the homeDrive drive X: rather than, say, drive Q:) then just leave out the line of code that assigns a value to homeDirectory:

Set objUser = GetObject("LDAP://cn=Ken Myer,ou=finance,dc=fabrikam,dc=com")

objUser.Put "homeDirectory", "\\atl-fs-01\users\kenmyer"
objUser.Put "homeDrive", "X:"

objUser.SetInfo

Yes, it is pretty easy, which is one reason why it drove us so crazy when it didn’t work before. (After all, it’s one thing if a 40,000 line script has a problem, but a four-line script?) We begin by binding to the Ken Myer user account in Active Directory, then use these two lines of code to specify the UNC path for the homeDirectory attribute and the drive letter (actually, the drive letter and colon) for the homeDrive:

objUser.Put "homeDirectory", "\\atl-fs-01\users\kenmyer"
objUser.Put "homeDrive", "X:"

And then we call the SetInfo method to save the changes and write the new values to the actual user account in Active Directory.

And now that our horrible secret is out, well, maybe we can pick up the pieces of our shattered lives and try to move on. In The Tell-Tale Heart the narrator was arrested and thrown in jail for his crime; you don’t think they’d do that to us just for leaving a colon out of a script, do you?

Wow; that’s harsh.


How Can I Clear the Logon Script Path for All My Users?

Hey, Scripting Guy! How can I clear the logon script path for all my users?

-- TF

Hey, TF. You know, during in a momentary bout of insanity one of the Scripting Guys once agreed to play his son in an Xbox college football game. “It’s really easy to play,” noted the Scripting Son, who then proceeded to provide a brief tutorial on how to run, pass, kick, and tackle. A half hour or so later – and trailing 150,000 to nothing – the Scripting Guy sighed. “Man, your guys just seem so much faster than mine: I can never outrun them and never catch up to them.”

“Well, that’s because you never use the turbo button,” replied the Scripting Son. “Did I forget to tell you about the turbo button?”

Yes, son, you did. Forearmed with the new-found knowledge about the turbo button, the Scripting Guy and his son squared off once more, this time in a fair-and-square battle for family video game supremacy.

Note. No, it doesn’t matter what the score was. After all, this was about quality bonding time between a father and a son; it had nothing to do with winning or losing. Besides, there was a glare on the TV screen and the B button on the Scripting Guy’s controller didn’t seem to work right. And, really, it’s just a silly little video game: what difference does it make if you happen to get crushed 104-7? Not that 104-7 really counts as a crush, mind you. Edged 104-7? Nipped 104-7? Lucked out 104-7? Something like that.

As it turns out, clearing the logon script path is just as easy as playing college football on the Xbox … provided that you know about ADSI’s version of the turbo button. What does that mean? Well, let’s show you the script and then answer the question:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2
Const ADS_PROPERTY_CLEAR = 1

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Set objUser = GetObject(objRecordSet.Fields("AdsPath").Value)
    objUser.PutEx ADS_PROPERTY_CLEAR, "scriptPath", 0
    objUser.SetInfo
    objRecordSet.MoveNext
Loop

For the most part this is a standard script for searching Active Directory: we simply retrieve the AdsPath attribute for all the user accounts, then bind to each user account and clear the value for the scriptPath attribute. We don’t have room in this column to explain how all that works; if you aren’t familiar with scripts that search Active Directory, you might want to take a look at our two-part Tales from the Script series titled Dude, Where’s My Printer?

So where’s the turbo button in all this? Well the turbo button shows up here, the part where we actually clear the logon script path for a user:

objUser.PutEx ADS_PROPERTY_CLEAR, "scriptPath", 0
objUser.SetInfo

As you can see, this is somewhat odd-looking code. You might expect us to do something a little more straightforward, like set the value of the scriptPath attribute to nothing:

objUser.scriptPath = ""
objUser.SetInfo

Why don’t we do this? There’s a simple reason for that: it won’t work. Setting the value of scriptPath to an empty string will not actually clear the scriptPath; instead, it sets the value to an empty string. You might think, “Well, so what?”, but to Active Directory there’s definitely a difference. Suppose we conduct a search for all the users that have a logon script assigned to them. Believe it or not, any user whose scriptPath has been set to an empty string will show up as having a logon script assigned to them. That’s because Active Directory believes that this user does have a logon script assigned; it’s just that the scriptPath happens to equal an empty string.

We know, we know. But that’s just the way it is.

Because of this, we have to specifically clear the scriptPath attribute; we can’t just set the value to an empty string. To clear the attribute we define a constant named ADS_PROPERTY_CLEAR and set the value to 1. We then call the PutEx method followed by three parameters:

· ADS_PROPERTY_CLEAR, indicating the type of operation we want to perform (clearing the value).

· “scriptPath”, indicating the attribute we want to clear.

· 0, a placeholder for the new value being assigned. We aren’t really assigning a new value, but without adding the 0 we’ll get an error message because PutEx will not have the required number of parameters.

After all that we then call the SetInfo method to officially write the changes to the user account object in Active Directory. The logon script path will now be cleared.

In case you’re wondering, in relative terms our hero’s performance in Xbox football wasn’t all that bad; after all, you should have seen what happened when he tried to play Halo 2. Not a pretty sight at all.


How Can I Configure an Active Directory Account So the Password Never Expires?

Hey, Scripting Guy! How can I configure an Active Directory user account so that the account password never expires?

-- BB

Hey, BB. Before we start we have a question for you: wouldn’t you agree that Catch-22 is one of the four greatest novels ever written? We ask that because, in his English class, the Scripting Son was recently asked to select, and read, a book from a list provided by the teacher. “Hey, Scripting Dad,” he said. “Have you read any of these books? Are any of them any good?”

“Good heavens, Scripting Son!” replied the Scripting Dad. “Catch-22 is on the list! Catch-22 is one of the four greatest novels ever written. Everyone knows that.”

“OK, I guess I’ll read that one,” replied the Scripting Son.

Of course, ever since then the Scripting Dad has had to listen to an unending litany of complaints:

· “This book is weird.”

· “This book doesn’t make any sense.”

· “I thought you said this was a good book.”

· “This is all your fault.”

Note. Actually that last one has nothing to do with Catch-22. That’s just a standard complaint the Scripting Dad hears every day.

In order to fight fire with fire, the Scripting Dad is looking for people who agree with him that Catch-22 is one of the four greatest novels ever written. Can he count on your support, BB? OK, what if he sweetens the deal by giving you a script that can configure an Active Directory user account so that the account password never expires:

Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000

Set objUser = GetObject("LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com")

intUserAccountControl = objUser.Get("userAccountControl")

If Not objUser.userAccountControl AND ADS_UF_DONT_EXPIRE_PASSWD Then
    objUser.Put "userAccountControl", _
        objUser.userAccountControl XOR ADS_UF_DONT_EXPIRE_PASSWD
    objUser.SetInfo
End If

What do you mean, “That’s still not good enough”? Wow; you’re a tough negotiator, BB. OK, how about this: how about we explain how this script works? Deal? Deal.

As you can see, the script starts out by defining a constant named ADS_UF_DONT_EXPIRE_PASSWD and assigning this constant the hexadecimal value &h10000. We’ll need this constant when we reconfigure the account so that its password never expires.

After defining the constant we connect to the Ken Myer user account in Active Directory. Because Ken has a CN equal to Ken Myer and because his account is in the Finance OU of Fabrikam.com the code required to connect to that account looks like this:

Set objUser = GetObject("LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com")

Now it starts to get a little weird (but just a little). As it turns out, the Active Directory property that determines whether or not a password expires is contained within the userAccountControl property. The userAccountControl property is a “bitmask” property: that means it’s a single property that contains multiple values. Is that important? You bet it is: because userAccountControl contains multiple values we can’t simply set it to, say, True or False. Instead, we need to use “bitwise logic” to toggle the particular property that controls password expiration. That’s just a heads-up to explain why some of the code you’re about to see will look a little unusual, to say the least.

Note. We won’t to explain bitwise logic in any detail today; for more information, see this section of the Microsoft Windows 2000 Scripting Guide.

Our next step is to determine whether or not the account already has a password that doesn’t expire; after all, if Ken already has a non-expiring password then we don’t need to do anything, do we? Using our bitwise logic we employ this line of code to determine if the “password doesn’t expire” property (represented by the constant ADS_UF_DONT_EXPIRE_PASSWD) is not enabled (notice the syntax If Not):

If Not objUser.userAccountControl AND ADS_UF_DONT_EXPIRE_PASSWD Then

Like we said earlier, the userAccountControl property is actually composed of a number of different property values; for the time being, think of them as being like switches on a control panel. All we’re doing here is checking to see if the password expires switch (again, represented by the constant ADS_UF_DONT_EXPIRE_PASSWD) is on. If it is, then we’re done. If it’s not, then we need to turn it on. Bitwise logic is confusing, especially at first glance. But, at least in this case, the basic idea is simple: we take an individual switch (property) and either turn it on (enable it) or turn it off (disable it).

And that’s exactly what we do with the next two lines of code. In the first line we use the Put method to change the value of the userAccountControl in the local cache. (What’s this local cache business all about? If you aren’t sure, see this Scripting Puzzle answer for details.) Notice that we need to pass the Put method two parameters:

· userAccountControl, the property we want to modify.

· objUser.userAccountControl XOR ADS_UF_DONT_EXPIRE_PASSWD, bitwise code for "toggling" the current value of the “password doesn’t expire” property. Essentially what we’re saying here is this: if the switch (property) represented by the constant ADS_UF_DONT_EXPIRE_PASSWD is on then turn it off; if it’s off then turn it on. This is easy to do, because the XOR operator simply toggles bitwise values on and off. That’s why we had to make sure that Ken Myer’s account currently did not have a password that never expires. If he already had a non-expiring password XOR would toggle that value, thus giving him a password that did expire.

See? Every now and then we do have a reason for doing whatever it is we do.

In the next line we call the SetInfo method to write the changes back to Active Directory. And with that, we’re done.

Of course, a lot of people might be more interested in going the other route; that is, taking an account where the password never expires and configuring it so that the password does expire. Here’s a script that does just that, checking to see if the “password never expires” property has been enabled and, if so, disables it (again, using XOR to simply toggle the value):

Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000

Set objUser = GetObject("LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com")

intUserAccountControl = objUser.Get("userAccountControl")

If objUser.userAccountControl AND ADS_UF_DONT_EXPIRE_PASSWD Then
    objUser.Put "userAccountControl", _
        objUser.userAccountControl XOR ADS_UF_DONT_EXPIRE_PASSWD
    objUser.SetInfo
End If

So what do you say, BB: is that enough to get your vote that Catch-22 is one of the four greatest novels ever written? We’ll take that as a “yes.”

Note. In case you’re wondering, the other three books on the list are – as everyone knows – Sometimes a Great Notion; Night of the Hunter; and The Old Man and the Sea. And, yes, we would put Green Eggs and Ham on there, too, except we don’t think it’s officially classified as a novel. But it’s a pretty good book nonetheless.


How Can I Configure the Hide From Exchange Address Lists Property for All the Contacts in a Domain?

Hey, Scripting Guy! How can I configure the Hide From Exchange Address Lists property for all the contacts in a domain?

-- MM

Hey, MM. You know what the say: you can run, but you can’t hide (especially from Microsoft Exchange!). But what they forgot to add is that you can hide, as long as you’ve set the value of the MSExchHideFromAddressLists attribute to True.

But you already knew that, didn’t you? What you really wanted to know was how you can configure this attribute for each and every contact in your domain, wasn’t it? Well, that’s easy; you just use a script like this one:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectClass='contact'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    strContactPath = objRecordSet.Fields("ADsPath").Value
    Set objContact = GetObject(strContactPath)
    objContact.MSExchHideFromAddressLists = TRUE
    objContact.SetInfo
    objRecordSet.MoveNext
Loop

If you look closely at the code (and we always hope that you do look closely at the code and don’t just take our word for it), you’ll see that this is an example of an Active Directory search script. As usual, we won’t take the time to explain each and every line of code here; if you need some background information on searching Active Directory, we recommend you take a look at our two-part series, Dude, Where’s My Printer? For now we’ll merely note that this is the query we use to retrieve a list of all the contacts in the domain:

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectClass='contact'"

Yes, very simple: all we do is request all the items in Active Directory where the objectClass attribute is equal to contact.

Of course, then the question becomes this: once we have a collection of all the contacts what the heck do we do with it?

Well, as you probably know, Active Directory search scripts are read-only: although we can use queries to retrieve data from Active Directory we can’t write any sort of Update query that can go out and modify that data. Instead, what we need to do is individually bind to each contact and change the value of the MSExchHideFromAddressLists attribute.

Don’t worry; it’s not as hard as it sounds. (We’ve already done the hard part, retrieving a list of all the contacts in the domain.) To begin with, we set up a Do Until loop that will walk us through our recordset, contact-by-contact (the EOF, of course, is short for end of file, meaning this loop will keep running until we run out of records):

Do Until objRecordSet.EOF

For each contact we grab the value of the ADsPath attribute and then use the GetObject method to bind to the contact account in Active Directory. That’s what we do here:

strContactPath = objRecordSet.Fields("ADsPath").Value
Set objContact = GetObject(strContactPath)

The ADsPath attribute, of course, looks something like this:

LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com

That’s why you don’t see us reference the LDAP provider when we call GetObject: we don’t need to reference the provider because it’s included in the ADsPath.

After binding to the contact account we set the MSExchHideFromAddressLists attribute to True, then call the SetInfo method to write the updated information to Active Directory:

objContact.MSExchHideFromAddressLists = TRUE
objContact.SetInfo

Don’t leave out the SetInfo method: if you do, the changes you make will not be saved to the contact account in Active Directory. And don’t leave out this line of code, either:

objRecordSet.MoveNext

This is the cue for the script to move from one record in the collection to the next. If you leave this out, your script will continue working with the first record in the collection from now until the end of time. (Which, in some ways, would be kind of cool, but not exactly what you had in mind.)

And that’s it: your contacts will now be hidden from the Exchange address lists. Now if only we could hide from email that easily ….


How Can I Copy My Users’ Phone Number Attribute to Their Description Attribute?

Hey, Scripting Guy! How can I copy a user’s phone number to his or her description for all the users in Active Directory?

-- DL

Hey, DL. You know, the Scripting Guy who writes this column considers today’s column to be his all-time favorite. Why? Because while you’re at work reading this he hasn’t even gotten out of bed yet. (Or, if he has, he’s just lying around the house eating doughnuts and watching TV.) Talk about the sweet life, huh?

Ah, yes, that’s a good question: how can he write and publish a column if he’s sleeping in past 10:00 AM? What is he, a master of the dark arts or something?

No, of course not. He’s simply – um, wait a second, we mean yes, yes he is a master of the dark arts, an evil sorcerer who will place a terrible spell on you unless you send him $19.95. No checks; credit cards or money orders only. Operators are standing by.

What’s that? You say you’re a little scared, but you’d like to see a demonstration of his powers before sending in the money? Well, we can do that, but, just remember, you asked for it. Hold on a second while we summon the Demons of the Netherworld (thanks goodness for instant messaging, huh?).

OK, here we go. By the power of Grayskull, we command thee to copy the telephone number attribute to the description, and for each and every user in a domain:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = "SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    strUserPath = objRecordSet.Fields("AdsPath").Value
    Set objUser = GetObject(strUserPath)
    strTelephone = objUser.telephoneNumber
    objUser.Description = strTelephone
    objUser.SetInfo
    objRecordSet.MoveNext
Loop

Sorry about that. The smoke will clear up in a second but the smell of brimstone might linger for a little while.

OK. As it turns out, we don’t really have time to discuss this script in minute detail, at least not the part that explains how to search Active Directory; after all, Oprah will be on any minute now. But don’t despair; if you need to know the fundamentals of conducting an Active Directory search take a look at our two-part series Dude, Where’s My Printer? available now for the low introductory price of just $19.95. Operators are standing by.

Come to think of it, that two-part series might actually be available for no charge. But you might want to send in the money anyway, just to be on the safe side.

About all we will do today is point out that this is the query used to select the value of the ADsPath attribute for all the users (objectCategory=’user’) in the fabrikam.com domain:

objCommand.CommandText = "SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

Why the ADsPath attribute? Well, ADsPath returns a connection string that makes it easy to bind to a user account in Active Directory. For example:

LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com

That’s important, because when we search Active Directory our queries are read-only; we can’t write some sort of Update query that automatically copies a user’s telephone number to his or her description attribute. Instead, we have to bind to each individual user account and copy the telephone number to the description. (We know, that sounds slow and tedious. But the script does all the work and, unless you have several million user accounts, it all happens remarkably fast as well.)

When we call the Execute method and run our query we get back a recordset containing the ADsPath for each user in the domain. We then set up a Do Until loop that runs until the value of the recordset’s EOF (End-Of-File) property is true; in other words, until we’ve looped through all the records in the recordset:

Do Until objRecordSet.EOF

The first thing we do inside the loop is grab the value of the ADsPath attribute and store it in a variable named strUserPath; we then use that variable to connect to the actual account object for that user. That’s what these two lines of code are for:

strUserPath = objRecordSet.Fields("AdsPath").Value
Set objUser = GetObject(strUserPath)

After we make the connection we take the value of the telephoneNumber attribute and store it in a variable named strTelephone. We then use these two lines of code to set the user’s Description attribute to his or her telephone number:

objUser.Description = strTelephone
objUser.SetInfo

Whatever you do, don’t leave out the call to the SetInfo method; that’s the method that actually writes the changes back to Active Directory. If you don’t call SetInfo the change will be made in memory, but not in Active Directory.

After that we call the MoveNext method to move to the next record in the recordset and then repeat the process for the next user.

And yes, we could have combined a couple of these lines of code in order to make the script a tiny bit smaller. However, we thought this method would be a little easier for people to follow.

Seeing as how Microsoft’s lawyers are breathing down our necks we should probably come clean here and state, for the record, that the Scripting Guy who writes this column is not an evil sorcerer and has no magic powers. (He used to be able to juggle a little, but that probably doesn’t count.) Instead, the Scripting Guy who writes this column is actually on vacation until the first of the year; however, he wrote a few extra columns before he left, the better to get everyone through this week and tide you over until he gets back. What a wonderful and selfless act, huh?

Note. OK, he didn’t really do it to be nice; he only wrote these columns because the Scripting Editor said he should. And we always do whatever the Scripting Editor says, because, like all editors everywhere, she really is a practitioner of the dark arts. What, you thought Peter Costantini was born a newt?

But take heart, Peter: she can’t hold a grudge forever.


How Can I Determine the ADsPath for the Logged-On User?

Hey, Scripting Guy! In my HTA, how can I determine the ADsPath for the logged-on user?

-- AC

Hey, AC. You know, when you work at Microsoft people automatically assume that you spend your days in an ivory tower, totally removed from the day-to-day activities and needs of system administrators. To be honest, that isn’t true: our tower is really more concrete and steel than it is ivory.

Well, except for Dean’s office.

As for being totally removed from the day-to-day activities and needs of system administrators, well, there might be an element of truth in that. For example, the Scripting Guys spend a lot of time writing about scripts and answering questions about scripts, but we rarely find ourselves sitting down to write a script that we actually use as part of our job. That’s not necessarily a good thing, but it is the nature of the technical writing business. To paraphrase the old saying, those who can’t – or at least those who don’t – teach.

Every now and then, however, we do get called upon to write a real script. One of those occasions arose just a week or two ago and, by happy coincidence, we were asked if we could do the very same thing you’re asking: determine the ADsPath for the logged-on user. As it turned out, we could, and we quickly put together a script to do so. And now we’ll let everyone else in on the secret.

Let’s start off by showing you how to do this in a plain old VBScript script, and then we’ll show you a simple way to embed the code in an HTA. Here’s a script that reports back the ADsPath for the logged-on user:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

Wscript.Echo objUser.AdsPath

Note. Well, sure, it is pretty simple. But, remember, we didn’t say we worked hard on this real-life script, we just said we actually worked on a real-life script.

The script starts out by creating an instance of the ADSystemInfo object:

Set objSysInfo = CreateObject("ADSystemInfo")

Incidentally, this is an incredibly useful object, one that can return all sorts of information about the logged-on user and the local computer. (You might take a look at this script to get an idea of the information that can be returned using ADSystemInfo.) The one drawback to this object is the fact that it can only be created locally: you can’t create an instance of ADSystemInfo on a remote computer and then get information about the user logged on to that machine. And, yes, that would be very cool. But it can’t be done.

One of the pieces of information that ADSystemInfo returns is the UserName. The UserName is actually the distinguished name of the logged-on user; that’s going to look something like this:

CN=Ken Myer,OU=Finance,dc=fabrikam,dc=com

At this point we can actually construct the ADsPath ourselves; after all, the ADsPath is simply the LDAP: provider plus the distinguished name:

LDAP://CN=Ken Myer,OU=Finance,dc=fabrikam,dc=com

However, we wanted to show you a more generic way to use ADSystemInfo, so we decided to go ahead and bind to the Ken Myer user account using these two lines of code:

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

As you can see, all we do is grab the value of the UserName property and store it in a variable named strUser, then use that variable as part of the GetObject call that binds us to the user account.

From there we simply echo back the value of the ADsPath attribute; seeing as how we’re connected to the Ken Myer user account, though, we could just as easily echo back the value of any (or all) of Ken’s account attributes.

That is easy, isn’t it?

Now, what about using this code in an HTA? Well, here’s a very simple example of that:

<html>
<head>
    <title>ADSystemInfo Sample</title>
</head>

<SCRIPT LANGUAGE="VBScript">

    Sub Window_OnLoad
        On Error Resume Next

        Set objSysInfo = CreateObject("ADSystemInfo")

        strUser = objSysInfo.UserName
        Set objUser = GetObject("LDAP://" & strUser)

        DataArea.InnerHTML = objUser.AdsPath
    End Sub

</SCRIPT>

<body>

    <span id=DataArea></span>

</body>
</html>

This HTA contains only a single element: a <span> tag named DataArea. When the HTA starts up, it runs a subroutine named Window_Onload. (Any subroutine with that name automatically runs any time an HTA is started or refreshed.) The code for the Window_Onload subroutine should look familiar:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

DataArea.InnerHTML = objUser.AdsPath

That’s right: it’s the script we just showed you, but with one exception: instead of using Wscript.Echo to echo back the AdsPath for the logged-on user we use this line of code to write the AdsPath to the <span> tag:

DataArea.InnerHTML = objUser.AdsPath

Now, does that look like the sort of thing someone sitting in an ivory tower could write?

Well, OK, now that you mention it ….


How Can I Determine the Home Directory For All the Disabled User Accounts in Active Directory?

Hey, Scripting Guy! How can I determine the home directory for all the disabled user accounts in Active Directory?

-- DB

Hey, DB. You know, it’s not like the Scripting Guy who writes this column is addicted to TV or anything, but last night he was watching TV (as usual) when he saw a brief feature on a new device that will scramble eggs for you. As the commentator noted, scrambling eggs is a “difficult and tiring chore.” But you’ll never have to scramble your own eggs again; instead, you can just let this new egg scrambler thing scramble those eggs for you.

Now, if you want to know the truth, the Scripting Guy who writes this column has never really found egg scrambling to be all that difficult or all that tiring. Admittedly, he usually lies down and takes a nap after scrambling eggs, but that’s due more to him being lazy than it is to egg scrambling being particularly difficult or tiring. Apparently, however, egg scrambling is a major problem for people. Or – check that – egg scrambling used to be a major problem for people. Now, thanks to the wonders of modern technology, you’ll “never use that whisk again.” Instead, the next time you want scrambled eggs all you have to do is take an egg, slide it onto this little needle-like thing, then turn on the egg scrambler. When you take the egg out and crack it open – voila! the egg has already been scrambled! Right in its very own shell!

Will wonders never cease?

At any rate, DB, we almost didn’t answer your question today; in fact, we toyed with the idea of never bothering to answer a question ever again. We’re sure you can understand why. After all, a script that determines the home directory for all the disabled user accounts in Active Directory looks pretty lame when compared to an automatic egg scrambler. But the show must go on, right? The following script won’t scramble eggs for you (and for that we apologize), but maybe someone can get some use out of it:

On Error Resume Next

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000

objCommand.CommandText = _
    "<LDAP://dc=fabrikam,dc=com>;" & _
    "(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=2));" & _
            "Name,homeDirectory;Subtree"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    Wscript.Echo objRecordSet.Fields("homeDirectory").Value
    Wscript.Echo
    objRecordSet.MoveNext
Loop

As usual, we won’t discuss each and every line of code in this script; if you could use a refresher course on writing Active Directory search scripts we recommend you take a look at our two-part Tales from the Script series Dude, Where’s My Printer? However, we will spend a miute or two looking at the query we use in this script, a query that retrieves the values of the Name and homeDirectory attributes for all the users whose user accounts have been disabled:

objCommand.CommandText = _
    "<LDAP://dc=fabrikam,dc=com>;" & _
    "(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=2));" & _
            "Name,homeDirectory;Subtree"

Why does our query look like this? Well, in order to determine whether or not a user account has been disabled we need to check the value of the userAccountControl attribute. As it turns out, userAccountControl is a bitmask attribute, an attribute that actually contains a number of properties and their values. (For a list of those properties, see the Active Directory Schema Reference on MSDN.) Because userAccountControl is a bitmask attribute, we can’t use a standard SQL query (e.g., “Select Name, homeDirectory From ….”) in order to retrieve information based on that attribute; it just won’t work. Instead, we have to use the LDAP query syntax. And that’s why this query looks so weird.

Without going into too much detail (for that, see our Scripting Guys webcast on writing Active Directory search scripts) here’s an explanation of the various parts of the query (note that the individual parts in the query itself are separated using semicolons):

· <LDAP://dc=fabrikam,dc=com>. This one you can probably figure out yourself: it’s simply the domain we want to search.

· (&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=2)). This, believe it or not, is our query filter, the part where we specify the criteria for the search. In this case, we have two criteria that must be met: the object must have an objectCategory equal to user, and the flag for a disabled user account (which has a value of 2) must be set. Granted, none of that is particularly obvious just from looking at the query, but it’s true. For example, the 1.2.840.113556.1.4.803 is the “LDAP bit matching rule,” something which is essentially equal to the AND keyword in a Boolean command like If objUserAccountControl AND 2.

So why didn’t the LDAP query folks just use the word AND? We don’t know; we can only speculate that it really wouldn’t be an LDAP bit matching rule if they didn’t. But don’t worry about that; just use the syntax exactly as we’ve shown and you’ll be fine.

Incidentally, the ampersand (&) at the beginning of the filter string simply ties together the two parts of our query: the objectCategory must be equal to user and the appropriate flag in the userAccountControl must be set. Again, take a look at our webcast if you’d like more information on LDAP queries. If you’d just as soon not have more information, well, that’s fine. In that case, just copy the code shown here and tweak it if you ever want to query on a different property found in the userAccountControl. For example, if you’re a user who has a password that never expires, the flag with the value 65536 will be enabled in your userAccountControl. That means we can search for you (and your fellow non-expiring password types) using this filter:

(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=65536))

All we did was change the value that we’re looking for, substituting 65536 for the value 2 we used in our original filter.

Note. You might have noticed that our LDAP query seems kind of jammed together; for example, we have objectCategory=user instead of spacing that out a bit nicer: objectCategory = user. Is that important? Surprisingly enough, it is. If you put blank spaces between the attribute, the equals sign, and the value (e.g., objectCategory = user) your script will fail. In other words, jam everything together as closely as you can.

· Name,homeDirectory. This filter piece is a bit easier to understand: it’s just a list of the property values we want the script to return. Note that we separate the individual values using commas. The semicolon (as noted) is used to separate the individual filter pieces.

· Subtree. This is the search scope, and simply tells the script to start the search in the domain root and then search every container found in that domain.

Yes, that is kind of crazy. However, it does work, and once we define the query all we have to do is call the Execute method to kick off the search and get back a recordset containing the Name and homeDirectory for all the disabled user accounts. As soon as we have that recordset we can set up a Do Until loop that loops through the returned data, echoing back the appropriate values for each account:

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    Wscript.Echo objRecordSet.Fields("homeDirectory").Value
    Wscript.Echo
    objRecordSet.MoveNext
Loop

Two things to pay attention to here. First, note the syntax for referring to an item in the recordset:

objRecordSet.Fields("Name").Value

Again, don’t worry about the why. Just copy the code as-is and your script should work just fine.

Second, remember to call the MoveNext method in order to move on to the next record in the recordset. Leave out MoveNext and your script will echo back information for the first record, return to the beginning of the loop, and then – alas – echo back information for the first record all over again. This will continue forever and ever and ever. Any time you work with an Active Directory recordset you must specifically tell the script to move to the next record. If you don’t, it won’t.

And that should do it, DB. Like we said, this won’t help you much with your scrambled egg problems; fortunately, though, there are entire teams of researchers and engineers working around the clock to help alleviate those problems.

Note. In the interests of full disclosure we should note that the Scripting Guys actually took a whack at this ourselves: we shook a chicken really, really hard right before she laid her eggs, hoping that she would go ahead and lay a pre-scrambled egg. (Editor’s Note: No, we really didn’t. And don’t you try it, either.) How did that go? Let’s put it this way: not quite as well as we had hoped.

And the chicken wasn’t all that happy with the process either.


How Can I Determine the Logon Name for a User?

Hey, Scripting Guy! How can I determine the user logon name for a user named John Smith?

-- FR

Hey, FR. You know, we’d like to tell you how to determine the user logon name for a user named John Smith, but we can’t: that’s because we have a specific list of names representing the only user names we can reference in one of our sample scripts. Unfortunately, John Smith isn’t on that list, so we can’t use his name.

Yeah, we feel bad about that, too, but our hands are tied. But tell you what: how about we show you a script that determines the user logon name for a user named Ken Myer? Yes, we know: it’s really not the same, is it? But it’s the best we can do:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT sAMAccountName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND givenName='Ken' AND sn='Myer'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("sAMAccountName").Value
    objRecordSet.MoveNext
Loop

As you probably figured out, this is a script that searches Active Directory. We’re not going to explain each and every line of code used in this script; that would take too long. If you’re not familiar with Active Directory search scripts we encourage you to take a look at our two-part Tales from the Script series Dude: Where’s My Printer? All the crazy-looking stuff you see in this script – ADsDSOObject, ADS_SCOPE_SUBTREE, ADODB.Command – is explained in detail in those two columns.

We will, however, point out a few things regarding the query used to conduct the search. Probably the hardest part about writing scripts that search Active Directory is knowing the property names to search for. For example, you referred to the user logon name. We know what you mean by that and you know what you mean by that, but Active Directory has no idea what a user logon name is. Instead, Active Directory calls that same thing the sAMAccountName. (Note: Although the letter casing doesn’t matter, we write this property name out as sAMAccountName simply because that’s the official name for the attribute.) Consequently, our SQL query retrieves the sAMAccountName for the specified user.

And how do we specify that user? Well, we’re looking for an Active Directory object that meets three criteria:

· Is a user account. To limit returned data to user accounts we search for items where the objectCategory is equal to user.

· Has a first name of Ken. Of course, Active Directory doesn’t know what a first name is. Therefore, we need to search for users with a givenName of Ken.

· Has a last name of Myer. As you might expect, Active Directory has never heard the term “last name,” either. Instead, we need to search for the sn (surname) Myer.

Add that all together, and we end up with a query that looks like this:

objCommand.CommandText = _
    "SELECT sAMAccountName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND givenName='Ken' AND sn='Myer'"

The rest is easy. We execute the query, and Active Directory returns a recordset consisting of all the users with a giveName of Ken and an sn of Myer. We then set up a Do Until loop to walk through the recordset, echoing back the sAMAccountName for each user. (Ideally there will be only one Ken Myer in Active Directory, but you could have more than one user with the same first and last name. In that case the sAMAccountName will be the differentiating factor, because sAMAccountNames must be unique.)

Does that help? OK, look, don’t tell anyone we said this, but take the script we just showed you, replace Ken with John and Myer with Smith and you’ll have a script that searches for a user named John Smith.

But that’s just between you and us, OK? OK.


How Can I Determine the OU a User Account Belongs To?

Hey, Scripting Guy! How do I determine the OU a user account belongs to?

-- CO

Hey, CO. Ah, yes: the OU a user belongs to. No doubt a lot of you are thinking, “Well, there’s probably an OU property of some kind in Active Directory that gives you that information. All you have to do is locate the user account, and then get the value of the OU property.”

That makes so much sense that – as you might have guessed – it’s not the way things work. As it turns out, there is no OU property (or anything equivalent) in Active Directory. But don’t fret. That doesn’t mean we can’t determine the user’s OU; it just means we – gasp! – have to work a little harder to do so.

For the purposes of this column, we’re assuming that you don’t know the user’s distinguished name; after all, with a distinguished name like CN=kenmyer,OU=Finance,DC-fabrikam,DC=com, well, even a Scripting Guy can figure out which OU the user belongs to. So we’ll assume that all you know is the user’s SAM account name, the name he or she typically uses to log on to the domain.

If you do know the SAM account name you’re in luck; that’s because these names must be unique in the Active Directory forest. Therefore, we can find this user by doing an Active Directory search:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND sAMAccountName='kenmyer'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("distinguishedName").Value
    objRecordSet.MoveNext
Loop

As we’ve noted before, there’s quite a bit involved in carrying out an Active Directory search, even though the script isn’t really all that long or all that complicated. For more information, check out this Tales From the Script column.

Without going into any detail, what we’ve done here is search fabrikam.com for the user with the sAMAccountName kenmyer, As soon as we find him we echo his distinguished name (the distinguishedName attribute). Because that’s in the form CN=kenmyer,OU=Finance,DC=fabrikam,DC=com, we can look at the results and determine the user’s OU.

But the truth is, we’re too lazy to look at the user’s distinguished name; we want the script to do that for us. So let’s make a slight modification to our Do loop. Here’s the modified code; we’ll explain how it works in a second.

Do Until objRecordSet.EOF
    strDN = objRecordSet.Fields("distinguishedName").Value
    arrPath = Split(strDN, ",")
    intLength = Len(arrPath(1))
    intNameLength = intLength - 3
    Wscript.Echo Right(arrPath(1), intNameLength)
    objRecordSet.MoveNext
Loop

Whoa! But don’t panic; this actually makes sense. We start, again, by getting the value of the user’s distinguished name; as we already know, this will be something like CN=kenmyer,OU=Finance,DC=fabrikam,DC=com.

We store this value in the variable strPath, then use the VBScript Split command to, well, split strPath into elements of an array. When we use the Split command and split on the comma (that is, we indicate that the comma is our delimiter, the character that separate the individual items in the string), we end up with an array that consists of the following elements:

CN=kenmyer
OU=Finance
DC=fabrikam
DC=com

We know that the second element in the array (and the second element in an array always has an index number of 1) is the OU where the user account resides. That means that, to get the name of the OU, we just need to take the second element (arrPath(1))and then get rid of the OU=.

Can we do that? Of course we can. We use the Len function to determine the number of characters in element 0 of our array. Remember, the value is OU=Finance, so the number of characters is 10.

Next we subtract 3 from the number of characters. Why? Well, we want to get rid of OU=, and OU= has three characters. Subtracting 3 from 10 leaves us with 7, which means we now know that the actual name of the OU – Finance – is 7 characters long.

Now – finally – we use the Right function to grab the last 7 characters in the string; in other words, we start at the e in OU=Finance and move back 7 characters. And guess what? You got it: the last 7 characters in OU=Finance just happen to be Finance, the name of the OU where the user account lives. Hey, we did it!

Here’s the revised script that searches Active Directory and reports back the OU where the user account resides.

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT distinguishedName FROM 'LDAP://dc=fabrikam,dc=com' " & _
        "WHERE objectCategory='user' " & _
            "AND sAMAccountName='kenmyer'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    strDN = objRecordSet.Fields("distinguishedName").Value
    arrPath = Split(strDN, ",")
    intLength = Len(arrPath(1))
    intNameLength = intLength - 3
    Wscript.Echo Right(arrPath(1), intNameLength)
    objRecordSet.MoveNext
Loop


How Can I Determine the SID for a User Account?

Hey, Scripting Guy! How can I determine the SID for a user account?

-- MD

Hey, MD. For those of you whose eyes glaze over any time they see an acronym (not that we blame you), SID is short for Security Identifier. A SID is a unique ID string (e.g., S-1-5-21-1454471165-1004336348-1606980848-5555) that is assigned to each account created in a domain or on a local computer. For our purposes, we’ll just say that SID is how the operating system keeps track of accounts. For example, you can rename the Administrator account on a computer and still use that account to function as an administrator because Windows doesn’t really care what the name is; Windows still knows that this account is the Administrator account because the SID remains the same regardless of the account name. It’s like your Social Security Number which – assuming you haven’t had your identify hijacked – uniquely identifies you regardless of the name you go by.

Most of the time you don’t need to worry about SIDs, which is good: obviously it’s easier to deal with an account name like kenmyer than it is to deal with a SID like S-1-5-21-1454471165-1004336348-1606980848-5555. However, there are times when it’s useful to know which SID goes with which user account. WMI’s security classes, for example, rely on SIDs; likewise, the Windows registry tracks user profiles by SID rather than by name (take a look at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList to see what we mean). You might be able to live your entire scripting life without ever needing to know a user’s SID. But, then again ….

So how do we find a user’s SID? Well, we use a script similar to this, which returns the SID for the user kenmyer with an account in the fabrikam domain:

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

Set objAccount = objWMIService.Get _
    ("Win32_UserAccount.Name='kenmyer',Domain='fabrikam'")
Wscript.Echo objAccount.SID

As you can see, the SID is practically longer than the script. All we do here is connect to the WMI service, and then use the Get method to bind to a specified instance of the Win32_UserAccount class. Notice we don’t use ExecQuery and return a collection of all the SIDs in our domain; that won’t work. Instead, we have to use Get and specify a particular user account. After that, it’s simply a matter of echoing the SID, which we do in the last line of the script.

Incidentally, this works just as well for local user accounts. The only difference is that you don’t specify a domain name for the Domain parameter; instead, you specify the name of the local computer. For example, this script returns the SID for the local user account kenmyer on the computer atl-ws-01:

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

Set objAccount = objWMIService.Get _
    ("Win32_UserAccount.Name='kenmyer',Domain='atl-ws-01'")
Wscript.Echo objAccount.SID

Pretty slick, huh?

Of course, it’s possible that you might need to go the other direction; that is, you might have a SID and need to know which account that SID belongs to. Can you do that? Of course you can:

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

Set objAccount = objWMIService.Get _
    ("Win32_SID.SID='S-1-5-21-1454471165-1004336348-1606980848-5555'")
Wscript.Echo objAccount.AccountName
Wscript.Echo objAccount.ReferencedDomainName

The big difference here is that instead of getting an instance of the Win32_UserAccount class we get an instance of the Win32_SID class (and note that we pass the SID as the parameter to the Get method). As soon as we’ve retrieved that instance, we echo the account name and domain name, and we’re off and running.


How Can I Determine When a User Last Changed His or Her Password?

Hey, Scripting Guy! How can I determine when a user last changed his or her password?

-- MG

Hey, MG. A long time ago one of the Scripting Guys worked at a local university. In one of the departments there it was easy to determine when a user last changed his or her password: that’s because the administrative assistant kept a list of all the users in the department, all their passwords, and the dates and times when those passwords were changed. Best of all, she kept that list in a folder out on her desk; that way if anyone ever forgot their password they could just grab the folder and look up their password.

And, yes, anyone else’s password as well.

So that’s one way to determine the last time a user changed his or her password. But here’s another way:

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=Fabrikam,DC=com")
Wscript.Echo "Password last changed: " & objUser.PasswordLastChanged

Yes, that is the entire script; this isn’t one of those Web sites where we just show you the beginning of something and then make you pay to see the rest. (Although now that we think about it….) If we want to know the last time Ken Myer changed his password we just need to do two things:

· Bind to Ken’s user account in Active Directory.

· Echo the value of the PasswordLastChanged attribute.

That’s all you have to do. Really.

Of course, we’re assuming that you’re talking about an Active Directory user account; it’s quite possible that you’re talking about a Windows NT 4.0 user account, or even a local user account. If so, we appear to have a bit of a problem; after all, you need to use the WinNT provider to access NT 4.0 or local user accounts, and the WinNT provider does not support the PasswordLastChanged attribute.

So does that mean we’re out of luck when it comes to NT 4.0 or local user accounts? Ah, you guys should know us better than that:

strComputer = "atl-ws-01"

Set objUser = GetObject("WinNT://" & strComputer & "/kenmyer")

intPasswordAge = objUser.PasswordAge
intPasswordAge = intPasswordAge * -1
dtmChangeDate = DateAdd("s", intPasswordAge, Now)

WScript.Echo "Password last changed: " & dtmChangeDate

As you can see, this script is a tiny bit longer, but still pretty simple. We begin by connecting to the Ken Myer account (username kenmyer) on the computer atl-ws-01. (What if you wanted to connect to an NT 4.0 domain instead? That’s fine; just replace the value atl-ws-01 with the name of that domain.) We then grab the value of the PasswordAge attribute and store it in a variable named intPasswordAge.

How come we don’t just echo the value of PasswordAge? Well, PasswordAge actually represents the number of seconds that have expired since the password last changed. Suppose you get back an answer like this:

50725249

Off the top of your head, can you subtract 50725249 seconds from the current date and time and calculate when the password was last changed?

Really? Wow; that’s impressive. We couldn’t do that ourselves, however, so we let the script do it for us. To begin with, we took the value of intPasswordAge and multiplied it by -1:

intPasswordAge = intPasswordAge * -1

Why? Well, in the very next line of code we’re going to use the DateAdd function to determine the date the password was last set. To do that, we need to pass DateAdd three parameters:

· The time interval we’re dealing with. Because PasswordAge is stored in seconds we use “s” as our first parameter.

· The number of seconds to add to the current date. This is why we multiplied intPasswordAge by -1. We don’t really want to add intPasswordAge to the current date; that would give us a time somewhere out in the future. Instead, we want to subtract intPasswordAge from the current date; after all, intPasswordAge is the number of seconds that have elapsed since the password was last changed. Because adding a negative number is the same thing as subtracting a positive number, we multiply intPasswordAge by -1.

· The current date and time. For that we use the VBScript function Now.

That’s it; the result of our equation is stored in the variable dtmChangeDate, and in the last line of code we echo the value of that variable. At the time we created and tested this script it told us the password was last changed on 11/22/2003 at 12:02:10 PM. (It also told us that we don’t do a very good job of keeping our local user account passwords up to date.)


How Can I Determine When an Active Directory User Account will Expire?

Hey, Scripting Guy! How can I determine when an Active Directory user account will expire?

-- JK

Hey, JK. By the way, please accept our apologies for hiding under the desk and pretending we weren’t in when your question arrived; we always get a little nervous any time we get questions involving dates and Active Directory. That’s because Active Directory uses a number of different methods for storing dates and times, some of which are downright weird. For example, the LastLogon time is stored as a Large Integer value, and represents the number of seconds that elapsed between the 0 hour on January 1, 1601 and the time the user last logged on to the domain. (We kid you not.) VBScript can’t handle Large Integer values directly; therefore we need to go through all sorts of machinations involving higher mathematics, conversion algorithms, and downright wishful thinking in order to turn the Large Integer value into something VBScript can handle. And even then we don’t actually have a date and time, just a value that even more mathematical manipulation can eventually turn into a date and time.

Note. For a more detailed explanation of this issue – and others – surrounding the last logon time see this article in the Scripting for Windows Server 2003 center.

Fortunately, though, we can determine when a user account will expire without resorting to quantum mechanics or black magic. This doesn’t mean that the AccountExpirationDate attribute – the property that tells us when an account expires – isn’t without a quirk or two, but at least those quirks are easy enough to work around.

Let’s take a look at a script that tells when the Ken Myer user account expires:

On Error Resume Next

Set objUser = GetObject("LDAP://cn=MyerKen,ou=Finance,dc=fabrikam,dc=com")

dtmAccountExpiration = objUser.AccountExpirationDate

If Err.Number = -2147467259 OR dtmAccountExpiration = #1/1/1970# Then
    WScript.Echo "This account has no expiration date."
Else
    WScript.Echo "Account expiration date: " & objUser.AccountExpirationDate
End If

To begin with, you might have noticed the very first line in the script: On Error Resume Next. Typically we don’t include error-handling in our sample scripts; error-handling, while important in real-life scripting, often obscures the key points in our sample scripts, scripts designed more for educational purposes than for immediate use in the real world. In this case, however, error-handling is a must; that’s because an error will occur if no expiration date has been specified for an account. If we don’t anticipate and trap that error, our script will fail. Thus we start things out with On Error Resume Next; this will allow our script to continue on should it encounter an account that has no expiration date.

In line 2 we bind to the Ken Myer account in Active Directory; following that we store the value of the AccountExpirationDate in a variable named dtmAccountExpiration. This is the point where an error can occur: if the user account has no specified expiration date then error number -2147467259 is generated. We need to check to see if that error has occurred.

We also need to check for another eccentric feature of AccountExpirationDate. In Active Directory you can configure a user account so it never expires; when you do that, the AccountExpirationDate is set for January 1, 1970. (Yes, that is a weird way to ensure that an account does not expire.) If we get back error number -2147467259 that means no expiration date has been set for the account; that means the account does not expire. However, if we get back an account expiration date of January 1, 1970, that also means that the account does not expire. Therefore, we need to use an If-Then statement that checks for both of these possibilities:

If Err.Number = -2147467259 OR dtmAccountExpiration = #1/1/1970# Then

As you can see, if the error number (Err.Number) is equal to -2147467259 or if the account expiration date is January 1, 1970 (1/1/1970) we echo a message stating that the account has no expiration date; otherwise we echo back the expiration date. It’s weird, but it works, and at least we don’t have to hide under the desk. (Although that wasn’t a total loss: we did find our stapler, a bunch of paperclips, and the other half of that sandwich while we were under there.)


How Can I Determine Which Domain Controller Authenticated a User?

Hey, Scripting Guy! In my logon script how can I determine the name of the domain controller that authenticated the user?

-- CK

Hey, CK. You know, one of the unwritten rules for being a Scripting Guy is that you should never do anything hard more than once a week. (Why is this an unwritten rule? Mainly because we were all too lazy to write it down.) In yesterday’s column we tackled the somewhat complicated issue of trying to associate an IP address with a network connection. Because yesterday’s column was hard, the unwritten laws for being a Scripting Guy pretty much compel us to take an easy question today.

So how easy could it be to determine the name of the domain controller that authenticated the logged-on user? As easy as three lines of code:

Set objDomain = GetObject("LDAP://rootDSE")
strDC = objDomain.Get("dnsHostName")
Wscript.Echo "Authenticating domain controller: " & strDC

No, you’re not dreaming; this really does take just three lines of code. (And even if you were dreaming, well, do you really want to admit that you dream about scripting?) We begin by binding to rootDSE, which represents the root of the Active Directory service on a domain controller. The rootDSE object exists to provide information about a domain and a domain controller; in fact, one piece of information rootDSE provides is the value of the dnsHostName property.

That might not be the most intuitive property name in the world, but dnsHostName is the name of the authenticating domain controller. Consequently we use the Get method to retrieve the value of the dnsHostName attribute and store that value in a variable named strDC. Echo back the value of strDC and we’ve determined the name of the domain controller that authenticated the user.

And now, having presented our three-line script, our work for today is done. If you need anything we’ll be at home watching the Jerry Springer Show.


How Can I Find All the Users with Remote Access Permissions?

Hey, Scripting Guy! How can I find all the users with remote access permissions; that is, all those with the Dial-In or VPN property set to Allow?

-- RC

Hey, RC. You know, it seems like people are always looking for something: true love, happiness, the meaning of life. We can’t help you with any of those. (We thought we could with the meaning of life, but it turns out we were wrong.) But that’s OK; after all, we can help you find all the users with remote access permissions:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND msNPAllowDialin = TRUE"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

One piece of advice we always give people is this: any time you’re looking for something, a good place to start your search is in Active Directory. This script is a good example of that. You’re looking for users who have the Allow access option selected on the Dial-in tab in Active Directory:

As it turns out, the Remote Access Permission (Dial-in or VPN) option equates to the msNPAllowDialin attribute in Active Directory. If access is allowed, msNPAllowDialin will be True. If access is denied, then msNPAllowDialin will be False. And if msNPAllowDialin has no value, then that means the Control access through Remote Access Policy option has been selected.

In other words, to find all the users who have remote access all we need to do is search for user accounts where the msNPAllowDialin attribute is True. We won’t undertake a lengthy explanation of the code for searching Active Directory; that’s covered in detail in our two-part Tales from the Script series Dude, Where’s My Printer? We will, however, show you the query that retrieves those users:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND msNPAllowDialin = TRUE"

As you can see, we’re looking for all the Active Directory objects where the objectCategory is equal to user (in other words, we’re looking for user accounts as opposed to group accounts or computer accounts) and where the msNPAllowDialin attribute is True. If we wanted to find all the users who have been denied access we would search for users where the value msNPAllowDialin was equal to False:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND msNPAllowDialin = FALSE"

And what about those users who have remote access determined via policy? In that case we can search for users who don’t have a value configured for msNPAllowDialin:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND msNPAllowDialin <> '*'"

Yes, it looks a little crazy, but in ADSI the asterisk is a wildcard representing anything. In this query, we’re asking for a list of users where the msNPAllowDialin property does not equal anything. In other words, show us all the users who have no value for the msNPAllowDialin attribute; users with any other value (True or False) will be excluded. In turn, the users returned by this query will be the users who have remote access determined via policy. (This, by the way, is the default setting. If you don’t specify otherwise, users will have remote access permissions determined by policy.)

Whew; got all that? After returning a recordset of users who meet the criteria, we simply employ these lines of code to cycle through the list, echoing back the name of each user:

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

Like we said, finding a list of users who have remote access permissions isn’t quite the same thing as finding the meaning of life. But we like to believe that it’s the next best thing.


How Can I Force All the Users in an OU to Change Their Password the Next Time They Log On?

Hey, Scripting Guy! How can I write a script that will force all the users in an OU to change their password the next time they log on?

-- TH

Hey, TH. You know, you picked a bad day to ask a question; the Scripting Guy who writes this column is in an even worse mood than usual (difficult as that might be to believe). A few weeks ago the Scripting Guys decided, what with the weather getting a bit better and the food in the Microsoft cafeteria getting a bit worse, that once a week they would – keep this under your hat, OK? – actually leave the campus at lunch time and go eat at a real restaurant. With that in mind, they bribed the guards, stole some uniforms from the Microsoft laundry, and dug a tunnel leading from their building across the street and into freedom.

Note. Wouldn’t it be easier to just walk out the door and go to lunch at lunchtime? Oh, sure, now you tell us.

And now that you mention it, Microsoft doesn’t actually have guards. Wonder who that was we bribed?

At any rate, today is supposed to be the day we go eat a real lunch. However, the Scripting Guys manager has called a team meeting right at the very moment the Scripting Guys had planned to enter the tunnel and make their escape. (It’s important to go at just the right moment, when the searchlights are pointed in the opposite direction and the dogs are patrolling on the other side of the campus.) That means we’re exchanging a real hamburger and fries for an update on team activities. Hurray!

Note. Aren’t team meetings an important way to get information, to foster morale, and to learn about ways to help us all do our jobs better? Hey, that’s pretty funny; it’s nice to see that someone has a sense of humor about all this.

At any rate, the Scripting Guy who writes this column is in a bad mood. Originally he was going to register his displeasure by refusing to answer TH’s question. However, that didn’t seem right; after all, TH isn’t the one who scheduled the team meeting for today. (Um, you’re not the one who scheduled the meeting for today, are you, TH?) Consequently, the Scripting Guy who writes this column opted for Plan B: he would go on a hunger strike in order to protest having a team meeting today. However, that was before he discovered that you’re not allowed to eat while on a hunger strike. (How crazy is that?) He then briefly considered going to Plan C, but coming up with yet another plan seemed like way too much work. Therefore, he decided to just answer TH’s question and be done with it:

Set objOU = GetObject("LDAP://ou=Accounting,dc=fabrikam,dc=com")
objOU.Filter = Array("user")

For Each objUser in objOU
    objUser.pwdLastSet = 0
    objUser.SetInfo
Next

As you can see, this is a very simple little script. (In our defense, we said we’d answer the question; we didn’t say we’d put an enormous amount of work into coming up with that answer.) To begin with, we connect to the desired OU in Active Directory; in our case, that happens to be the Accounting OU in the fabrikam.com domain:

Set objOU = GetObject("LDAP://ou=Accounting,dc=fabrikam,dc=com")

We actually get a lot of questions along these lines: how do I do something to all the user accounts/computer accounts/group accounts in an OU. People assume that this must be a very complicated task, one that requires both incredible coding skill and an intimate knowledge of Active Directory. Interestingly enough, this is actually about as easy a task as you’ll ever do. When you bind to an OU in Active Directory you, by default, get back a collection of each and every item stored in that container. If we wanted to echo back the name of each of these items all we’d have to do is set up a For Each loop and have at it:

For Each objItem in objOU
    Wscript.Echo objItem.Name
Next

Of course, we aren’t interested in each and every item found in the Accounting OU; we’re only interested in user accounts. Hence the second line of code in our script:

objOU.Filter = Array("user")

What we’re doing here is applying a filter to our collection; in particular, we’re saying that we want to filter out all the objects in the collection except for user objects. What if we were interested in working with computer accounts instead? Then we’d use this filter:

objOU.Filter = Array("computer")

Pretty simple, huh? Note that you must always use an array when assigning object types to a filter, even if you only have one object in the filter. And if that makes you wonder, “Hmmm, does that mean we could put multiple objects in a single filter?” the answer is: you bet you can. For example, here’s a filter that weeds out everything except group accounts and printers (printQueue objects):

objOU.Filter = Array("group", "printQueue")

Just separate the objects with commas and you can include as many items in the filter as your heart desires.

As soon as our collection consists solely of user accounts we can go ahead and set up a For Each loop to cycle through entire set of items. Inside the loop we (automatically) bind to the first user account and then execute the following two lines of code:

objUser.pwdLastSet = 0
objUser.SetInfo

In line 1 we’re assigning the value 0 to the pwdLastSet attribute. This attribute tells us when the user last set his or her password. If the value of pwdLastSet is 0 then the user must change his or her password the next time they log on. If the user is already logged on they’ll be fine; they can continue working away without any problem. However, once they log off and then try to log back on they’ll be forced to change their password.

Note. Don’t get too excited about the pwdLastSet attribute; it really does contain the date and time the password was last changed, but the date is stored – and we are not making this up – as a large integer that “represents the number of 100 nanosecond intervals since January 1, 1601 (UTC).” If you’d like to know the date and time a password was last changed take a peek at this Hey, Scripting Guy! column.

After setting the pwdLastSet attribute to 0 we then call the SetInfo method to write the change to the actual user account in Active Directory. We then loop around and repeat the process with the next user in the collection. When we’re all done, each and every user in the Accounting OU will be required to change their password the next time they log on.

Note. OK, that might not be entirely true: this approach works only if the user has a password that expires. If any of your users have non-expiring passwords (which, we might add, is definitely not recommended) then setting pwdLastSet to 0 will have no effect on their account. But don’t worry: we just happen to have yet another Hey, Scripting Guy! column that shows you how to identify non-expiring passwords, and even how to turn them into expiring passwords.

Let’s face it: after 3+ years we have a lot of Hey, Scripting Guy! columns.

In case you’re wondering, the food at the Microsoft cafeteria really isn’t all that bad; in fact, the only problem we have with it is that they have the philosophy, “If you liked what we served you on Monday you’ll like it even better on Tuesday. And by Friday, you’ll love it!” Nevertheless, having yet another gyro sandwich is still probably better than crawling a mile or so through a dank and dark tunnel.

Probably.

P.S. For once this column has a happy ending: the team meeting ran extremely short, and the Scripting Guys managed to go out to lunch after all!


How Can I Get a List of All the Disabled User Accounts in Active Directory?

Hey, Scripting Guy! How can I get a list of all the disabled user accounts in Active Directory?

-- RT

Hey, RT. Now, just remember, you asked for this. We have a script that returns a list of disabled user accounts in Active Directory; the only problem is that part of the script is a little cryptic (to say the least), and we won’t be able to fully explain how it all works in this column. But if you’re fine with that then read on.

Oh, what the heck: you might as well read on anyway. After all, you never can tell what you might find in one of these columns.

The problem we have here is that account status (enabled or disabled) is part of the userAccountControl attribute. This happens to be an example of a bitmask attribute: a single attribute that actually houses numerous property values. In fact, all of the following property values are stored in this single attribute:

· The user account is disabled.

· The account is currently locked out.

· No password is required.

· The user cannot change the password.

· This is a default account type that represents a typical user.

· When set, the password will not expire on this account.

· When set, this flag will force the user to log on using a smart card.

· The user password has expired.

Bitmask attributes can be a bit confusing, but, for the most part, they aren’t too hard to work with. The one exception occurs when you need to search Active Directory, which is exactly what we need to do here. Typically when you search Active Directory you use a SQL query similar to this:

Select Name from 'LDAP://dc=fabrikam,dc=com' Where Department = 'Finance'

That works fine for most Active Directory attributes; it doesn’t work so fine – in fact, it doesn’t work at all – for bitmask attributes. Therefore we have to rely on Plan B, and use the LDAP query syntax instead:

<LDAP://dc=fabrikam,dc=com>;(&(objectCategory=User)" & _
        "(userAccountControl:1.2.840.113556.1.4.803:=2));Name;Subtree

Yes, we know; we don’t like it any better than you do. But, really, after you know what the individual parts represent this isn’t as bad as it first looks:

· <LDAP://dc=fabrikam,dc=com>. This is just simply the starting point for our search: the root of the fabrikam.com domain. Other than the angle brackets that surround the ADsPath this should be pretty familiar to you.

· (&(objectCategory=User). This is part of our “Where” clause (note that we don’t actually use the word Where anywhere in the query). The objectCategory=User portion should be fairly straightforward; we’re interested only in user objects. The & is equivalent to the AND operator in a SQL clause: it just means we’re combining objectCategory=User with something else.

· (userAccountControl:1.2.840.113556.1.4.803:=2)). And this just happens to be that something else. It might look like gibberish, but this actually tells our script to search for objects (in this case, users) where bit 2 in the userAccountControl attribute has been enabled. We won’t spend any time discussing bitmask attributes here; for a brief discussion see the Reading User Account Password Attributes section of the Microsoft Windows 2000 Scripting Guide. For now all we have to know is that if bit 2 is enabled then the user account is disabled.

· So what about the 1.2.840.1113556.1.4.803? That happens to be the LDAP bit matching rule and is equivalent to the Boolean AND operator (we know, we know). In other words, this crazy-looking concoction is basically equal to this:

If objUser.userAccountControl AND 2 Then

If you’re familiar with bitmasks this might make some sense to you. If not, well, don’t worry too much about it. Go ahead and use the script as-is and save the understanding for later.

· Name. This is the just attribute we want returned.

· Subtree. This is our search scope; it simply means we want to search the entire Active Directory tree.

Clear as the proverbial mud, right? You might take a look at the April 2005 Tales from the Script column, which offers an introduction to searching Active Directory. (And stay tuned for Part 2, due in May 2005).

Oh, right: you might also want to take a look at the completed script that returns a list of all the disabled user accounts in Active Directory. Well, why didn’t you say so?

On Error Resume Next

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000

objCommand.CommandText = _
    "<LDAP://dc=fabrikam,dc=com>;(&(objectCategory=User)" & _
        "(userAccountControl:1.2.840.113556.1.4.803:=2));Name;Subtree"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop


How Can I Get a List of All the Users in an OU and Its Sub-OUs?

Hey, Scripting Guy! How can I get a list of all the users in an OU and its sub-OUs?

-- MN

Hey, MN. Thanks to either faulty wiring or a neighbor’s fireworks (the investigation was inconclusive), one of the Scripting Guys had the delightful experience of watching his garage burn down this summer. Because the Scripting Cars were never parked in the garage, this Scripting Guy decided to rebuild the thing as a family room. That’s fine, and, in general, the contractors did a pretty good job. Of course, as a garage, the room originally had a pair of electrical outlets in the ceiling in order to plug in garage door openers. Upon rebuilding the garage as a family room, the contractor left those two electrical outlets exposed, in order to plug in … well, whatever kind of device you normally plug into a ceiling. (Best of all, neither outlet is near a wall. Instead, both are out towards the middle of the room, the very place you want to see electrical cords hanging.)

On the one hand, this Scripting Guy can’t help but look at those two electrical outlets and think, “How could you miss something so obvious?” On the other hand, that same Scripting Guy can look back over 15 months of Hey, Scripting Guy! columns and notice that we’ve never answered this particular question. Good heavens, how could we miss something that obvious?!? Realizing that we’ve never answered this often-asked questions makes it a bit harder to yell at the contractor for not covering over those electrical outlets.

Barring more faulty wiring (or another poorly-aimed firework), our Scripting Guy is stuck with a pair of electrical outlets in his family room ceiling forever and ever. But at least readers of this column no longer have to wonder how to get a list of all the users in an OU and its sub-OUs:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name FROM
'LDAP://ou=finance,dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Wscript.Echo objRecordSet.Fields("Name").Value
objRecordSet.MoveNext
Loop

As you can see, this is an Active Directory search script. We like using Active Directory search scripts when answering these questions, for two reasons. One, these search scripts are extremely powerful, yet relatively simple to write. And two, they’re just complicated enough that we can’t explain all the ins and outs in this column; instead, if you’re unfamiliar with scripts for searching Active Directory we always encourage you to read our two-part Tales from the Script series Dude, Where’s My Printer?

Note. Why is that a good thing? Well, to be honest, we’re lazy, and this way we don’t have to write very much. But if anyone asks, it’s a good thing because the two-part series does a much better job of discussing Active Directory search scripts than we could possibly do in a daily column. Which is actually true.

The whole secret to searching just an OU and its sub-OUs is picking a starting point. Typically when searching Active Directory you start the search in the root and work your way down through the directory tree:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

Note as well that we also specify the objectCategory as being equal to user. That ensures that we get back only user accounts and not computer accounts, group accounts, or any other objects found in Active Directory.

But what if we want to search only the Finance OU and its sub-OUs? In that case, we simply start the search in the Finance OU:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://ou=finance,dc=fabrikam,dc=com' WHERE objectCategory='user'"

It’s that simple. Suppose the Finance OU has a sub-OU named Accounting and we wanted to search just the Accounting OU and its sub-OUs? Okey-doke:

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://ou=accounting,ou=finance,dc=fabrikam,dc=com' " & _
        "WHERE objectCategory='user'"

Incidentally, the reason we search the OU and its sub-OUs is because we defined a constant named ADS_SCOPE_SUBTREE and set the value to 2. We then used that constant when defining the search scope:

objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

When the Searchscope property is set to 2, a search will be conducted in a container and all its sub-containers. That’s how we can search all of Active Directory: we simply start in the root, and with Searchscope set to 2 we’ll search not only the root but all its sub-containers (and all their sub-containers). That, of course, will amount to the entire domain. If we wanted to search just an OU (ignoring its sub-OUs) then we would set Searchscope to 1.

Well, that was easy, wasn’t it? Now if we could just figure out what to do with those electrical outlets in the ceiling all our problems would be solved.


How Can I Get a List of All the Users Who Have an Alternate Recipient?

Hey, Scripting Guy! How can I get a list of all the users who have an alternate recipient (altRecipient)?

-- SA

Hey, SA. You know, one of the great things about being a Scripting Guy is that we don’t ever have to know what we’re talking about; all we do have to know is how to write a script that does whatever it is you ask us to do and everyone’s happy. Case in point: retrieving a list of all the users who have an alternate recipient. To be honest, we have only the vaguest notion of what an alternate recipient is, believing that this represents an alternate location for forwarding Microsoft Exchange email addresses. That’s all we know, and, like we said, we aren’t even sure that’s correct. But that’s OK; after all, everyone will forgive us our ignorance as long as we can show you a script that will return a list of all the users who have an alternate recipient:

On
Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name, altRecipient FROM
'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
"AND altRecipient='*'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
Wscript.Echo objRecordSet.Fields("Name").Value,
objRecordSet.Fields("altRecipient").Value
objRecordSet.MoveNext
Loop

As you can see, this script simply searches Active Directory, ferreting out all the users who have an alternate recipient. As usual, we won’t discuss the ins and outs of searching Active Directory; that goes beyond what we can do in this column. But don’t worry: we would never leave you hanging like that, wondering how an Active Directory search script works. If you need (or want) more information on the principles behind Active Directory search scripts, take a look at our two-part Tales from the Script series Dude: Where’s My Printer?

No, thank you. After all, you guys let us get away with writing a column that doesn’t actually explain anything but instead just refers you to some other column if you actually want to know what’s going on! (Editor’s Note: This statement implies that you have a choice in the matter. In case you were wondering – you don’t. And yes, in case you were wondering about that too, the Scripting Editor has had a long day today.)

What we’ll focus on today is the actual query that returns the list of users who have an alternate recipient. As SA noted, the Active Directory attribute name for an alternate recipient is altRecipient; to get a list of users who have an alternate recipient what we need to do is search for all the user accounts where the altRecipient attribute is set to something. That’s exactly what this query does:

objCommand.CommandText = _    "SELECT Name, altRecipient FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _        "AND altRecipient='*'"

What we’re doing here is retrieving two attribute values – Name and altRecipient – from the fabrikam.com domain. More importantly, we’re retrieving those values only for Active Directory objects that meet both of the following criteria:

· The objectCategory is equal to user. This ensures that we get back only user accounts that have an alternate recipient. Could other objects even have alternate recipients? Well, we’re not sure, so we decided not to take any chances and eliminate the possibility of getting back anything but user accounts.

· The value of altRecipient is equal to something. To indicate this we use wildcard syntax: altRecipient=’*’. As you might have guessed, when searching Active Directory the asterisk represents any character or set or characters. By using an asterisk all by itself we’re asking the script to return all the user accounts where altRecipient is equal to something; we don’t care what the value is, just as long as it’s something.

You know, that’s a good question. If the asterisk means “Show us all the user accounts where altRecipient is equal to something,” doesn’t that mean we’ll get back all the user accounts, even those that don’t have an alternate recipient? As the Scripting Guys have told you many times before, just because something is blank that doesn’t mean that it doesn’t have a value.

Well, to tell you the truth, we tell people a lot of things; you can’t expect us to keep track of everything we say, can you? But you’re right: if a user has a blank altRecipient attribute value then he or she will show up in the search. Hopefully, though, you don’t have any users with blank altRecipient attributes; instead, users who do not have an alternate recipient should have a Null altRecipient value (which happens to be the default value). Unlike a blank space or an empty string (“”), a Null value truly means no value whatsoever; users with a Null altRecipient value will not show up in our search results.

Note. No, we won’t leave you hanging here, either. For more information on setting Active Directory attribute values to Null take a look at this much-beloved Hey, Scripting Guy! column.

Good point: all Hey, Scripting Guy! columns are much-beloved, aren’t they?

After executing our query we get back a recordset consisting of all the users who have an alternate recipient. All we have to do then is set up a Do Until loop that loops through that collection, echoing back the values of the Name and altRecipient attributes:

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value, objRecordSet.Fields("altRecipient").Value
    objRecordSet.MoveNext
Loop

It’s that simple. (Just imagine all the cool things we Scripting Guys could show you if we actually knew what we were doing!)


How Can I Get a List of All the Users Whose Passwords Never Expire?

Hey, Scripting Guy! How can I get a list of all the users whose passwords never expire?

-- NW

Hey, NW. As you probably know, Internet gambling is illegal in the USA. That’s too bad, because if it wasn’t we’d be willing to bet $10 that the answer to your question will be this: search Active Directory.

And as much as we hate to say “We told you so,” well, guess what:

On Error Resume Next

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000

objCommand.CommandText = _
    "<LDAP://dc=fabrikam,dc=com>;" & _
        "(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=65536));" & _
            "Name;Subtree"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

You’re right: we should have bet way more than $10. What were we thinking?

Actually, it’s just as well that we didn’t bet on this. The truth is, we cheated: we had some inside information. After all, any time you’re looking for Active Directory-related stuff the answer is going to be the same: search Active Directory. We knew the answer long before you even asked the question.

What we didn’t know, at least in this case, was how to actually conduct our search. We won’t talk too much about the details of writing scripts that search Active Directory; that background information can be found in our two-part Tales from the Script article Dude, Where’s My Printer? However, we will take a minute of two to talk about the query we ended up using in order to conduct the search, a query that might look a bit different from the Active Directory queries you’re used to working with:

objCommand.CommandText = _
    "<LDAP://dc=fabrikam,dc=com>;" & _
        "(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=65536));" & _
            "Name;Subtree"

If you’re thinking, “Whoa, that doesn’t look like a SQL query to me,” well, there’s a good reason for that: this isn’t a SQL query. Instead, this is an example of the LDAP query syntax, a query syntax that can retrieve the same information as a SQL query, albeit in a much more cryptic fashion.

But if that’s the case, then why did we decide to use this weird-looking syntax rather than the more familiar (and more comfortable) SQL query syntax? Well, as it turns out, the property that determines whether or not a password expires (ADS_UF_DONT_EXPIRE_PASSWD) is not a “stand-alone” property; that is, you can’t get at the value using code similar to this:

Wscript.Echo objUser.ADS_UF_DONT_EXPIRE_PASSWD

Instead, this value is one of several “flags” found in the userAccountControl attribute. The userAccountControl attribute is an example of a bitmask attribute, a single attribute that houses multiple property values; this single attribute tells you whether or not a password will expire, whether or not a password has expired, whether or not an account is disabled, etc. In this case, if the flag with the value 65536 is switched “on,” then the password never expires; if the flag is switched off, then the password does expire.

Note. Yes, we know: many of you have no idea what we just said. For a brief introduction to bitmasks you might take a look at this section of the Microsoft Windows 2000 Scripting Guide.

So what difference does that make to us? Well, it makes a big difference: you can’t (or at least can’t easily) use a SQL query to search for a single value inside a bitmask attribute. You can do this using the LDAP query syntax, however, so that’s what we did. Like the cars driven by a couple of the Scripting Guys, it’s a bit ugly, but it works just fine. (Sorry, guys, but the truth hurts.)

As for the query itself, we’ll briefly explain what the individual pieces do, to give you some idea of how the whole thing works. Thus:

Item | Description
<LDAP://dc=fabrikam,dc=com> | The starting point for the search. We want to search all of Active Directory, so that means starting in the root (i.e., fabrikam.com).
& | Equivalent to the AND operator in a SQL query. We need this because we’re searching for users and we’re searching for a specific value in the userAccountControl attribute. Both of these criteria must be met for an object to be returned.
(objectCategory=User) | Limits the returned data to user accounts.
(userAccountControl:1.2.840.113556.1.4.803:=65536) | Indicates that we want to return only those accounts where the userAccounControl flag for 65536 is switched on; that equates to user accounts where the password doesn’t expire. We’ll explain this werid-looking block of code in a little more detail down below.
Name | The Active Directory attributes we want reported back. We’re asking to get back only a single attribute: Name. To report back additional attributes just tack them on to the end of Name, separating each one using commas: Name,cn,AdsPath
Subtree | Indicates the type of search. Specifying Subtree causes the script to search all the OUs and containers found in the root of fabrikam.com. Because all the OUs and containers have to be found in the root this causes the script to search all of Active Directory.

As odd as this might look, most of it actually makes sense: once you know what the individual parts represent, it isn’t too hard to read (or even to modify) an LDAP query. Well, OK, with one exception:

(userAccountControl:1.2.840.113556.1.4.803:=65536)

Yes, we know how weird it looks and, yes, we wish some of the werindess had been hidden from view. But, setting looks aside, this is really pretty simple stuff. The 1.2.840.113556.1.4.803 is nothing more than an example of an LDAP matching rule object identifier (OID); in turn, this is just a somewhat-clunky way of saying: “Show me all the objects where the userAccountControl flag with the value 65536 is on.” That’s really all there is to it. That also means that you can search for other attribute values found in userAccountControl simply by substituting the desired value for 65536. Need a list of all the disabled accounts?

Here you go:

(userAccountControl:1.2.840.113556.1.4.803:=2)

Cool.

And yes, we know your next question: can you modify our original query to find all the users who don’t have non-expiring passwords? Sure, although the syntax is, again, a bit odd-looking. Without bothering to explain it all, here’s modified code that searches for user accounts where the password does expire:

(!(userAccountControl:1.2.840.113556.1.4.803:=65536))

In this case, the ! indicates “does not equal.” Bet you 10 bucks that you didn’t know that, did you?

Oh. Well, OK then, just let us know where the send the check. This is not going to look good on our expense account. (Or at least it wouldn’t if any of us actually had an expense account.)


How Can I Get a List of Exchange Servers Assigned to My Users?

Hey, Scripting Guy! How do I get a list of the Exchange servers (msExchHomeServerName) assigned to the users in an OU?

-- BG

Hey, BG. As you probably already figured out, you have two issues: you need to get a list of all the users in an OU, and then you need to figure out which Exchange server they’ve been assigned. Believe it or not, this is probably one of the easiest scripts you’ll ever have to write.

Well, OK, that we’ll ever have to write for you ….

Let’s start by getting a list of all the users in an OU. There really isn’t much to say about that; all you have to do is bind to the OU and you automatically get back a collection of all the objects in that OU. Thus if we want to get a collection of all the objects found in the Finance OU (and echo back the object’s common name, or CN), we can use this code:

Set objOU = GetObject("LDAP://OU=Finance,DC=fabrikam,DC=com")
objOU.Filter = Array("user")

For Each objItem in objOU
    Wscript.Echo objItem.CN
Next

Notice the one little “trick” we did here. We set a filter on the collection using this line of code:

objOU.Filter = Array("user")

This tells our script, hey, filter out everything in the collection except user accounts. Why do we do that? Well, suppose you have computer accounts or print queues or other objects in the OU. We don’t want to try to determine the Exchange server for a bunch of print queues, so we use the filter to weed out everything except user accounts.

And now comes the “hard” part: retrieving each user’s Exchange server. Of course, that’s hard only if you don’t know that the name of the attribute. But seeing as how you are told us the attribute you’re looking for is msExchHomeServerName, well:

Set objOU = GetObject("LDAP://OU=Finance,DC=fabrikam,DC=com")
objOU.Filter = Array("user")

For Each objItem in objOU
    Wscript.Echo objItem.CN
    Wscript.Echo objItem.msExchHomeServerName
Next

And just for the heck of it, here are a few more Exchange-related properties you might want to grab while you’re at it:

Set objOU = GetObject("LDAP://OU=Finance,DC=fabrikam,DC=com")
objOU.Filter = Array("user")

For Each objItem in objOU
    Wscript.Echo objItem.CN
    Wscript.Echo objItem.mailNickname
    Wscript.Echo objItem.msExchHomeServerName
    Wscript.Echo objItem.homeMDB
    Wscript.Echo objItem.mdbUseDefaults
Next


How Can I Get a List of User Accounts Created On Or Between Two Dates?

Hey, Scripting Guy! How can I query Active Directory for a list of all the accounts that were created on or between two dates?

-- PG

Hey, PG. You know, August is a propitious month for the Scripting Guys and this column: after all, today’s edition marks the beginning of the fourth year of Hey, Scripting Guy! Our very first column debuted on August 2, 2004, and – other than an occasional holiday or vacation day – we’ve run a new column every weekday ever since. How many columns does that add up to? Well, you do the math.

No, seriously: you’ll have to do the math. We kind of lost track of things after the 500th column appeared last year.

Of course, that brings up an obvious question. “Gosh, after writing all those columns you must have some pretty interesting stories to tell, don’t you?” And you’d think so, wouldn’t you? As it turns out, however, we don’t have any stories to tell. Instead, all we have is a script that lists all the Active Directory user accounts that were created on or between two dates:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

dtmCreationDate1 = "20070701000000.0Z"
dtmCreationDate2 = "20070731000000.0Z"

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name, whenCreated FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectClass='user' "  & _
        "AND whenCreated>='" & dtmCreationDate1 & "' AND whenCreated<='" & dtmCreationDate2 & "'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value, objRecordSet.Fields("whenCreated").Value
    objRecordSet.MoveNext
Loop

We know that many of you are concerned that, after four years, Hey, Scripting Guy! might begin to stray from its original intention (which is entirely possible considering the fact that none of us can remember what its original intention even was). But take it from the Scripting Guys: you have nothing to worry about there. If you need proof, try this: as we’ve consistently done for the last 3 years, today’s column on searching Active Directory includes the following disclaimer:

We won’t discuss the ins and outs of searching Active Directory; that goes beyond what we can do in this column. But don’t worry: we would never leave you hanging like that, wondering how an Active Directory search script works. If you need (or want) more information on the principles behind Active Directory search scripts, take a look at our two-part Tales from the Script series Dude: Where’s My Printer?

See? We’re every bit as consistent – and lazy – as we’ve always been.

What we will talk about today is the SQL query we use to retrieve a collection of all the user accounts that were created between two dates (in this case, July 1, 2007 and July 31, 2007). Before we do that, however, let’s take a quick peek at the following two lines of code:

dtmCreationDate1 = "20070701000000.0Z"
dtmCreationDate2 = "20070731000000.0Z"

Believe it or not, these two lines of code assign our start date (July 1, 2007) and our end date (July 31, 2007) to the variables dtmCreationDate1 and dtmCreationDate2. Granted, these don’t really look like dates; that’s because of the date-time format (“generalized time”) required when doing an Active Directory search. Trust us, however; the value 20070701000000.0Z can be broken down like this:

· The 2007 represents the year.

· The 07 represents the month (July), in two-digit format.

· The 01 represents the day (the first), also in two-digit format.

· The 000000 represents, respectively, the hour, minutes, and seconds, all in two-digit, 24-hour format. (For example, 8:35 AM would be coded as 083500.)

· The .0Z represents the time differential between local time and Greenwich Mean Time. Because Active Directory stores date-time values using Greenwich Mean Time, we’ve left this offset at 0. If we wanted to specify an offset 8 hours later than Greenwich Mean Time we’d do something like this: .0+0800. Eight hours earlier than Greenwich Mean Time would result in this: .0-0800 (note the minus sign). Etc.

Now that we know what our two variables are for we can turn our attention to the SQL query that returns all the user accounts created on or between July 1, 2007 and July 31, 2007:

objCommand.CommandText = _
    "SELECT Name, whenCreated FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectClass='user' "  & _
        "AND whenCreated>='" & dtmCreationDate1 & "' AND whenCreated<='" & dtmCreationDate2 & "'"
Set objRecordSet = objCommand.Execute

What are we doing here? What we’re doing is requesting two attribute values (Name and whenCreated) from the fabrikam.com domain. Of course, we don’t want this information for all the objects in the fabrikam.com domain; instead, we only want those objects that meet the following criteria:

· The objectClass is equal to “user”. What if we wanted a list of computer accounts created during the month of July? No problem; just change “user” to “computer”. What if we wanted a list of all the accounts (regardless of objectClass) created during the month of July? Again, no problem; in that case, just leave out the objectClass= clause altogether.

· The value of the object’s whenCreated attribute must be greater than or equal to July 1, 2007 (represented by the variable dtmCreationDate1).

· The value of the object’s whenCreated attribute must be less than or equal to July 31, 2007 (represented by the variable dtmCreationDate2).

If you follow the logic (and, yes, this might be the first time in four years that one of these columns has actually had some logic) you’ll see that this limits the returned data to user accounts created during July, 2007. Which is exactly what we wanted the returned data to be limited to.

In case you don’t trust us, we added a Do Until loop that runs until we reach the end of our recordset (that is, the end of the returned data). Inside that loop we echo back the value of the first object’s Name and whenCreated attributes, then use the MoveNext method to move on and repeat the process with the next item in the recordset:

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value, objRecordSet.Fields("whenCreated").Value
    objRecordSet.MoveNext
Loop

The net result? Something very similar to this:

Ken Myer 7/31/2007 6:38:40 PM
Pilar Ackerman 7/29/2007 7:22:06 PM
Jonathan Haas 7/26/2007 7:28:42 PM
Gail Erickson 7/24/2007 7:45:14 PM
Carol Phillips 7/17/2007 9:55:16 PM
Kim Abercrombie 7/14/2007 6:11:31 PM
Dylan Miller 7/06/2007 3 :29:44 PM

Note. That’s a good point: even though we had to use that crazy generalized time format to specify values for the whenCreated attribute we didn’t have to do anything to get real date-time values (e.g., 7/26/2007 7:28:42 PM) displayed on screen. As it turns out, we only have to use the generalized time format when querying Active Directory; when we echo back the value of the whenCreated attribute the value is automatically converted into something a little easier to read.

And there you have it, PG: all the user accounts created on or between July 1, 2007 and July 31, 2007.

Before we go, we’d like to add that we are reasonably proud of Hey, Scripting Guy! and its longevity; we daresay that no other daily column published on TechNet has been in business as long as we have. (Due, in large part, to the fact that no one but the Scripting Guys are dumb enough to publish a daily column on TechNet.) So then why aren’t we holding a birthday party in honor of the occasion? That’s easy: we had a big celebration a year ago when the historic 500th column was published. For the Scripting Guys, one party every four years or so is about all we can handle.

Note. Well, except for the Scripting Editor, of course, who parties pretty much non-stop; she doesn’t even know the meaning of the word “quit.” (Which is too bad, because we’ve been trying to get her to quit for almost 3 years now, and with no such luck.)


How Can I Get the GUID for a User Account If All I Have is the User’s Logon Name and Domain?

Hey, Scripting Guy! How can I get the GUID for a user account if all I know is the user’s logon name and domain?

-- IU

Hey, IU. You know, one of the great things about the Scripting Guy who writes this column is – well, OK, right off the top of our head we can’t think of any great things about the Scripting Guy who writes this column. (Although, no doubt the Scripting Editor will chime in with a list of great things. Scripting Editor, you there?)

Nevertheless, the Scripting Guy who writes this column is pretty good at looking past outward appearances and getting right to the heart of a problem (admittedly, that’s due in large part to the fact that he typically is the heart of a problem). When you get right down to it, IU, you have a translation problem: you’re sitting there with a Windows NT-style domain name (e.g., fabrikam\kenmyer), and you need to translate that to a user GUID (e.g., {97614339-087d-4f26-9fc5-86e8e7425ce7}. So here’s a question for you: what do you need any time you have to translate something? That’s exactly right: you need a translator.

Note. You know, it’s amazing insights such as that one – in order to translate something you need a translator – that make this column what it is. It is hard to believe that we give this stuff away for free, isn’t it?

Of course, there’s just one problem here: there’s simply now way to translate an NT 4 domain name to a GUID. No way at all.

Or is there? Take a peek at this:

Const ADS_NAME_INITTYPE_GC = 3
Const ADS_NAME_TYPE_NT4 = 3
Const ADS_NAME_TYPE_GUID = 7

strUserName = "fabrikam\kenmyer"

Set objTranslator
= CreateObject("NameTranslate")

objTranslator.Init ADS_NAME_INITTYPE_GC, ""
objTranslator.Set ADS_NAME_TYPE_NT4, strUserName

strUserGUID = objTranslator.Get(ADS_NAME_TYPE_GUID)

Wscript.Echo strUserGUID

We have to admit that this was actually kind of a fun little script to write; that’s because it gave us a chance to use the IADsNameTranslate utility, an ADSI interface that’s built right into the operating system. As the name implies, the IADsNameTranslate interface can translate many of the different name types used in Active Directory. For example, give IADsNameTranslate a distinguished name and it can give you back a display name. Give IADsNameTranslate a canonical name and it can give you back a UPN name. Give IADsNameTranslate a Windows NT 4 domain name and it can give you back a GUID.

Or so we hope, anyway. Let’s find out.

Our script begins by defining three constants:

· ADS_NAME_INITTYPE_GC (with a value of 3) tells the script that we want to look up name information by contacting a global catalog server.

· ADS_NAME_TYPE_NT4 (also with a value of 3) is used to specify an NT 4 domain name.

· ADS_NAME_TYPE_GUID (with a value of 7) is used to specify an Active Directory GUID.

After defining the constants we next assign the user name to a variable named strUserName, taking care to use the NT 4 domain naming convention domain\user name:

strUserName = "fabrikam\kenmyer"

Now we’re ready to roll. For starters we create an instance of the NameTranslate object, the ProgID for the IADsNameTranslate interface. We then call the Init method to bind us to Active Directory; in this case, Init binds us to a global catalog server because we passed the constant ADS_NAME_INITTYPE_GC as the method parameter. (Note, too, that we also passed an empty string as the second parameter. This second parameter gives you an opportunity to specify a computer or domain name. We don’t need to do that when we contact a global catalog server, but we do need to pass an empty string as this “optional” parameter. If we don’t, the script will fail.)

Incidentally, here’s what our call to the Init method looks like:

objTranslator.Init ADS_NAME_INITTYPE_GC, ""

And here’s what our call to the Set method looks like:

objTranslator.Set ADS_NAME_TYPE_NT4, strUserName

As you can see, we pass Set two parameters: the type of Active Directory name to be translated (represented by the constant ADS_NAME_TYPE_NT4) and the name itself (represented by the variable strUserName). After calling the Set method we then call the Get method. And what is it we want to get? That’s easy: we want to get the user’s GUID, something we indicate by passing the constant ADS_NAME_TYPE_GUID. The NameTranslate object will grab the GUID and stash it in a variable named strUserGUID:

strUserGUID = objTranslator.Get(ADS_NAME_TYPE_GUID)

So is that really going to work? Of course it is; here’s what we see when we echo back the value of strUserGUID:

{97614339-087d-4f26-9fc5-86e8e7425ce7}

Pretty cool, huh?

Like we said, there are other name formats you can work with as well, including the following:

Constant | Value | Description
ADS_NAME_TYPE_1779 | 1 | Name format as specified in RFC 1779. For example, "CN=Jeff Smith,CN=users,DC=Fabrikam,DC=com".
ADS_NAME_TYPE_CANONICAL | 2 | Canonical name format. For example, "Fabrikam.com/Users/Jeff Smith".
ADS_NAME_TYPE_NT4 | 3 | Account name format used in Windows NT 4.0. For example, "Fabrikam\JeffSmith".
ADS_NAME_TYPE_DISPLAY | 4 | Display name format. For example, "Jeff Smith".
ADS_NAME_TYPE_DOMAIN_SIMPLE | 5 | Simple domain name format. For example, "JeffSmith@Fabrikam.com".
ADS_NAME_TYPE_ENTERPRISE_SIMPLE | 6 | Simple enterprise name format. For example, "JeffSmith@Fabrikam.com".
ADS_NAME_TYPE_GUID | 7 | Global Unique Identifier format. For example, "{95ee9fff-3436-11d1-b2b0-d15ae3ac8436}".
ADS_NAME_TYPE_USER_PRINCIPAL_NAME | 9 | User principal name format. For example, "JeffSmith@Fabrikam.com".
ADS_NAME_TYPE_CANONICAL_EX | 10 | Extended canonical name format. For example, "Fabrikam.com/Users Jeff Smith".
ADS_NAME_TYPE_SERVICE_PRINCIPAL_NAME | 11 | Service principal name format. For example, "www/www.fabrikam.com@fabrikam.com".

Give some of those a try and see what happens. We haven’t discussed the IADsNameTranslate interface much, but it’s a handy little thing to know about. On top of that, you can make a good case that this approach is a little faster and a little easier to use than an Active Directory search script, which would be an alternate method for looking up a specific user and then returning his or her GUID.

We hope that helps you with your translation problem, IU. Having recently returned from a trip to Italy, the Scripting Guy who writes this column knows all about translation issues. Not because of his trip to Italy: not only do a large number of Italians speak English, but the Scripting Guy who writes this column learned enough Italian to at least read menus and stuff. Instead, the Scripting Guy who writes this column needs a translator now that he’s back at Microsoft, listening to people say things like Web 2.0; strategic alignment; mission statements; empowerment and immersion; and proactive synergy.

Not to mention our all-time favorite: campus-wide infusion. (Which turns out to be the activity you engage in any time you hang up a poster in a Microsoft building.)

Ah, it’s good to be back!


How Can I Move All the Users from One OU to Another OU?

Hey, Scripting Guy! How can I move all the users from one OU to another OU?

-- PD

Hey, PD. You know, when you work at Microsoft you quickly learn to hate to word “move.” After all, if it wasn’t for the fact that we own the buildings you would think we were trying to skip out on paying the rent; that’s how often we move from one place to another. On top of that, the moves we make often boggle the mind. For example, at one time Greg and Dean had offices right across the hall from one another; for some reason, the two were forced to exchange offices, leaving them, well, right across the hall from one another. Another time Greg was moved to a new office in a new building. Two months later was moved around the corner to a different office. Three months later he was moved back to his previous office!

In case you’re wondering, that’s why we don’t like the word move.

But seeing as how all you want to do is move users from one OU to another (no packing and box-hauling required), well, that we can handle. Here’s a script that moves all the users in the Accounting OU to the Finance OU:

Set objNewOU = GetObject("LDAP://ou=finance,dc=fabrikam,dc=com")
Set objOldOU = GetObject("LDAP://ou=accounting,dc=fabrikam,dc=com")

objOldOU.Filter = Array("User")

For Each objUser in objOldOU
    objNewOU.MoveHere objUser.ADsPath, vbNullString
Next

You’d think a chore like this would be more complicated, wouldn’t you? But really, this is all the code you need. We start off by creating a pair of object references. The first one (objNewOU) binds us to the OU where we want to move the accounts:

Set objNewOU = GetObject("LDAP://ou=finance,dc=fabrikam,dc=com")

The second object reference (objOldOU) binds us to the OU where the accounts are currently housed:

Set objOldOU = GetObject("LDAP://ou=accounting,dc=fabrikam,dc=com")

Got that? It’s pretty simple, but it’s easy to get them mixed up. And you don’t want to do that: otherwise you’ll end up moving everyone from Finance to Accounting, which is not what you had in mind.

As you know, when you bind to an OU in Active Directory you automatically get back a collection of all the objects found in that OU. In this case, though, we aren’t interested in all the objects; the only objects we care about are the user accounts. With that in mind, we apply this filter to limit the items in the collection to user accounts:

objOldOU.Filter = Array("User")

This ensures that the only objects we’ll be working with – and thus the only objects we’ll be moving – are user accounts. If that’s not a problem – that is, if you also want to move computer accounts, printers, and any other objects found in the Accounting OU – then just leave out the line of code that applies the filter.

Now all we have to do is loop through the collection of user accounts and move each one to the Finance OU. That’s what we do here:

For Each objUser in objOldOU
    objNewOU.MoveHere objUser.ADsPath, vbNullString
Next

Notice how we move the user account; we call the MoveHere method, passing two parameters:

· objUser.ADsPath. This, as you probably guessed, represents the ADsPath of the account to be moved.

· vbNullString. This VBScript constant simply tells the script to move the account without renaming it. If we specified a different name here the account would not only be moved but would be renamed as well.

That’s all there is to it.

So does this mean the Scripting Guys are better able to handle the concept of moving? Let’s put it this way: having been in our current offices for less than a year, there’s a 50-50 chance we’ll be moving again in January. The great circle of Microsoft life goes on. (Editor’s Note: This next move might not be a bad idea, because somehow in the last move the Scripting Editor ended up on a completely different floor of the building than the rest of the Scripting Guys. Although this editor suspects the other Scripting Guys – or at least Greg – may have had something to do with that….)


How Can I Put New Users in the Same OU as the Person Creating Their Accounts?

Hey, Scripting Guy! We use a script to create user accounts. How can I modify this script so that it will place the new user in the same OU as the person running the script?

-- CB

Hey, CB. Interesting question. We’re assuming that you’ve delegated control of Active Directory. User A, for example, has the right to create users in the Finance OU, but only the Finance OU. User B, meanwhile, can also create user accounts, but only in the Headquarters OU. If you’re like other people we know in this situation you’ve done one of two things: either you’ve created a generic script that prompts the user to enter the OU each time they create a user account, or you’ve created separate scripts for each user who has been authorized to create accounts.

Unfortunately, neither of these approaches is optimal. The first one requires the user to type the Active Directory path each and every time they create a user account; the second one requires you to maintain, distribute, update, and otherwise take care of multiple scripts, all of which ultimately carry out the same exact task (they create user accounts). You’re looking for a single script that can say, “Oh, you’re User A, huh? Well, I know that you’re in Finance OU, so I’ll automatically create this new user account in that same OU.”

And good news: we just happened to have a script that will do this very thing. In fact, here it is:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

arrDirectoryLocation =  Split(objSysInfo.UserName, ",")

For i = 1 to Ubound(arrDirectoryLocation)
If i = 1 Then
strLocation = arrDirectoryLocation(i)
Else
strLocation = strLocation &
"," & arrDirectoryLocation(i)
End If
Next

strOU = "LDAP://" & strLocation

Set objOU = GetObject(strOU)
Set objUser = objOU.Create("User", "cn=Myer Ken")
objUser.sAMAccountName = "myerken"
objUser.GivenName = "Ken"
objUser.SN = "Myer"
objUser.AccountDisabled = FALSE
objUser.SetInfo

Admittedly, it might not be immediately obvious what’s going on here. So let’s take a minute or two to walk you through the process.

The script starts by creating an instance of the ADSystemInfo object; this object can return basic Active Directory information for the logged on user and his or her computer, including the user’s Distinguished Name (the UserName property). In other words, we can get back something that looks like this:

CN="Jonathan Haas", OU="Finance",DC="fabrikam",DC="com"

As you can see, this is close to the information we need; if we can get rid of the CN=”Jonathan Hass” we’ll have a path to the desired OU. And before you ask, no, we can’t just ask for the value of the user’s OU; for some reason, Active Directory doesn’t store that information directly. Because of that, we’ll have to figure out the OU path on our own. But trust us, that’s easy.

We begin the process with this line of code:

arrDirectoryLocation =  Split(objSysInfo.UserName, ",")

This code takes the user’s Distinguished Name and – thanks to the magic of the Split function – creates an array out of that name. Our new array (which divvies up the items using the comma as a delimiter) looks like this:

CN="Jonathan Haas"
OU="Finance"
DC="fabrikam"
DC="com"

What did that gain us? Well, now we have an array with four elements. If we get rid of the first element (Jonathan Haas, item 0) we can then construct the OU path using the remaining elements. To do that, we’re simply going to loop through array beginning with the second element (also known as item 1; remember the first element in an array is item 0, making the second element item 1). That’s what this code does: it bypasses the first element (CN=“Jonathan Haas”) and then grabs each of the remaining elements until there’s nothing left to grab (Ubound represents the last item in an array):

For i = 1 to Ubound(arrDirectoryLocation)

Inside the loop we reconstruct the OU path, jamming all the names back together and separating them with commas. You might notice that, when dealing with item 1, we don’t add a comma before tacking on the element value. If we did, we’d end up with a string that looked like this:

, OU="Finance",DC="fabrikam",DC="com"

Not what we want. By omitting the comma for the first item, we end up with this path:

OU="Finance",DC="fabrikam",DC="com"

Believe it or not, we’re home free now. To construct the ADsPath to the OU, we just need to tack on the provider name. By adding LDAP:// to the beginning of our string, we end up with a path that looks just like this:

LDAP://OU="Finance",DC="fabrikam",DC="com"

And guess what? By remarkable coincidence, this is exact information we need to bind to the OU and create the new user account. In other words:

Set objOU = GetObject(strOU)

The rest of the script simply creates the new user account, sets a few property values, and then uses SetInfo to save the new account to Active Directory. We had to go through a few gyrations to get there, but you now have a generic script that will create new user accounts in the same OU as the user running the script.


How Can I Retrieve Information Required by the Sarbanes-Oxley Act?

Hey, Scripting Guy! As part of our Sarbanes-Oxley requirements I need to get a list of all my Active Directory users, along with the date that their account was created and the last time their password was changed. How can I get a list like that?

-- DB

Hey, DB. You know, this past weekend the Scripting Guy who writes this column was helping out at a baseball tournament. As the time for the next game approached, this Scripting Guy happened to glance over to the side of the field: there, rising up over the hill, was The Umpire You Never Want to See. Even though the Scripting Guy wasn’t coaching in this game, and even though he had absolutely no stake in the tournament, he couldn’t help but shudder and avert his eyes. Seeing The Umpire simply sends a chill up and down every coach’s spine.

Sarbanes-Oxley tends to have the same effect on system administrators. (Although we’d willing to bet that, unlike some umpires we know, both Sarbanes and Oxley understand the infield fly rule.) Although ostensibly aimed at accountants and auditors, Sarbanes-Oxley has all sorts of implications for system administrators, too: for example, it’s up to the system administrator to produce things like a list of all Active Directory users, the date each account was created, and the last time each password was changed.

Note. True umpire story: One time the Scripting Guy was coaching third base. The Scripting Guy’s runner rounded second and slid into third, right about the time the ball arrived. Everyone turned to The Umpire: was the runner safe or out? The Umpire, meanwhile, turned to the Scripting Guy – the third base coach – and said, “My view was blocked. Was he safe or out?” The Scripting Guy said he was safe.

No, come on: he really was safe. Does that mean that the Scripting Guy would have said he was out if the runner really had been out? Well, no doubt he would have said that – hey, isn’t this column supposed to be about scripting and not about baseball?

If you’re a baseball coach, there’s good reason to be scared of certain umpires. But if you’re a system administrator, there’s no reason to be afraid of Sarbanes-Oxley, at least not when you have scripts like this one at your disposal:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    strPath = objRecordSet.Fields("ADsPath").Value
    Set objUser = GetObject(strPath)
    Wscript.Echo "User: " & objUser.FullName
    Wscript.Echo "Account created: " & objUser.whenCreated & " GMT"
    Wscript.Echo "Password last changed: " & objUser.passwordLastChanged
    Wscript.Echo
    objRecordSet.MoveNext
Loop

As you can see, this is a script that searches Active Directory; in particular, it searches Active Directory for all the objects that have an objectCategory equal to user (as you probably figured out, that’s just a fancy way of saying that it searches for all the user accounts in Active Directory). As usual, we won’t discuss the nuts-and-bolts of Active Directory searching today; for details, see the two-part Tales from the Script series Dude, Where’s My Printer?

In fact, the only nut or bolt we will mention today is the query we use, a query that retrieves the ADsPath for all the users in the domain:

objCommand.CommandText = _
    "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

Good question: why do we retrieve just the ADsPath? Well, what we’re going to do is bind to each individual user account and then retrieve the desired attribute values. The ADsPath – which is the Active Directory equivalent of a UNC file path – makes it a snap to locate and bind to each of these user accounts.

And that’s true: in theory, we could have specified all three of the attributes we’re interested in – FullName, whenCreated, and passwordLastChanged – in our SQL query. We say “in theory” because you can’t actually retrieve the passwordLastChanged attribute from within an LDAP query. Instead you have to bind to the user account to get this value.

Note. The passwordLastChanged attribute is an interesting little attribute: what it does is take the value of the pwdLastSet attribute – which represents the number of 100-second intervals that elapsed between January 1, 1601 and the time the password was last changed – and convert that value to a regular old date-time value. And, yes, if you’re far more comfortable dealing with 100-nanosecond intervals by all means retrieve the value of pwdLastSet instead. (Although you will also have to convert the 64-bit integer format of pwdLastSet to something VBScript can work with.)

To bind to each individual user account we begin by executing our query, then we use this line of code to position ourselves at record 1 in the recordset:

objRecordSet.MoveFirst

When that’s done we set up a Do Until loop that runs until we reach the end of the recordset (or, for you database aficionados, until the recordset’s EOF – end of file – property is true). Inside that Do Until loop we use this line of code to assign the ADsPath for the first user in the recordset to a variable named strPath:

strPath = objRecordSet.Fields("ADsPath").Value

Don’t remember what an ADsPath looks like? This must be your lucky day; we just happened to have a handy sample ADsPath we can share with you:

LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com

Best of all, that’s exactly the value we need to pass to the GetObject method in order to bind to the Ken Myer user account:

Set objUser = GetObject(strPath)

After we bind to the user account we can then echo back any – or even all – of the attribute values for that account. As we noted, we’ve chosen to echo back three attribute values:

Wscript.Echo "User: " & objUser.FullName
Wscript.Echo "Account created: " & objUser.whenCreated & " GMT"
Wscript.Echo "Password last changed: " & objUser.passwordLastChanged

After echoing the value for the first user in the recordset we simply call the MoveNext method and repeat the process for the second user in the recordset. This continues until we have retrieved and reported back information for all the users in the domain. That information, incidentally, will look something like this:

User: Ken Myer
Account created: 8/1/2006 3:01:00 PM GMT
Password last changed: 8/1/2006 8:01:00 AM

Oh, you’re right: we did tack a GMT on the end of the date and time the user account was created, didn’t we? Why? Well, the whenCreated attribute reports back the account creation date based on Greenwich Mean Time rather than local time. (By contrast, passwordLastChanged uses the local time when identifying the date and time the password was last changed.) We added the GMT designation to indicate that the displayed date and time is in Greenwich Mean Time.

Alternatively, we could convert the value returned by whenCreated to local time. That’s easy enough, provided you know the difference (or the “offset”) between local time and Greenwich Mean Time. For example, if your local time is 7 hours before Greenwich Mean Time, code similar to this will do the conversion for you:

dtmCreated = objUser.whenCreated
dtmCreated = DateAdd("h", -7, dtmCreated)
Wscript.Echo "Account created: " & dtmCreated

All we’re doing here is assigning the value of whenCreated to a variable named dtmCreated. We then use this line of code – and the DateAdd function – to add minus-7 hours to that value:

dtmCreated = DateAdd("h", -7, dtmCreated)

That results in dtmCreated being equal to our Greenwich Mean Time value minus seven hours. Or, in other words, local time.

Note. If you have a worldwide enterprise spanning multiple time zones, well, this can all get a bit crazy. In cases like that – and assuming that’s OK with Sarbanes and Oxley – you might be better off leaving the whenCreated value set for Greenwich Mean Time.

Time for one last question: why is the Scripting Coach so afraid of The Umpire? Here’s one reason. In one game the bases were loaded with one out. The ball was hit back to the pitcher, who immediately threw home. The catcher touched home plate and then threw to first in time to get the batter. Double play, right? Wrong. The Umpire – who was the only umpire on duty for that game – called the runner at home safe because “the catcher never tagged him.” Of course, the catcher didn’t have to tag him; the catcher only had to have his foot on the base. “Well, I wasn’t watching for that,” said The Umpire. “I was only watching for the tag. So I have to call him safe.”

And what about the runner at first? “Oh, I didn’t see the play at first,” said The Umpire. “I was bending down to pick up my mask. So I guess he’s safe, too.”

And some of you think Sarbanes-Oxley is unfair. You have no idea what unfair really is!


How Can I Return a List of All the Users Whose User Account Never Expires?

Hey, Scripting Guy! How can I return a list of all the users whose user account never expires?

-- PG

Hey, PG. You know, no one – not even his own mother! – has ever claimed that the Scripting Guy who writes this column is a genius. Needless to say, then, the first time he read this question he thought it said, “How can I return a list of all the users who never expire?” Having once served as a system administrator, the idea of having users who never expired, users who would continue to live – and to torment – their system administrators forever and ever was truly frightening. Fortunately, though, you were just talking about user accounts and not the users themselves.

Note. We should clarify that this Scripting Guy never wanted any of his users to die, not even the one who reformatted her hard drive because she read somewhere that doing so would remove all the old files that she didn’t use anymore. Well, it did.

For some reason, this calls to mind an episode of The Simpsons where Bart – saying his evening prayers – asks God to kill Sideshow Bob. “Bart!” said his mother. “You don’t ask God to kill people!” “That’s right,” added Homer, “you do your own killing.”

Fortunately, no one has to be harmed in any way in order for us to get back a list of all the user accounts that never expire. In fact, all you have to do is run a script similar to this:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Set objUser =
GetObject(objRecordSet.Fields("AdsPath").Value)

If objUser.AccountExpirationDate = "1/1/1970" Or
Err.Number = -2147467259 Then
Wscript.Echo objUser.Name
End If

objRecordSet.MoveNext
Loop

We should point out that we took a bit of a shortcut here, a shortcut that makes the script easier to write and easier to understand, but one that does make the script a tad bit slower and a little less efficient. Although there are two different properties that can be used to determine whether a user account expires or not – accountExpires and accountExpirationDate – neither one is a simple yes/no kind of value: yes the user account will expire someday, no the user account will never expire. Instead, each property contains a value that either indicates the date that the account expires or the fact that the account will not expire. Because accountExpires is a large integer value that tracks time via the number of nanoseconds that have expired since January 1, 1601, we decided to skip that whole mess and use accountExpirationDate instead.

Not that this totally solves all our problems, mind you. Technically, accountExpirationDate is not even an attribute of a user account; instead, it’s an Active Directory property method. When you ask for the accountExpirationDate, ADSI will actually grab the value of the accountExpires attribute and convert it to a real date. That’s the good news: when you ask for accountExpirationDate you’ll get back a real date-time value, not the number of nanoseconds that have expired since January 1, 1601. The bad news? Because accountExpirationDate isn’t a hard-coded user account attribute you can’t use ADO to search for accounts based on that attribute.

Having fun yet? Now, we could create a somewhat complicated script and do a search using the accountExpires attribute. Being the Scripting Guys, however, we opted to take the easy way out. Instead of retrieving only those user accounts where accountExpires is equal to a certain value, we decided to retrieve all the user accounts, then bind to each individual account and use accountExpirationDate to determine whether the account expires. Like we said, this is not the optimal way to do things; your script will take a bit longer to run. (Although, depending on the number of users you have, “a bit longer” might only be a few seconds.) However, this script is much easier to write, and thus much easier for you to use and modify.

Wait a second: we aren’t done just yet. There’s one more issue we have to deal with. How do we know whether or not a user account will expire? Well, if accountExpirationDate is set to January 1, 1970 then the account will never expire. (Weird, but true.) Unfortunately, though, that’s not the only way to determine whether or not a user account is configured to never expire. An account also doesn’t expire if no accountExpirationDate has been set. You find out accountExpirationDate has no value when you try to access this attribute and get back error number -2147467259 (the much-beloved “Unspecified error” error). That means for each account we need to determine whether accountExpirationDate is set to January 1, 1970 or whether we get back error number -2147467259 when trying to get the value of accountExpirationDate. That’s what this block of code is for:

If objUser.AccountExpirationDate = "1/1/1970" Or Err.Number = -2147467259 Then
    Wscript.Echo objUser.Name
End If

If either of our criteria is true, we echo back the user name; we do that because this is an account that does not expire. If neither criteria is true then we do nothing at all; that’s because this account does expire, and all we care about here are those accounts that never expire.

Well, at least that part is simple.

There’s obviously more to the script than just this simple block of code, but the remainder is pretty much a run-of-the-mill Active Directory search script. If you need more information about searching Active Directory, take a look at our two-part Tales from the Script series Dude, Where’s My Printer? That should give you a good explanation of what the rest of the script does.

Disclaimer. No users were harmed during the making of today’s column. We aren’t saying we weren’t tempted (like, say, the user who uninstalled Microsoft Word because he was finished using it for the day) but ….


How Can I Search Active Directory for User Names Stored in an Excel Spreadsheet?

Hey, Scripting Guy! How can I take user names from an Excel spreadsheet, search for those users in Active Directory, and then indicate whether or not each user was found?

-- SD

Hey, SD. You know, one of the drawbacks to writing a daily scripting column is that people never seem to believe you. For one thing, they don’t believe that you write a daily scripting column. (Well, OK: many people believe that you write one, they just can’t figure out why you write one.)

For another, they aren’t always convinced that you know what you’re talking about, at least when it comes to this daily scripting column. After all, this column touches on everything from the exploits of Home Run Baker to Schrodinger’s cat to the origin of Grandparents Day. That’s all nice, but is any of it actually true?

For example, there’s little doubt that many people were skeptical when this column first broached the notion of the turducken: a chicken cooked inside a duck cooked inside a turkey. “A chicken inside a duck inside a turkey?” they sniffed. “Then why not stuff the turkey inside a deer, the deer inside a bear, and the bear inside a hippopotamus?”

Hmmm …. Are you thinking what we’re thinking?

As it turns out, though, this time the Scripting Guy who writes this column knew what he was talking about. Take a look at this email he received from Bannon O’Leary:

“Just so ya'll know the Turducken is a reality and purty darn good eatin’. As a recent transplant to Nashville, TN from New Orleans I thought this was going to be my first Thanksgiving without a Turducken in a long time. Luckily a business trip took me back to New Orleans; a quick trip to the Gourmet Butcher Block on the West Bank and I had secured a wonderful Thanksgiving meal.”

See: the Scripting Guy who writes this column was right! Now do you believe him when he says that the Scripting Editor rides a broom to work each day? (Editor’s Note: Given that the Scripting Guy who writes this column has also accused the Scripting Editor of being a Muggle , he apparently can’t be right all the time.)

The moral of the story is that you really can trust Hey, Scripting Guy!, at least when it comes to esoteric trivia that no one else ever cares about. How about when it comes to answering scripting questions, especially questions like how someone can take user names from an Excel spreadsheet, search for those users in Active Directory, and then indicate whether or not each user was found? You’ll have to judge that for yourself:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
objExcel.Visible = True

i = 1

Do Until objExcel.Cells(i, 1).Value = ""
    strName = objExcel.Cells(i,1)
    objCommand.CommandText = _
        "SELECT * FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
            "AND samAccountName='" & strName & "'"
    Set objRecordSet = objCommand.Execute

    If objRecordset.RecordCount = 1 Then
        objExcel.Cells(i,2) = "Found"
    Else
        objExcel.Cells(i,2) = "Not found"
    End If

    i = i + 1
    objRecordset.Close
Loop

objConnection.Close

You’re right: thisis a bit longer than the typical script discussed in this column. Is that going to be a problem for the Scripting Guys? Of course not; after all, we’re just going to ignore a large portion of the script anyway. In particular, we won’t discuss Active Directory searching in any detail; that’s a subject way that’s simply too complex for Hey, Scripting Guy! But don’t despair: if you’re not familiar with the ins and outs of writing scripts that can search Active Directory then just take a peek at our two-part series Dude, Where’s My Printer?

With that in mind we’ll thus skip past the first eight lines of code and start here, with three lines in which we create an instance of the Excel.Application object, use the Open method to open to file C:\Scripts\Test.xls, and then set Excel’s Visible property to True:

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
objExcel.Visible = True

What does that do for us? That gives us a running instance of Test.xls that we can see on screen, an instance that – for our purposes – looks like this:

As you can see, there’s not much to our version of Test.xls. In column A we have a list of samAccountNames; in column B we have, well, nothing. But don’t worry: before we’re through we’ll use column B to indicate whether or not these samAccountNames can be found in Active Directory.

Once Excel is up and running our next task is to assign the value 1 to a variable named i, a counter variable that we’ll use to keep track of our location within the spreadsheet. We then set up a Do Until loop that runs until we encounter an empty cell in column A:

Do Until objExcel.Cells(i, 1).Value = ""

And yes, that’s a good point: this script assumes that you didn’t skip any rows when entering your user names. For example, suppose you have samAccountNames in rows 1 and 3, but for some reason have a blank cell in row 2. If you run this script it will only search for the name listed in row 1. Why? Because when it hits the blank cell in row 2 the script assumes that it’s out of data and automatically exits the Do Until loop.

Just something to watch out for: don’t skip rows when entering your data.

So what happens inside this Do Until loop? Well, for starters, we assign the Value of cell row 1, column 1 to a variable named strName (remember, i starts off equal to 1):

strName = objExcel.Cells(i,1)

After we’ve assigned this first samAccountName to strName we then construct a SQL query we can use to search fabrikam.com for all objects that: 1) have an objectCategory equal to user; and, 2) have a samAccountName equal to the value of strName:

objCommand.CommandText = _
    "SELECT * FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND samAccountName='" & strName & "'"

We then use the Execute method to run the query and return a recordset consisting of all the users with a samAccountName equal to strName:

Set objRecordSet = objCommand.Execute

As you probably know, samAccountNames must be unique within a domain. That means our recordset will either have 0 records (because it couldn’t find a user account with that name) or 1 record (because the script could find a user with that name). In turn, that means we can verify whether or not a given name was found simply by checking the value of the recordset’s RecordCount property (as the name implies, this tells us the number of records in the recordset):

If objRecordset.RecordCount = 1 Then

If the RecordCount is equal to 1, we set the value of row 1, column 2 to Found; otherwise we set the value of row 1, column 2 to Not found. That’s what we do here:

If objRecordset.RecordCount = 1 Then
    objExcel.Cells(i,2) = "Found"
Else
    objExcel.Cells(i,2) = "Not found"
End If

We then increment the value of i by 1, meaning that, the second time through the loop, we’ll check the value of row 2, column 1. After that we close the recordset and then loop around and repeat the process with the next row in the spreadsheet. When we’re all done we close our connection to Active Directory and display the finished results:

Not bad, if we do say so ourselves.

OK, now that this scripting stuff is out of the way let’s get back to something that people really care about: the turducken. According to Bannon, “My big issue is what to do with it, my wife is a vegetarian and my son is just working on getting his first tooth. 16 pounds of Turkey, Duck, and Chicken is gonna make for a lot of sandwiches.”

Yes, that does sound like a problem, doesn’t it? Tell you what, Bannon: any time you have 16 pounds of Thanksgiving leftovers feel free to send them to the Scripting Guy who writes this column.

Oh: and that goes for anyone else who might have Thanksgiving leftovers, too.


How Can I Set a User’s Password and Then Require Them to Change That Password the Next Time They Logon?

Hey, Scripting Guy! In our school, how can I enable teachers to change a student’s password to their logon name, then force those students to change their password the next time they log on?

-- DS

Hey, DS. Boy, did you pick the wrong person to ask. After all, the Scripting Guy who writes this column used to be a teacher; if he knew how to force students to do things he’d probably still be teacher!

Actually, the Scripting Guy who writes this column always got along really well with his students; in fact, he’s always gotten along really well with kids, period. As his fellow Scripting Guys will attest, it’s only adults he has problems with. Perhaps that’s because, as the Scripting Son pointed out a few weeks ago, “You know, any place you go you’re usually the most immature person there.” And no, we have no idea what could have provoked this outburst.

Well, OK, maybe we have some idea ….

But regardless of whether you’re dealing with a grownup, a child, or even a Scripting Guy, here’s a script that will set a user’s password to their logon name, then require that user to change that password the next time they logon:

Set objUser = GetObject("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

objUser.SetPassword objUser.sAMAccountName

objUser.Put "pwdLastSet", 0
objUser.SetInfo

And yes, it is a very short and concise script, isn’t it? Yet another reason why the Scripting Guy who writes this column is no longer a teacher: teachers have to work hard. As for Scripting Guys, well ….

As you can see, we start out by binding to the user account in Active Directory; that’s what this line of code does:

Set objUser = GetObject("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

We then call the SetPassword method in order to change the user’s password. With SetPassword we have to supply one parameter: the new password for the user. Because we want the new password to be the user’s logon name we pass the value of the sAMAccountName attribute:

objUser.SetPassword objUser.sAMAccountName

Believe it or not, that’s all we have to do to set the user’s password.

Requiring the user to change that password the next time they log on is twice as hard; after all, that requires two whole lines of code:

objUser.Put "pwdLastSet", 0
objUser.SetInfo

In the first line we assign the value 0 to the pwdLastSet attribute. This attribute keeps track of the last time a user changed his or her password. If the value is set to 0 then the user has to change their password the next time they log on.

In line 2 we then call the SetInfo method to write the updated attribute value back to Active Directory.

That should do the trick, DS, provided, of course, that teachers have the right to change user passwords and to update the pwdLastSet attribute.

Speaking of teachers, the Scripting Guy who writes this column actually enjoyed teaching; that’s one reason he spends so much of his time coaching. What he didn’t like were school districts: huge, impersonal bureaucracies that never know what they’re doing, or why. And so he ended up at Microsoft instead.

And yes, we know what you’re thinking. But if anyone asks, you said it, not us.


How Can I Specify the Computers a User Can Use to Log On to the Domain?

Hey, Scripting Guy! How can I configure a user account so that the user can only log on to the domain using specific computers?

-- RB

Hey, RB. You know, at one point we had some really good solutions for you; unfortunately, Microsoft’s legal department said no to every single one of them. In their view, “We believe most Human Resources departments would frown on system administrators hitting their users over the head with a 2x4.”

Well, maybe. Although you’ll never convince us that those people from Human Resources haven’t thought about hitting their clients over the head with a 2x4. And probably on more than one occasion. (Editor’s Note: Yes, once again, the editor needs to step in and make a small, probably futile, attempt at keeping everyone out of trouble: Microsoft and the Scripting Guys do not promote, support, or have anything to do with violence in the workplace, or anywhere else for that matter. Well, okay, football is a little violent, and that’s okay. And many people think boxing is okay, and that’s pretty violent. All right, forget it; we’ll let the lawyers figure it out.)

Which means, of course, that all our solutions involving electric shocks, trap doors, and trained attack monkeys had to be thrown out. That left us with just one answer for you: a usable – albeit somewhat boring – solution involving ADSI. Something that looks a lot like this:

Set objUser = GetObject _
  ("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

objUser.Put "userWorkstations", "atl-ws-01,atl-ws-02,atl-ws-03"
objUser.SetInfo

Like we said, this was not our first choice. Still, it is a bit easier to write and run a three-line script than it is to create a race of zombie slaves that will guard your computers and prevent unauthorized users from logging on. As you can see, there isn’t much to this script. After binding to the Ken Myer user account in Active Directory it then takes just two lines of code to specify the computers that Ken can use to log on to the domain:

objUser.Put "userWorkstations", "atl-ws-01,atl-ws-02,atl-ws-03"
objUser.SetInfo

In the first line we’re simply assigning the allowed computers to the userWorkstations attribute. Note the syntax here: we specify all the computer names inside a pair of double quote marks, separating each name with a comma. When doing this keep in mind what our goal is: we’re saying that these are the only three computers Ken Myer can use to log on to the domain. You already know that, but we don’t want people getting confused and thinking that we’re denying Ken access to these computers. On the contrary: we’re denying Ken access to every machine except these computers.

After assigning the computer names to userWorkstations we then call the SetInfo method to write the changes back to the Ken Myer user account in Active Directory. And that’s it; we’re done. Which means we can save the catapult for another day.

Of course, it’s possible that you’ll change your mind later on and want to give Ken the right to use any computer to log on to the domain. Then what?

Well, you could simply give your zombie slaves two weeks notice. However, as we’ve learned – the hard way – zombies don’t take bad news very well. Consequently, your best bet might be to use another script to eliminate all the values in the userWorkstations attribute. When this attribute is blank, that means the user can log on to the domain using any computer:

Const ADS_PROPERTY_CLEAR = 1

Set objUser = GetObject _
    ("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")

objUser.PutEx ADS_PROPERTY_CLEAR, "userWorkstations", 0
objUser.SetInfo

Seeing as how we’re a little busy right now we won’t bother to explain how this bonus script works. We also won’t talk about how you can remove a single computer from the userWorkstations attribute or how you can add a new computer to the list. After all, that’s what the Microsoft Windows 2000 Scripting Guide is for.

Besides, the attack monkeys are here for their mid-year performance review. (To tell you the truth, other than that one regrettable moment at the company Christmas party they’ve really had a good year so far.)


How Can I Tack a Prefix Onto the Telephone Numbers of All the Users in My Domain?

Hey, Scripting Guy! How can I tack a prefix onto the telephone numbers of all the users in my domain?

-- JB

Hey, JB. It’s Monday morning as this column is being written, and the Scripting Guy who actually writes this column is still a bit tired from driving across the state and back in order to attend his niece’s wedding. Yes, yet another a wedding: the third he’s had to attend in the last six months. But that’s all right; after all, is there anything more fun and exciting than a wedding?

Actually, come to think of it, just about everything is more fun and exciting than a wedding. Nevertheless, the Scripting Guy who writes this column has to rate Tim and Tiffany’s wedding as the best he has ever had the … privilege … of attending. Is that because of the obvious love and affection between the bride and groom? Well, to tell you the truth, the Scripting Guy who writes this column didn’t pay much attention to that. What he did pay attention to was this: the entire ceremony, from start to finish, was over in 19 minutes. No slide shows of the happy couple as children; no poems read by lifelong friends; no songs written and performed by a second-cousin; none of that. Instead it was do you take this man/woman to be your lawful wedded husband/wife; we do; you may now kiss the bride. Oh, and Scripting Guy who writes that column, you may now dig into the roast pork loin with apple-brandy sauce.

Amen!

It probably goes without saying that the Scripting Guy who writes this column isn’t much of one for pomp and circumstances; in his view, the quicker he can get something done the quicker he can get to those Tuscan potatoes. Which is one reason why he liked this question about tacking a prefix onto the phone numbers of all the users in Active Directory; that’s a question that can be answered just this easy, and just this quick:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT ADsPath, telephoneNumber FROM " & _
        "'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    strUser = objRecordSet.Fields("ADsPath").Value
    strPhoneNumber = "425-" & objRecordSet.Fields("telephoneNumber").Value
    Set objUser =  GetObject(strUser)
    objUser.telephoneNumber = strPhoneNumber
    objUser.SetInfo
    objRecordSet.MoveNext
Loop

Following a long-standing Scripting Guys tradition, we’re going to explain only part of this script. That’s because the first 9 or so lines are boilerplate code for searching Active Directory; they can be used pretty much as-is in any Active Directory search script you write. But you don’t just have to take our word for it; instead, you can take a look at this two-part Tales From the Script series that explains the mystery behind such terms as ADODB.Connection and ADsDSOObject.

But we won’t totally shirk our responsibilities here. Instead, we’ll at least take a minute or two to explain this line of code:

objCommand.CommandText = _
    "SELECT ADsPath, telephoneNumber FROM " & _
        "'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

As you probably guessed, this is the line of code that specifies the criteria to be used when searching Active Directory. What we’re doing here is searching the fabrikam.com domain. What exactly are we searching for? We’re searching only for user accounts; we know that because we included a WHERE clause limiting the returned data to objects where the objectCategory is equal to user. (Which, in turn, means we’ll get back only user accounts.) What if we wanted to search for computer accounts instead? No problem; we’d just change the CommandText property so it looked like this:

objCommand.CommandText = _
    "SELECT ADsPath, telephoneNumber FROM " & _
        "'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='computer'"

That was easy enough. And as long as we’re on the subject of returned data, whenever we search Active Directory we need to specify the attribute values we want to get back. (Don’t try a “Select * FROM” command; you’ll be very disappointed.) In this script we’re asking to get back values for two attributes: ADsPath, which is roughly equivalent to an Active Directory file path (thus enabling us to uniquely locate an individual user account); and telephoneNumber, which just happens to be the user’s telephone number.

After specifying our search criteria we then call the Execute method, which runs the query and returns a recordset consisting of all the users in Active Directory, along with their ADsPath and phone number:

Set objRecordSet = objCommand.Execute

In other words, that gives us a list of all our users and their current phone numbers. All we need to do now is stick a prefix onto each of those phone numbers.

So how do we intend to do that? To begin with, it’s important to note that Active Directory searches are read-only; we can’t write a query that changes attribute values. (If you’re wondering why we didn’t do some sort of SQL Update query, well, now you know.) Instead, we have to individually connect to each and every user account and then change the phone numbers one-by-one. Admittedly, we could have made this script a tiny bit shorter by combining a few of the lines of code we’re about to discuss. However, in order to make things crystal clear we decided to to break the process into somewhat more-discrete steps, beginning with this:

strUser = objRecordSet.Fields("ADsPath").Value

What we’re doing here is grabbing the ADsPath for the first user in the recordset and storing it in a variable named strUser; that value is going to look something like this:

LDAP://CN=Ken Myer,OU=Finance,dc=fabrikam,dc=com

After getting the ADsPath we then use this line of code to construct a new phone number for this first user:

strPhoneNumber = "425-" & objRecordSet.Fields("telephoneNumber").Value

Here we’re sticking the prefix 425- to the front of each phone number; that’s because (for this sample script) we’re adding that area code to each number. As you can see, this is a very simple line of code: we’re just combining the prefix 425- with the user’s current phone number (objRecordSet.Fields("telephoneNumber").Value), and then assigning the resulting value to a variable named strPhoneNumber.

We should also point out that, for simplicity’s sake, we’re assuming that all users have a phone number and that none of these phone numbers already include the area code. Depending on your setup those might not be safe assumptions to make. But that’s OK; it’s easy enough to build some safeguards into the script that will help prevent you from assigning the “phone number” 425- to someone who doesn’t currently have a phone number, or from giving someone else a phone number like 425-425-555-1981 (because this user’s phone number already included the area code). For example, you can use the IsNull function to verify that a user actually has a phone number before you add the prefix:

If Not IsNull(objRecordSet.Fields("telephoneNumber").Value) Then

In other words, if the telephoneNumber is not Null (that is, if it does have a value) then go ahead and add the prefix. What if the phone number is Null, what if it doesn’t have a value? In that case, configure the script so that it doesn’t even bother with the phone number for this user, but instead loops around and tries the next user in the recordset.

As for checking to see if the area code has already been added to the phone number, well, this line of code should do the trick:

If Left(objRecordSet.Fields("telephoneNumber").Value, 4) <> "425-" Then

See how that works? Here we’re using the Left function to look at the first 4 characters in the telephone number. If the first four characters are already 425- then we won’t do anything; we’ll just loop around and work with the next record in the recordset. If the first four characters aren’t 425- then we’ll go ahead add the prefix.

Pretty easy stuff, and you should be able to add that code in without much trouble.

Now we’re ready to change the phone number, something we do using these three lines of code:

Set objUser =  GetObject(strUser)
objUser.telephoneNumber = strPhoneNumber
objUser.SetInfo

In the first line we’re simply binding to the user account in Active Directory. In line 2, we’re assigning the new phone number (stored in the variable strPhoneNUmber) to the telephoneNumber attribute for that account. And then in line 3 we call the SetInfo method and actually write the new phone number to Active Directory.

Whatever you do, don’t leave out SetInfo; if you do, the phone number will not be changed. And don’t leave out this line of code, either:

objRecordSet.MoveNext

Leave out that line, and the script will never move on to the next record in the recordset; instead, your script will drag on forever and ever, never getting anywhere at all.

Just like the typical wedding ceremony.

Anyway, that should do it, JB. Let us know if you run into any problems here.

In addition to the food, we should note that there was one other factor that made this last wedding worthwhile: the Scripting Son caught the garter. That’s not that big of a deal (although he did make a nice catch); what was a big deal is that, during the reception, the “young man who caught the garter and the young lady who caught the bouquet” were asked to come to the front of the room. At that point, the two of them were forced to dance with one another (while they played the song The Chapel of Love). (Actually, the young lady who caught the bouquet was happy to dance. The Scripting Son? Not so happy.)

So is the Scripting Son a good dancer? Let’s put it this way: have you ever seen Fred Astaire or Gene Kelly dance? You have? OK; in that case we won’t lie to you and say that the Scripting Son is a good dancer; you obviously know what good dancing looks like. Instead, we’ll say this: you know how the guards at Buckingham Palace are famous for never moving and never changing expression? Based on his dancing alone, we have no doubt that the Scripting Son could get himself a job at Buckingham Palace any time he wants one.


How Can I Tell On What Date an Active Directory User Account Was Created?

Hey, Scripting Guy! Is there any way to tell the date that an Active Directory user account was created?

-- DD

Hey, DD. And thanks: between the work that piled up over the holidays, the upcoming Scripting Week 2, and a nasty flu bug that’s going around, it’s nice to have a question that can be answered with just two lines of code.

That’s right: two lines of code. To determine the date that a user account (or any other Active Directory object) was created, all you have to do is bind to the account and retrieve the value of the WhenCreated attribute:

Set objUser = GetObject("LDAP://cn=ken myer, ou=Finance, dc=fabrikam, dc=com")
Wscript.Echo objUser.WhenCreated

Cool, huh? The nice thing is that the value comes back as a real date and time, something on the order of 1/6/2005 8:10 AM. If you know anything about Active Directory, you know it isn’t always this easy to work with dates.

Incidentally, it’s just as easy to determine when an account was last modified. All you need to do is retrieve the value of the WhenChanged attribute:

Set objUser = GetObject("LDAP://cn=ken myer, ou=Finance, dc=fabrikam, dc=com")
Wscript.Echo objUser.WhenChanged

We know: it seems way too easy, doesn’t it? But, hey, enjoy it while you can; one of these days we’ll talk about the last logon and last logoff dates, and we can guarantee you that those won’t be quite as easy.


How Can I Use a Script to Create a Sequential Series of User Accounts?

Hey, Scripting Guy! For purposes of testing, I often need to create multiple user accounts like this: pgeorge01, pgeorge02, etc. How can I write a script that automatically creates the next account in the sequence; that is, pgeorge03?

-- RG

Hey, RG. You know, after our last experiment went horribly, horribly wrong, we vowed never to get involved in anything that even smacked of human cloning ever again. (In case you’re wondering, creating an army of uncontrollable zombies that rise up and terrorize the countryside does not look good on your performance review.) Still, we’re not really cloning people here, we’re just kind of cloning user accounts. That seems safe enough.

Besides, you know what they say: you can’t keep a good man (or woman) down.

Turns out, you can’t keep the Scripting Guys down, either.

Although there are several different ways we could approach this task we decided to stick with an old favorite: an Active Directory search. What we’re going to do is search Active Directory for all the user accounts that have a samAccountName beginning with the letters kenmyer. We’re then going to count the number of users we get back from this search. Why? Well, suppose we get back 3 users. Assuming we’ve stuck to the naming convention (something that our script hinges on) that can only mean that we already have these three user accounts in Active Directory:

· kenmyer01

· kenmyer02

· kenmyer03

That also means that all we need to do is increment the record count by 1 and we’ll know which user account to create next: kenmyer04. Again, assuming you stick with the naming convention, everything should work just fine.

Note. What do we mean when we say “stick with the naming convention?” Well, suppose you manually create two users accounts: kenmyer and kenmyer03. Is that a problem? As a matter of fact, it is. When we search Active Directory we’ll discover that there are two accounts that have samAccountNames beginning with the letters kenmyer. The script will thus assume that the next account to be created should be kenmyer03. Unfortunately, though kenmyer03 already exists. Admittedly, there are ways that we can handle that problem, but we won’t worry about it today. If there’s enough interest in this, we’ll return to it in a future column.

Let’s take a look at the script:

On
Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

strName = "kenmyer"
strSearchName = strName & "*"

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='user' " & _
"AND samAccountName = '"
& strSearchName & "'"
Set objRecordSet = objCommand.Execute

intRecordCount = objRecordset.RecordCount
intRecordCount = intRecordCount + 1

If intRecordCount < 10 Then
strNewName = strName & "0" &
intRecordCount
Else
strNewName = strName & intRecordCount
End If

Set objOU = GetObject("LDAP://OU=Finance,dc=fabrikam,dc=com")
Set objUser = objOU.Create("User", "cn= " & strNewName)
objUser.samAccountName = strNewName
objUser.SetInfo
objUser.SetPassword "password"
objUser.AccountDisabled = False
objUser.SetInfo

As you can see, we start out by defining a constant named ADS_SCOPE_SUBTREE and setting the value to 2; we’ll use this constant to tell the script to search all of Active Directory. Having said that, that’s pretty much all we will say about the code we use to conduct an Active Directory search. If you’re unfamiliar with Active Directory search scripts then you might want to take a look at our two-part Tales from the Script feature Dude, Where’s My Printer?

After defining the constant we then assign values to a pair of variables:

strName = "kenmyer"
strSearchName = strName & "*"

You can probably figure out what we’re going to do with the variable strName: that variable contains the string of letters (kenmyer) we’re searching for. It might not be so obvious, however, why we’re assigning the value kenmyer* to the variable strSearchName. Oh, right: considering the fact that we named the variable strSearchName it actually is kind of obvious, isn’t it? Well, you’re right: strSearchName is simply the search string we’re going to use; as we noted earlier, we want to get back a collection of all the user accounts where the samAccountName begins with the letters kenmyer. To do that we simply specify the letters we want to search for and then tack on an asterisk to use as a wildcard character.

Note. When searching Active Directory the wildcard character simply represents “anything,” meaning that, in this script, we’re searching for all samAccountNames that begin with kenmyer followed by anything (or even by nothing).

After assigning values to our two variables we then encounter a big block of code that actually performs the Active Directory search. Like we said, we won’t discuss that code in any detail today; all we’ll do is point out the query we use for retrieving the user accounts:

objCommand.CommandText = _
    "SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
        "AND samAccountName = '" & strSearchName & "'"

We’re just searching for all the user accounts (objectCategory = ‘user’) that have a samAccountName equal to the search variable strSearchName. Pretty simple, really.

That brings us to this part of the script:

intRecordCount = objRecordset.RecordCount
intRecordCount = intRecordCount + 1

In the first line of code we’re setting a variable named intRecordCount to the value of the RecordCount property; it probably comes as no surprise that RecordCount simply tells us the number of records in the returned dataset (or, more specifically, the number of users with a samAccountName that begins with kenmyer). In the second line of code we then increment the value of intRecordCount by 1. Why? Well, suppose we get back two records. That means the next account we create should be kenmyer03. Because 2 + 1 = 3, we increment the current record count by 1.

And, yes, all those years of high school and college math have resulted in this: we know that 2 + 1 = 3. Time and money well-spent!

Note. What if the record count equals 0? That’s OK: that just means we don’t have any kenmyer* accounts at the moment. That also means that when we increment the record count by 1 we’ll get, well, 1; thus the next account we create will be kenmyer01. Which is just as it should be.

In the next section of code we then create a name for the new account:

If intRecordCount < 10 Then
    strNewName = strName & "0" & intRecordCount
Else
    strNewName = strName & intRecordCount
End If

What we do here is first check to see if the record count is less than 10. If it is, we create a user account using these three elements:

· The user name (kenmyer), as stored in the variable strName.

· A 0.

· The record count.

Why do we toss the 0 into the middle? That’s easy: that gives us accounts like kenmyer02 and kenmyer03 rather than kenmyer2 and kenmyer3. That maintains consistency, and also makes it easier to view and sort the account names. If the record count is 10 or more we create the user name using only the variable strName and the record count. For example:

kenmyer + 11 = kenmyer11

And now we’re ready to create the account:

Set objOU = GetObject("LDAP://OU=Finance,dc=fabrikam,dc=com")
Set objUser = objOU.Create("User", "cn= " & strNewName)
objUser.samAccountName = strNewName
objUser.SetInfo
objUser.SetPassword "password"
objUser.AccountDisabled = False
objUser.SetInfo

This really isn’t the time or place to discuss the ins and outs of creating user accounts in Active Directory. If the preceding code looks a bit mysterious to you, don’t fret; instead, check out the Active Directory Users chapter in the Microsoft Windows 2000 Scripting Guide.

And that’s it: each time we run the script we’ll create a new, properly-incremented kenmyer* account. Turns out that this human cloning thing isn’t that big of a deal after all.

Note. Nonetheless, we do need to set the record straight on one thing. The army of clones we created didn’t actually go around terrorizing the countryside. Instead, being clones of the Scripting Guys they pretty much lounged around the office all day, surfing the Internet, taking coffee breaks, and then going home early in order to coach baseball. Or at least they said they were going to “coach baseball ….”


How Come My Search Doesn’t Return All My User Accounts?

Hey, Scripting Guy! I’m trying to pull a list of all my users out of Active Directory. I’m using ADO to search for these users, but no matter what I do I can only get 1,000 user names, even though we probably have 10 times that many user accounts. What could I be doing wrong?

-- MC, Athens, GA

Hey, MC. Boy, if we had a nickel for every time we’ve been asked this question we’d have – well, OK, we’d have a nickel. But that’s only because we just started doing this column; trust us, this is a question that gets asked all the time.

What you’re running into here is a default built into Active Directory: by default, any time you run a query against Active Directory you only get back the first 1,000 objects. That’s it. It doesn’t matter how many objects you actually have in Active Directory, it doesn’t matter how much memory you have on your domain controllers, nothing matters except this: you only get back the first 1,000 objects.

Fortunately, though there’s an easy workaround. Here’s a script that returns all the user accounts in Active Directory. Yes, we said all; if you have 100,000 user accounts, this script will return all of them:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' " & _
        "WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

So what’s the secret? The secret is the line that sets the Page Size to 1000. What’s so special about that? Well, if you don’t specify a Page Size, Active Directory returns only the first 1,000 items. If you do specify a Page Size, however, Active Directory will return the first x items, then pause for a split second and return the next x items, then pause for a split second, and so on. This will continue until all the items have been returned. In this example, we set the Page Size to 1,000 (the maximum), so we’ll get the first 1,000 items, there will be a (typically imperceptible) pause, then we’ll get the next 1,000 items. That will continue until all the items have been returned.

For more information about using scripts to search Active Directory, see this Tales From the Script column.


Can I Create and Delete a DSN Using a Script?

Hey, Scripting Guy! Is it possible to create an ODBC Data Sources Name (DSN) on the fly and then remove it when done?

-- KC

Hey, KC. Is it possible to create and then delete an ODBC DSN? To tell you the truth, we weren’t entirely sure. As it turns out, however the answer is this: as long as you’re willing to write a script that modifies the registry, then yes, you can programmatically create and delete DSNs.

Note. If you aren’t sure what a DSN is, it’s a way for you to connect to a database merely by referencing the DSN name, and without having to specify the entire path to the database. That’s particularly handy if you have a bunch of scripts that access this database. If you hardcode the path into the scripts and then move the database from one server to another, you’ll have to edit the path on each and every script. If you use a DSN, however, you won’t have to edit the scripts at all; instead, you just need to modify the DSN. That’s one change versus hundreds of changes (depending on how many scripts you have).

What we’re about to show you is a script that creates a DSN that uses SQL Server. Is that important? Yes. You can create DSNs that use other ODBC databases (Access, Oracle, FoxPro, whatever). However, the registry keys and values used when creating a DSN to access an Oracle database aren’t necessarily the same as those used to access a SQL Server database. If you’re using SQL Server, you can just copy our script and change the names of the database, the server, etc. If you’re using a different database, we’d recommend that you manually create a DSN. After you do that, fire up your favorite registry editor and look for the DSN here:

HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources

And here:

HKLM\SOFTWARE\ODBC\ODBC.INI\Name You Gave the DSN

Take a look at the registry keys and values required for that type of database, and then plug that information into our script. If you aren’t sure how to do that, give us a week or two, and we’ll see what we can do about getting some sample scripts for different ODBC databases posted in the Script Center.

At any rate, here’s a script that creates a DSN that uses SQL Server:

Const
HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\"
& _
strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources"
strValueName = "Script Repository"
strValue = "SQL Server"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\Script Repository"

objReg.CreateKey HKEY_LOCAL_MACHINE,strKeyPath

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\Script Repository"

strValueName = "Database"
strValue = "Script Center"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strValueName = "Driver"
strValue = "C:\WINDOWS\System32\SQLSRV32.dll"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strValueName = "Server"
strValue = "atl-sql-01"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strValueName = "Trusted_Connection"
strValue = "Yes"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

Yes, we know, it’s a bit long, but that’s because there are a number of registry keys and values that have to be created. For the most part, though, the script is pretty straightforward. We start by setting the constant HKEY_LOCAL_MACHINE to &H80000002, the value required to connect to the HKLM portion of the registry. We then bind to the WMI service, and to the StdRegProv class (which – as we always remind you – happens to be in the root\default namespace).

From there we create a new registry value (HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\Script Repository), with Script Repository the name we’re giving to our new DSN.We set the value of this, well, value to SQL Server, because that’s the type of database we’re connecting to.

Got that? After that we create a new registry key: HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\Script Repository (note that the key name is the same as our DSN Name). We then need to configure the following registry values:

· Database -- Name of the actual database we want to connect to. In this example, the database is named ScriptCenter.

· Driver -- Path to SQL Server ODBC drive. In this example, that path is C:\WINDOWS\System32\SQLSRV32.dll.

· Server -- Name of the server where the database is found. In this case, the server name is atl-sql-01.

· Trusted_Connection -- Tells SQL Server to use our logon credentials when we access the database. This enables us to connect to the database without having to supply a user name and password.

That’s it; run the script, and you’ll have yourself a fully-functioning DSN.

And what about getting rid of that DSN once you’re finished with it? Well, that’s equally easy: after all, we just need to delete the registry keys and values our first script created. Here’s a script that will delete the DSN Script Repository :

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
    strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\Script Repository"
objReg.DeleteKey HKEY_LOCAL_MACHINE, strKeyPath

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources"
strValueName = "Script Repository"
objReg.DeleteValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName

Note that what this script does is delete the first registry value we created (Script Repository), and then deletes the registry key we created. We don’t have to delete the individual registry values within that key, because deleting HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\Script Repository will delete all the values as well.


How Can I Append a Value to a Specified Field For Each Record in a Database?

Hey, Scripting Guy! How can I add the letter s to the end of a specified field for each record in an Access database?

-- A

Hey, A. It’s day 2 of TechEd 2007 and the Scripting Guys are still sitting here in the CMP Media booth (that’s booth No. 1301 for those of you attending TechEd); they’re busy handing out copies of Dr. Scripto’s Fun Book, holding drawings for Dr. Scripto bobblehead dolls, and otherwise just hanging around and having fun. But we haven’t forgotten those we’ve left behind. Remember, you can win a Dr. Scripto bobblehead, too: you just need to enter the TechEd Challenge. Plus we left some interesting reading material to help pass the time until we get back; today, for example, we have Part 1 of a two-part series on using scripts to work with CDs in Windows Vista and Windows Server 2008. If you can’t make it to Disney World, well, using scripts to burn CDs in Windows Vista is definitely the next best thing.

As for TechEd 2007, well, so far, so good. It’s kind of fun to man a booth here, at least in part because Microsoft doesn’t always give system administration scripting its due; it’s safe to say that many people who work at Microsoft don’t feel as though the Scripting Guys serve much of a purpose. But that’s not true. For example, right now Scripting Guy Jean Ross is manning our booth. Let’s listen in as she tries to help Microsoft customers:

“Go through that door and turn left. The bathrooms are just down the hall.”

“No, you still have time: lunch is served until 1:00 PM today.”

“Yes, that’s true: the attendee party is Thursday night at Universal Islands of Adventure.”

See? Bathrooms, lunch, parties: where would Microsoft customers be without the Scripting Guys to help answer questions?

Just a second, someone’s asking Jean another question: “How can I add the letter s to the end of each record in a database field?” What an amazing coincidence; thank goodness we have today’s column, huh? Here’s how you, or anyone else, can add the letter s to the end of each record in an Access database field:

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = c:\scripts\test.mdb"

objRecordSet.Open "UPDATE TextFiles SET FileName = FileName & 's'", _
    objConnection, adOpenStatic, adLockOptimistic

objRecordSet.Close
objConnection.Close

Sorry; we have to answer another question here. No, the Technical Learning Center closes at 6:00 PM. No thanks necessary; that’s what we’re here for.

OK, back to the script. We start out by defining a pair of constants: adLockOptimistic and adOpenStatic. We aren’t going to discuss these constants in any detail today; if you’d like to know more about these things and what they are used for then take a look at our Scripting Guys webcast on working with databases. For now, it’s enough to know that these constants will define the record-locking and cursor type for our recordset.

After defining the constants we create instances of the ADODB.Connection and ADODB.Recordset objects; we then use the following line of code to open the database C:\Scripts\Test.mdb:

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb"

Note: If you’re running this against an Access 2007 database your Open statement will look a little different. Not only is the file extension on the database file different (.accdb rather than .mdb) but the provider is a little different too:

objConnection.Open _
    "Provider = Microsoft.ACE.OLEDB.12.0; " & _
        "Data Source = C:\Scripts\Test.accdb"

As it turns out, Test.mdb is a database that’s leftover from a previous Hey, Scripting Guy! column. But that’s OK: as long as it has a table in it and as long as that table has a field in it, we can use Test.mdb for demonstration purposes. Assuming we have a table named TextFiles (which we do) and assuming that this table has a field named FileName (which it does) we can add an s to the FileName field for every record in the table by using one line of code:

objRecordSet.Open "UPDATE TextFiles SET FileName = FileName & 's'", _
        objConnection, adOpenStatic, adLockOptimistic

Cool, huh? In case you’re wondering, the heart and soul of this line of code is the SQL query itself:

UPDATE TextFiles SET FileName = FileName & 's'

What we’re doing here is using an Update query to update all the records in our recordset. (In this case, of course, that’s going to be every record in the TextFiles table.) And how are we going to update these records? We’re going to do that by setting the value of the FileName field (SET FileName =) to the existing value of the FileName field plus the letter s (FileName & 's’). Notice the syntax there. Because FileName is the name of a field in the table we can include it in the query without having to surround that name with quote marks of any kind. The field name is followed by the ampersand (the standard character for concatenating information); in turn, the ampersand is followed by the value to be appended to the field: the letter s. And because the letter s is a string value, we do need to enclose that in single quote marks.

It’s kind of cumbersome-looking, but it all makes sense.

And believe it or not, that’s all we have to do: no sooner do we issue the query than the letter s will be tacked onto the value of the FileName field for every single record in the table. Talk about easy, huh?

After that there’s nothing left to do except call the Close method and discard the Recordset and Connection objects. At that point, you can go back to work, and we can go back to answering questions. In fact here comes someone now.

Note: Here’s another difference between Access 2007 and previous versions. In Access 2007, the script will run and the database will be changed, but you’ll receive an error when you try to close the recordset. Just remove this line from the script and it will run fine:

objRecordSet.Close

What’s that? Sorry, Peter and Dean aren’t here. No, we’re afraid that Peter and Dean won’t be attending TechEd at all this year. You say it’s too bad that the Scripting Guys sent the second team to TechEd rather than the big stars? Well, we apologize for that, but – hey, wait a minute; we resent that. OK, sure, we do apologize for sending Jean Ross. But Greg Stemp a second-teamer? That hurts ….


How Can I Create a Table in a SQL Server Database?

Hey, Scripting Guy! How can I create a table in a SQL Server database?

-- FD

Hey, FD. And thanks: you’ve saved the Scripting Guys!

Or, at any rate, you’ve saved the Scripting Guy who writes this column. As it turns out, this Scripting Guy is getting ready to leave town and attend his nephew’s wedding. Before he can leave, however, he needs to do two things: 1) he needs to write the next Hey, Scripting Guy! column; and 2) he needs to come up with a wedding gift for the bride and groom. Your question has solved both his problems, and in one fell swoop!

At any rate, FD, here’s a script that can create a table in a SQL Server database. We hope you like it. And Corey and Katie, congratulations; here’s a script that can create a table in a SQL Server database. We hope you like it, too:

strComputer = "atl-sql-01"

Set objConnection = CreateObject("ADODB.Connection")

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=" & strComputer & ";" & _
        "Trusted_Connection=Yes;Initial Catalog=Master"

objConnection.Execute "CREATE TABLE TestTable (UserName TEXT,TotalAmount INTEGER)"

OK, admittedly, there’s not much to this; remember, though, that it’s the thought that counts. (And, personally, we like the thought of a script for creating a table in a SQL Server database that requires only a few lines of code.) As you can see, we start out easy enough: we simply assign the name of the SQL Server computer to a variable named strComputer. Once that’s done we create an instance of the ADODB.Connection object; as you probably already know, ADO (ActiveX Data Objects) is the technology of choice when it comes to writing scripts that interact with databases.

With the Connection object in hand we then use this line of code to connect to the SQL Server service on the computer atl-sql-01 and, in particular, to the Master database on that computer:

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=" & strComputer & ";" & _
        "Trusted_Connection=Yes;Initial Catalog=Master"

Note. Yes, we know: the code says Initial Catalog=Master. Don’t worry about that; that’s just the syntax we need to use. The important thing is, weird syntax or not, this does connect us to the Master database. If you want to connect to a different database (say, your own Employees database) then just modify the code accordinglty: Initial Catalog=Employees.

After we’ve made the connection to the database all we need is one line of code to create a simple two-field table:

objConnection.Execute "CREATE TABLE TestTable (UserName TEXT,TotalAmount INTEGER)"

Let’s talk about this line of code for a moment. What we’re doing here is calling the Execute method to execute a SQL command. And what is that SQL command? This:

"CREATE TABLE TestTable (UserName TEXT,TotalAmount INTEGER)"

You probably don’t need us to tell you that this is the command that creates a new table. And you’re right: there isn’t much to this, either. We simply use the Create Table command followed by:

· TestTable, the name of our new table.

· (UserName TEXT,TotalAmount INTEGER), the field definitions for the new table. For this example our table contains just two fields: a text field named UserName, and an integer field named TotalAmount. Obviously you can add as many fields as your table needs: just specify the field name followed by the field data type, separating individual values (that is, individual field definitions) with commas.

Note. That’s a good question: just what field data types are available to you? For a complete list, take a look at the Transact-SQL Reference on MSDN.

And that’s it: call the Execute method and you’ll have your new table in no time. From that point on, we expect that you’ll live happily ever after.

But that’s a good point: what it you don’t live happily ever after, what if you later come to regret having created this new table? Listen, don’t lose any sleep over that. If push comes to shove, just use the following script (which relies on the Drop Table command) to delete the table from the database:

strComputer = "atl-sql-01"

Set objConnection = CreateObject("ADODB.Connection")

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=" & strComputer & ";" & _
        "Trusted_Connection=Yes;Initial Catalog=Master"

objConnection.Execute "DROP TABLE TestTable"

Easy come, easy go.

At any rate, thanks again for the question, FD, and for the idea about what to get as a wedding gift. Our only concern, of course, is this: what happens if everyone decides to give the happy couple SQL Server scripts?

Oh, well. We’ll just save the receipt and they can take it back if they need to.

Note. Well, sure, we could just give the bride and groom money. But our concern was this: if we give Corey and Katie money then FD might say, “Wait: you gave them money and all I got was a script that creates a table in a SQL Server database?” We’re just trying to make everyone happy.


How Can I Delete a Record From a Disconnected Recordset?

Hey, Scripting Guy! How can I delete a record – or a set of records – from a disconnected recordset?

-- SM

eHy sm.,.

Sorry; let’s try that again:

Hey, SM. We apologize for the poor typing the first time around; the Scripting Guy who writes this column is trying to adjust to typing with just one hand and just one eye. Last night this Scripting Guy and his Scripting Son engaged in a … spirited … game of basketball, a game which ended with up with:

· The Scripting Dad jamming his finger while blocking a shot. (To add insult to injury he did block the shot, but the Scripting Son simply picked the ball up and made the basket anyway.)

· The Scripting Son poking the Scripting Dad in the eye while trying to steal the ball.

· The Scripting Son “accidentally” shoving the Scripting Dad headfirst into a wall.

And that was just in the first 5 minutes. After that the game began to get a little rough.

But do you think a few bumps and bruises would be enough to keep the Scripting Guy who writes this column from, well, writing this column? Well, to tell you the truth it would have. However, after seeing what passes for daytime television these days he decided he might as well come in to work and figure out how to delete a record from a disconnected recordset.

Before we begin we should note that we aren’t going to talk about disconnected recordsets in much detail today; for more information take a peek at the Microsoft Windows 2000 Scripting Guide or at this Scripting Week 2 webcast. For now we’ll just mention the fact that a disconnected recordset is like a database table that exists only in memory; it’s not tied to a real, live database file.

Second, as far as we know, you’re right, SM: you can’t query a disconnected recordset, at least not by using a query like Select * From or Delete * From. As you noted, you tried different variations on these queries and none of them seemed to work. For better or worse, that’s to be expected.

So is that going to be a problem? Let’s put it this way: after you’ve been flung headfirst into a wall, deleting a record from a disconnected recordset hardly seems like a problem at all.

Note. In all fairness, the Scripting Son did not fling his father headfirst into the wall and then simply walk away without a word. Instead he turned to his dad and said, in all sincerity, “You traveled. My ball.”

Of course, had the circumstances been reversed, the Scripting Dad probably would have said the same thing.

Let’s start out by taking a look at a script that creates a simple disconnected recordset and then deletes a record from that recordset. We’ll explain how this sample script works, then show you a modified version that will prove that it really does delete a record from the disconnected recordset.

Here’s the script:

Const adVarChar = 200
Const MaxCharacters = 255
Const adFldIsNullable = 32

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Name", adVarChar, MaxCharacters, adFldIsNullable
DataList.Open

arrItems = Array("A","B","C","D","E","F","G")

For Each strItem in arrItems
    DataList.AddNew
    DataList("Name") = strItem
    DataList.Update
Next

DataList.Filter = "Name = 'B'"

DataList.MoveFirst

Do While Not DataList.EOF
    DataList.Delete
    DataList.MoveNext
Loop

DataList.Filter = ""

Like we said, we don’t have the time to discuss the ins and outs of disconnected recordsets in today’s column. (Besides, you try typing a detailed discussion of disconnected recordsets with just one hand!) Therefore we’ll simply note that the following block of code creates – and then opens – a disconnected recordset consisting of a single field (Name):

Const adVarChar = 200
Const MaxCharacters = 255
Const adFldIsNullable = 32

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Name", adVarChar, MaxCharacters, adFldIsNullable
DataList.Open

Once we have a disconnected recordset our next step is to add some data to the thing. We said this was going to be simple, and we meant it: our recordset is going to consist of the letters A, B, C, D, E, F,and G. In preparation for adding these records to the recordset we first store each of the letters in an array named arrItems:

arrItems = Array("A","B","C","D","E","F","G")

We then set up a For Each loop to loop through each of the items in the array. For each of these items we call the AddNew method to create a new record in the disconnected recordset. We set the value of the Name field to the value of the array item, then call the Update method to write the new record to the recordset. That’s what we do in this block of code:

For Each strItem in arrItems
    DataList.AddNew
    DataList("Name") = strItem
    DataList.Update
Next

In turn, that gives us a disconnected recordset consisting of the following records:

A
B
C
D
E
F
G

Having worked so hard to add these records to the recordset we’re now going to turn right around and delete one of these records.

Good question: how are we going to do that when we can’t even query a disconnected recordset? Well, what we can do is add a Filter to the recordset. This will have the effect of temporarily filtering out all the records in the recordset except those we want to delete. We can then go ahead and delete those records and then remove the filter.

Maybe this will make more sense as we walk you through it. First, take a look at the filter we apply to the recordset:

DataList.Filter = "Name = 'B'"

There’s really nothing magical about this filter; it simply says that, for the moment anyway, we want to work with only those records where the value of the Name field is equal to B. Because we have only one record in the recordset that has the Name B only one record will be deleted; if we had multiple records with the Name B all of those records would be deleted.

Note. Yes, if we had a “fancier” recordset we could apply a fancier filter, one that used an AND clause or an OR clause or, well, pretty much anything short of a Santa clause.

Hey, come on, give us a break: after all, we did get smashed headfirst into a wall last night.

After applying the filter we use the MoveFirst method to move to the first record in our recordset, a recordset which – thanks to the Filter – now consists of just a single record. We then use this block of code to loop through the recordset and delete each and every record in the filtered recordset (which, again, will just be the one record that has the Name B):

Do While Not DataList.EOF
    DataList.Delete
    DataList.MoveNext
Loop

When we’re done we remove the filter by setting the Filter property to an empty string:

DataList.Filter = ""

And there you have it: we’ve deleted a record from a disconnected recordset, despite having jammed our deleting finger.

Of course, in the preceding script there’s no way to verify that the record really did get deleted. Because of that we put together a modified version of the script. In this script we echo back all the records in the recordset before we apply the filter. We apply the filter, delete the record, then remove the filter and echo back the recordset again. If all goes well, record B should have disappeared.

Here’s the code:

Const adVarChar = 200
Const MaxCharacters = 255
Const adFldIsNullable = 32

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Name", adVarChar, MaxCharacters, adFldIsNullable
DataList.Open

colItems = Array("A","B","C","D","E","F","G")

For Each strItem in colItems
    DataList.AddNew
    DataList("Name") = strItem
    DataList.Update
Next

DataList.MoveFirst

Do While Not DataList.EOF
    Wscript.Echo DataList.Fields.Item("Name")
    DataList.MoveNext
Loop

Wscript.Echo

DataList.Filter = "Name = 'B'"

DataList.MoveFirst

Do While Not DataList.EOF
    DataList.Delete
    DataList.MoveNext
Loop

DataList.Filter = ""

Do While Not DataList.EOF
    Wscript.Echo DataList.Fields.Item("Name")
    DataList.MoveNext
Loop

And here’s the output:

A
B
C
D
E
F
G

A
C
D
E
F
G

Hopefully record B is no longer in the recordset. (We can’t verify that ourselves because our left eye is starting to water up again. But we’re pretty sure it worked.)


How Can I Delete a Table from a Jet Database?

Hey, Scripting Guy! Can I delete a table from a Jet database using a script?

-- AK

Hey, AK. Of course you can delete a table from a Jet database using a script; after all, you can do anything using a script!

Well, OK, maybe not DHCP server stuff. And maybe you can’t change the display resolution of your computer. And maybe not – well, let’s just say that when it comes to databases you can do pretty much anything using a script. Including deleting a table.

Let’s take a look at a script that opens a hypothetical database named Inventory.mdb and then deletes a table named HardwareBackup:

Set objCatalog = CreateObject("ADOX.Catalog")
Set objConnection = CreateObject("ADODB.Connection")

objConnection.Open _
    "Provider= Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source=inventory.mdb"

Set objCatalog.ActiveConnection = objConnection
objCatalog.Tables.Delete "HardwareBackup"

objConnection.Close

You’re right: there isn’t much to it. We begin by creating a pair of object references. The first – objCatalog – represents an instance of the ADOX Catalog object. (ADOX stands for ActiveX Data Objects Extensions for Data Definition Language and Security – trying saying that three times fast. Or try saying it once without taking a breath.)

ADOX, by the way, is an extension to ActiveX Data Objects (ADO) that enables you to do things like, well, delete tables.

The second object reference – objConnection – represents an instance of the ADO Connection object; we’ll use this object to open the database Inventory.mdb. That’s what we do here:

objConnection.Open _
    "Provider= Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source=inventory.mdb"

Note that because Inventory.mdb is located in the same folder as our script we didn’t have to specify the entire path.

After we make the connection we assign the objConnection reference to the ActiveConnection property of our Catalog object; this lets the Catalog object know which database to work with. We then use the Delete method of the Tables collection to delete the table named HardwareBackup:

objCatalog.Tables.Delete "HardwareBackup"

That’s all there is to it. We close the connection and go home, another job well done.

Disclaimer. Ok, so maybe your boss won’t let you go home after you run this script. But it never hurts to ask, right?


How Can I Get a List of All the Databases on a SQL Server Computer?

Hey, Scripting Guy! How can I get a list of all the databases on a SQL Server computer?

-- ER

Hey, ER. Before we answer your question, we’d like to take a moment to offer our opinions on all the major – and controversial – issues of the day. We can do that because the Scripting Editor is on vacation and, well, you know what they say: when the cat’s away, the mice will play.

Note. Did we forget to call this a “well-deserved” vacation? No; forgetfulness had nothing to do with us not calling it that.

To tell you the truth, having the opportunity to show off the depth and breadth of his knowledge is important to the Scripting Guy who writes this column. That’s because he has long been typecast as some sort of nerd who cares only about computers. For example, when he worked at the University of Washington he was on his way out for the day when he was asked to stop by the Dean’s office and fix a computer. To do so he set down his book – a collection of short stories by P.G. Wodehouse – on a nearby table.

“P.G. Wodehouse! I just love P.G. Wodehouse!” declared the administrator when she saw the book lying there. “Whose book is this?”

“Mine,” said the future Scripting Guy.

"No, really, whose book is this?” she said. “I’d love to talk to them about it.”

“It’s mine,” insisted the Scripting Guy.

“Never mind,” she said. “I’ll find out later whose book it is.”

As she later explained, "I had no idea you'd ever read anything other than computer books."

Note. True fact: the Scripting Guy who writes this column has never read a book about computers or technology, at least not all the way through. Well, provided you don’t count The Brave Little Toaster, of course.

But now, at long last, the completely un-nerdish Scripting Guy who writes this column has a chance to show off his knowledge of current events. That’s right, current events. All the important things going on in the world today. Important things he knows all about. Yes. All his knowledge and wisdom, all those things he’s been dying to say but has never been allowed to.

Right ….

So, um, that Dred Scott decision of 1857: what was up with that?

You know what? When the Scripting Editor gets back she’ll no doubt go through and delete all these insightful -- albeit controversial -- opinions anyway. It wouldn’t be very nice to have her to come back from vacation and then have to do all that work, doesn't? Therefore, solely in the spirit of making life easier for the Scripting Editor, let’s just show you a script that can list all the databases on a computer running Microsoft SQL Server:

strComputer = "atl-ds-01"

Set objConnection = CreateObject("ADODB.Connection")

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=" & strComputer & ";" & _
        "Trusted_Connection=Yes;Initial Catalog=Master"

Set objRecordset = objConnection.Execute("Select Name From SysDatabases")

If objRecordset.Recordcount = 0 Then
    Wscript.Echo "No databases could be found."
Else
    Do Until objRecordset.EOF
        Wscript.Echo objRecordset.Fields("Name")
        objRecordset.MoveNext
    Loop
End If

So how does this script work? How the heck would we know? What do we look like, a bunch of nerds?

Good point; we probably should get rid of pocket protectors, shouldn’t we? OK, well, in that case, the script starts out by assigning the name of the target computer to a variable named strComputer. We then use this line of code to create an instance of the ADODB.Connection object, the object we need to connect to the SQL Server service on the target computer:

Set objConnection = CreateObject("ADODB.Connection")

As soon we have a Connection object we can then call the Open method to actually connect to SQL Server:

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=" & strComputer & ";" & _
        "Trusted_Connection=Yes;Initial Catalog=Master"

We won't explain SQL Server connections and connection strings in any detail today; however, we will note that we pass the Open method several parameters:

Parameter | Description
Provider | Essentially indicates the type of database we're connecting to. Because we're connecting to SQL Server, we use the SQLOLEDB provider, which is short for "Microsoft OLE DB Provider for SQL Server.” And no, you don’t have to be a nerd to know what SQLOLEDB is short for.
Data Source | Denotes the computer we're connecting to. Here we indicate the computer by using the variable strComputer.
Trusted_Connection | Indicates the user authentication mode. Setting this value to Yes causes us to connect uses Windows Authentication mode, which happens to be the recommended way of connecting to SQL Server.
Initial Catalog | Specifies the default catalog to use when making the connection. We use the Master catalog because that's where we'll find the list of databases.

After opening the Master catalog we can then use the Execute command to return a recordset consisting of all the databases found on this SQL Server machine. To do that we execute the following query, which simply selects the Name of each item (that is, each database) stored in the table SysDatabases:

Set objRecordset = objConnection.Execute("Select Name From ysDatabases")

So what do we do with that recordset once we have it? Well, to begin with, we check the value of the RecordCount which tells us how many items are in the recordset. If the RecordCount is 0, then there aren’t any databases on this computer; in turn, we echo back a statement to that effect.

Of course, the odds are pretty good that the RecordCount won’t be 0, meaning the computer has at least one database. If that’s the case, we then set up a Do Until loop that loops through the entire collection of records (or, as we nerds would put it, until the recordset’s EOF property – the End of File property – is True). Inside that loop we do two things: we echo back the value of the each record's Name field, and we use the MoveNext method to advance from one record to the next:

Wscript.Echo objRecordset.Fields("Name")
objRecordset.MoveNext

Incidentally, don’t forget the MoveNext; if you do, your script will simply echo back the name of the first database in the collection, and then continue to echo that one name forever and ever. That’s because, with databases, you have to explicitly tell them to move on to the next record.

Note. Do we know that through personal experience, like, say, because we left that line out when we first sat down to write the script for today’s column? Well, we – um, sorry, but Microsoft doesn't like us to talk about personal experiences in our columns ....

And what do we get when we’re all done? Why, we get a list of all the databases on the computer, of course:

master
tempdb
model
msdb
pubs
Northwind
CustomerScripts
CommunityScripts

Which is all we ever really wanted to get.

Now, tell us the truth, ER: do you think the Scripting Guy who writes this column is a nerd? (Note: No fair asking the Scripting Son for his opinion of dear old dad.) No, really, we want to know your honest opinion.

Yeah, well, who asked you anyway?

But it really was his book. Really.


How Can I Get a List of the ODBC Drivers that are Installed on a Computer?

Hey, Scripting Guy! How can I get a list of the ODBC drivers that are installed on a computer?

-- VV

Hey, VV. This is one of those questions that end up driving us crazy. That’s because it: a) seems so simple and, b) appears as though there are a number of ways to get this information. For example, on our Windows XP computer we have no fewer than eight WMI classes than include the acronymn ODBC somewhere in the class name. Surely one of those classes will return a list of installed drivers, right?

Wrong. What we’re trying to get at is the information found on the Drivers tab of the ODBC Data Source Administrator dialog box:

But none of the WMI classes seem to return that information, at least not directly. (There might be a convoluted way of getting at this information, but we Scripting Guys try to avoid convoluted ways whenever possible.) All hope seemed lost, but then one of the Scripting Guys decided to apply the Upstairs Closet theory to the problem. In this particular Scripting Home, any time something is lost you can almost bet that it will be found in the upstairs closet, even though the item should have never been thrown into the closet and even though everyone in the house solemnly swears that they didn’t put it there.

Note. Befitting his status as father, this Scripting Guy used to say sarcastically, “Well, it didn’t just walk there itself, did it?” Now, 11 years later, he’s beginning to think that maybe these lost items do just walk there themselves.

The registry, as you doubtless know, is the upstairs closet of the operating system: if you’re willing to look, you can find almost anything in the registry. And, sure enough, that’s where we found the list of installed ODBC drivers, in HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers. (Oddly enough, we also found a pair of shoes, that jacket that we always liked, and a hacksaw that disappeared 5 or 6 years ago.)

As soon as we knew we could get this information out of the registry it was easy to write a script that retrieved a list of all the installed ODBC drivers:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers"
objRegistry.EnumValues HKEY_LOCAL_MACHINE, strKeyPath, arrValueNames, arrValueTypes

For i = 0 to UBound(arrValueNames)
    strValueName = arrValueNames(i)
    objRegistry.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue
    Wscript.Echo arrValueNames(i) & " -- " & strValue
Next

We begin by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; we’ll use this constant to indicate that we want to work with the HKEY_LOCAL_MACHINE registry hive. We connect to the WMI service and to the standard registry provider (root\default:StdRegProv), then use this line of code to assign the registry path within HKEY_LOCAL_MACHINE to the variable strKeyPath:

strKeyPath = "SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers"

As it turns out, the installed ODBC drivers are stored as individual registry values within this registry key, something like this:

Therefore, to retrieve a collection of all these registry values we need to call the EnumValues method, a method which automatically grabs all the values within a specified key:

objRegistry.EnumValues HKEY_LOCAL_MACHINE, strKeyPath, arrValueNames, arrValueTypes

When we call EnumValues we need to supply two in parameters and two out parameters. “In parameters” are values we supply to the method; in this script we pass the constant HKEY_LOCAL_MACHINE and the variable strKeyPath. Taken together, these parameters tell the script which registry key we’re working with.

“Out parameters” represent information that the method provides to us. To get this information all we have to do is provide a pair of variable names. In our script, the variable arrValueNames will end up holding an array of all the registry value names found in SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers; the variable arrValueTypes will hold an array of data types for each of these registry values. (We’re including this particular parameter only because it’s required. Because all our registry values have the same data type – REG_SZ – we don’t need to worry about data types and thus won’t use arrValueTypes.)

After calling EnumValues all the individual registry value names will be stored (as an array) in the variable arrValueNames. To access those values we need to set up a For-Next loop that runs from 0 to the last item (the upper bound or UBound) in the array. (As you know, in VBScript the first item in an array is always item 0.) That’s what this line of code does:

For i = 0 to UBound(arrValueNames)

Inside the loop, we assign the name of the first registry value to a variable named strValueName:

strValueName = arrValueNames(i)

We do that because, at the moment, all we have are the names of the individual registry values. To actually get the values that have been assigned to those registry entries we need to call the GetStringValue method, something we do here:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

As you can see, we pass GetStringValue four parameters:

· HKEY_LOCAL_MACHINE, representing the registry hive.

· strKeyPath, the registry path within HKEY_LOCAL_MACHINE.

· strValueName, representing the individual registry value.

· strValue, an out parameter which will store the value of the registry entry.

After that we can simply use this line of code to echo back the name of the registry value and the value assigned to it:

Wscript.Echo arrValueNames(i) & " -- " & strValue

And when we run the script, we’ll get back output similar to this:

SQL Server -- Installed
Microsoft Access Driver (*.mdb) -- Installed
Microsoft Text Driver (*.txt; *.csv) -- Installed
Microsoft Excel Driver (*.xls) -- Installed
Microsoft dBase Driver (*.dbf) -- Installed
Microsoft Paradox Driver (*.db ) – Installed

Might not have been as straightforward as we would have liked but you know what they say: a list of installed ODBC drivers is a list of installed ODBC drivers. (We’re not sure why they say that, but at least in this case it makes some sense.)


How Can I Retrieve a List of the System DSNs on a Computer?

Hey, Scripting Guy! How can I retrieve a list of the System DSNs on a computer?

-- RT

Hey, RT. You know, there’s an old Hollywood superstition that suggests that famous movie stars always die in threes: if a famous star dies today, then according to legend two more famous stars are doomed to die in the next week as well. We don’t know if that’s true or not, but we know about an eerily-similar scripting corollary that is true: questions about ODBC Data Sources always come in pairs.

Scoff if you will, but you can’t argue with the facts: two weeks ago we answered a question about retrieving the set of ODBC drivers installed on a computer. And now, out of the blue, we get a question about retrieving System DSNs!

Listen, don’t feel bad: that is spooky.

Note. Yes, we know: you thought this thing about System DSNs was just another urban legend, akin to the old scare story about people eating Pop Rocks, drinking a pop, and then having their stomachs explode. Better think again, huh?

If you have no idea what we’re talking about (something which seems to occur more and more often with this column) System DSNs are simply a shortcut method for connecting to databases and other data sources. You can view a list of the System DSNs available on a computer by bringing up the ODBC Data Source Administrator dialog box and looking on the System DSN tab:

That’s fine if you’re working on the local machine. But what if you’re interested in retrieving a list of the System DSNs on a remote machine, or what if you’d like to inventory the System DSNs on a whole bunch of computers? How do you do something like that?

Why, you use a script, of course:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\ODBC\ODBC.INI\ODBC DATA SOURCES"

objRegistry.EnumValues HKEY_LOCAL_MACHINE, strKeyPath, arrValueNames, arrValueTypes

For i = 0 to Ubound(arrValueNames)
    strValueName = arrValueNames(i)
    objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue
    Wscript.Echo strValueName & " -- " & strValue
Next

For some reason there’s no WMI class or other COM object designed to retrieve System DSNs. But that’s OK: because this information is stored in the registry we can still write a script to grab and return the DSNs. As you might expect, that’s exactly what the preceding script does: it opens the registry, zips down to HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\ODBC DATA SOURCES, and then returns the names and values of the all the registry entries found there. Each entry will consist of a name (representing the DSN name) and a value (representing the DSN driver). We’ll return and echo both the name and the value, thus replicating the information found in the dialog box.

Our script begins by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; we’ll use this constant to indicate the registry hive we want to work with. We then bind to the WMI service, connecting to the StdRegProv class. (Which, as we always hasten to add, is found in the root\default namespace, not root\cimv2. In fact, this was the subject of our first column ever.)

Following that, we assign the registry path within HKEY_LOCAL_MACHINE to a variable named strKeyPath. With that done we can then use this line of code to call the EnumValues method and return a list of all the registry values stored in HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\ODBC DATA SOURCES:

objRegistry.EnumValues HKEY_LOCAL_MACHINE, strKeyPath, arrValueNames, arrValueTypes

As you can see, we pass EnumValues four parameters. The first two – HKEY_LOCAL_MACHINE and strKeyPath – are “in parameters” that represent the registry hive and registry path. The second two – arrValueNames and arrValueTypes – are “out parameters;” that means they represent information that the EnumValues method returns to us. After EnumValues runs, arrValueNames will be populated with the names of all the registry values found in HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\ODBC DATA SOURCES; arrValueTypes, meanwhile, will be populated with the registry data type for all those values.

Note. In this particular script we won’t actually use the data types; that’s because all the values will be string values of type REG_SZ.

At this point in time we have the name of each registry entry; if all we wanted to do was echo back the name we’d practically be done. However, we also wanted to echo back the value (that is, the driver name) for each DSN. To do that, we need to connect to each individual registry entry and return the value. And to do that we need to set up a For Next loop that walks through the array of registry entries. For each item in that array we assign the DSN name to a variable named strValueName. We then call the GetStringValue method to return the value assigned to that registry entry:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

In this script, strValue is an out parameter that contains the registry value. We now have the DSN name in one variable (strValueName) and the DSN driver in another variable (strValue). All that’s left is to display that information onscreen:

Wscript.Echo strValueName & " -- " & strValue

When we run the script we should get back information similar to this (depending on the DSNs available on the computer):

Northwind -- SQL Server
Scripting Content -- SQL Server
Events -- Microsoft Access Driver (*.mdb)

Cool, huh? Bear in mind, though, that you should never run this script while eating Pop Rocks. No sense taking any chances, right?

P.S. No need to ask: now you want to know if it’s possible to create and delete System DSNs using a script. Well, for once we’re way ahead of you.


How Can I Save a Disconnected Recordset?

Hey, Scripting Guy! How can I save a disconnected recordset?

-- RW

Hey, RW. You know, the Scripting Guys believe in keeping things as simple as possible. For example, when one of the Scripting Guys had to buy a new tie several years ago, he simply asked his dad to tie it for him; on the rare occasions when this Scripting Guy has to wear a tie he carefully removes the thing at the end of the day and – leaving it knotted – hangs it up and waits for the next time he needs to dress up. Granted, there are probably other ways to deal with this (like, say, maybe learning how to tie his own tie), but this was definitely the simplest way to handle the situation, so that was the path he took.

We took the same approach when it came time to answer your question. There are several ways we could save a disconnected recordset: we could open an existing database and copy the records there; we could use the FileSystemObject to save the data to a text file; we could use the Excel object model and write the information to a spreadsheet. Despite all those wonderful possibilities, though, we decided to keep things as simple and possible and save the disconnected recordset to an XML file, an approach requiring just two lines of code. As we noted, there are other ways to deal with this, but this was definitely the simplest way to handle the situation, so this was the path we took.

One quick note: we aren’t going to talk about disconnected recordsets in today’s column, nor are we going to explain the code for creating a disconnected recordset; we simply don’t have room to do all that. For now, simply understand that a disconnected recordset is like a database table that exists only in memory; it’s considered “disconnected” because it isn’t tied to any real, live database. Disconnected recordsets can be very useful to script writers; for one thing, you can store WMI data into a disconnected recordset and then sort that data on any property value you wish. That’s nice; as you well know, when it comes to WMI data you’re usually at the mercy of whatever sort order WMI happens to use.

Note. So how do you learn more about disconnected recordsets? For starters, take a look at this section of the Microsoft Windows 2000 Scripting Guide.

Here’s a sample script that gets a list of all the services running on a computer and then stores the name and state of each of those services in a disconnected recordset. After we’ve created and populated our recordset we then use the Save method to save the data to an XML file:

Const adPersistXML = 1
Const adVarChar = 200
Const MaxCharacters = 255
Const adFldIsNullable = 32

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "ServiceName", adVarChar, MaxCharacters, adFldIsNullable
DataList.Fields.Append "State", adVarChar, MaxCharacters, adFldIsNullable
DataList.Open

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer& "\root\cimv2")
Set colItems = objWMIService.ExecQuery("SELECT * FROM Win32_Service")
For Each objItem in colItems
    DataList.AddNew
    DataList("ServiceName") = objItem.Name
    DataList("State") = objItem.State
    DataList.Update
Next

DataList.Sort = "ServiceName"

DataList.Save "C:\Scripts\Test.xml", adPersistXML

Don’t worry too much about the size of the script; most of the code here involves creating and populating the disconnected recordset. We’re really interested in only two lines of code: the first line and the last line. In line 1, we define a constant named adPersistXML and set the value to 1; this tells the script that we want to save the data in XML format:

Const adPersistXML = 1

Note. What if we don’t like XML? Could we choose a different format? Well, alternatively we could define a constant named adPersistADGT and set the value to 0; this enables us to save data in the Advanced Data Tablegram format, a proprietary file format used by ActiveX Data Objects (ADO). In keeping with our philosophy, we felt XML would be the easiest format for most people to use.

In the last line of the script we then call the Save method and save the data:

DataList.Save "C:\Scripts\Test.xml", adPersistXML

As you can see, we pass Save a pair of parameters: the complete path to the new XML file, and the constant adPersistXML. It’s that easy.

Note. OK, with one small caution: make sure that the file Test.xml does not exist before calling the Save method. Suppose Test.xml already exists and you run this script; when you call the Save method the script will fail. However, suppose Test.xml does not exist and you run the script. Inside this one script you can call the Save method (thus creating the file) and then, later on in the same script, you can call the Save method again. That’s OK: because the Save method does not close the file you can continue writing to the XML file as long as the file is open. You’ll encounter a “file exists” error only if you close the XML file and then try overwriting it.

Of course, we know what you’re thinking: what the heck am I supposed to do with an XML file? Well, here’s one suggestion: use ADO to later reopen that file. Here’s a very simple script that opens the file Test.xml and then echoes back the contents:

Set DataList = CreateObject("ADOR.Recordset")
DataList.Open "C:\Scripts\Test.xml"

DataList.MoveFirst
Do Until DataList.EOF
    Wscript.Echo DataList.Fields.Item("ServiceName") _
        & " – " & DataList.Fields.Item("State")
    DataList.MoveNext
Loop

Easy, huh? You create an instance of the ADOR.Recordset object and then call the Open method, passing along the path to the XML file you want to open.

With any luck this should take care of your problem, RW; if not please let us know. After all, we’re well aware that sometimes things can go wrong even when you do take the simplest possible route. For example, through a tragic mix-up our Scripting Guy’s tie was recently sent to the cleaners. It came back nice and clean; it also came back untied. Now he has to hope none of his friends or relatives get married or win any prestigious awards until he has had a chance to go back home and get his dad to re-tie his tie for him. (Although, all things considered, that still seems way simpler than trying to learn how to tie a tie.)


How Can I Save Each Paragraph in a Word Document as a Database Record?

Hey, Scripting Guy! A few days ago you showed us how to open a bunch of text files and save the data in each file as a record in a database. I have a similar question, except that I want to save each paragraph in a Word document as a database record. How can I do that?

-- HY

Hey, HY. Before we tackle today’s question we know a lot of you are concerned about Microsoft’s recent purchase of online advertising giant aQuantive, Inc., and whether that will affect the Script Center in any way. Rest assured that there is nothing for you to worry about. In fact, as the good folks at Contoso Cola always say, “Don’t worry; drink Contoso.”

Contoso Cola. One sip and you’ll think, “This is a cola?”

In other words, no, the fact that Microsoft spent $6 billion on an online advertising company will have no effect on the Script Center whatsoever.

This 30-second Script Center update was brought to you by Tailspin Toys. Tailspin. Because anything else is just a toy.

In case you’re wondering, according to the press release this acquisition will definitely enhance the “broader advertising ecosystem.” Now, to tell you the truth, the Scripting Guys have no idea what is meant by the “broader advertising ecosystem.” We’re hoping, however, that this means “writing a script that can save each paragraph in a Word document as a database record.” If that’s the case, then the Scripting Guys can help to enhance the broader advertising ecosystem, too:

Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb"

objRecordSet.Open "SELECT * FROM WordParagraphs" , _
    objConnection, adOpenStatic, adLockOptimistic

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set objSelection = objWord.Selection

For i = 1 to objDoc.Paragraphs.Count
    objDoc.Paragraphs(i).Range.Select
    If Len(objSelection.Text) > 1 Then
        objRecordSet.AddNew
        objRecordSet("ParagraphNumber") = i
        objRecordSet("ParagraphText") = objSelection.Text
        objRecordSet.Update
    End If
Next

Thanks to the peace of mind that comes when you’re insured by Humongous Insurance, we should be able to take a few minutes to see if we can figure out how this script works. To begin with, we have a very simple little Word document (C:\Scripts\Test.doc), a document that looks like this:

We also have a very simple little Access database (C:\Scripts\Test.mdb). This database includes a table named WordParagraphs; that table contains two fields:

· ParagraphNumber, a field we’ll use to keep track of paragraphs by number (i.e, the first paragraph in the document is number 1, the second paragraph is number 2, etc.).

· ParagraphText, a memo field where we’ll stash the text from each paragraph.

OK, let’s roll up our sleeves and get down to business, the same outstanding business that Woodgrove Bank has provided for the past 50 years. Woodgrove Bank: We treat your money as if it was our money.

We start out by defining a constant named adLockOptimistic and setting the value to 3; we’ll use this constant to tell the database to lock a record only when that record is being updated. We then use the following block of code to create instances of the ADODB.Connection and ADODB.Recordset objects, and to open the datatase C:\Scripts\Test.mdb:

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb"

And then we use this line of code to grab a recordset consisting of all the records in the table WordParagraphs:

objRecordSet.Open "SELECT * FROM WordParagraphs" , _
    objConnection, adOpenStatic, adLockOptimistic

Note. Yes, we did kind of zip through all that database stuff, didn’t we? If you could use a little more background information on working with databases you might want to take a peek at the Scripting Guys webcast on that very subject.

It’s now time to shift gears a little bit and turn our attention to Microsoft Word. (Having trouble shifting gears? Then maybe it’s time to call Margie’s Travel at 555-1283.) To begin with, we use these two lines of code to create an instance of the Word.Application object and to make that instance visible on screen:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Once Word is up and running we use the Open method to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

After that we create an instance of the Word.Selection object, an object we’ll need in order to grab the text from each paragraph. At this point we’re ready to have some real fun, the same kind of fun offered each and every day at the Alpine Ski House.

As it turns out, every Word document contains a Paragraphs collection; as the name implies, this is a collection of all the paragraphs found in the document. If we want to get information about each paragraph (which we do) then we need to set up a For Next loop that runs from 1 to the total number of paragraphs in the document (something we can determine by using the Paragraphs collection’s Count property). Or, to put it a bit more programmatically:

For i = 1 to objDoc.Paragraphs.Count

What do we do inside the loop? Well to begin with, we’re going to select the very first paragraph; that’s what this line of code does:

objDoc.Paragraphs(i).Range.Select

By calling the Select method against the paragraph range, we end up selecting the entire paragraph and automatically storing it in our object reference objSelection. That means that we can then check to see how many characters are in the Text property of objSelection:

If Len(objSelection.Text) > 1 Then

Why do we do that? Well, if you look back at our Word document you’ll see that we have blank lines between each paragraph. However, these lines aren’t truly blank, at least not in the sense that there’s absolutely nothing there. Instead, each “blank” line contains one of Word’s paragraph marks. What we’re doing with this line of code is checking to see if the paragraph text has more than one character (the Len function, of course, tells us the number of characters in a string). If the Text length is equal to 1 we’re going to assume that this is a blank line and simply skip it; we’ll only save the paragraph to the database if the Text has more than 1 character.

Note. This is probably a good time to note that this script works only with straight text documents. If you have a bunch of tables in your document we can’t guarantee that this script will work. It might work, but, then again, it might not.

Let’s assume that we have a “real” paragraph here, a paragraph containing more than a single character. In that case we execute this block of code:

objRecordSet.AddNew
objRecordSet("ParagraphNumber") = i
objRecordSet("ParagraphText") = objSelection.Text
objRecordSet.Update

As you probably figured out, this is the code that saves the paragraph text as a record in the database. To do that we start out by creating a new, blank record; that’s why we call the AddNew method. We then assign the value of our counter variable i to the ParagraphNumber field and the paragraph text (objSelection.Text) to the ParagraphText field. With the values assigned we then call the Update method to actually write the new record to the database.

From there we loop around and repeat the process with the next paragraph in the collection.

That should do the trick, HY. If it doesn’t, feel free to email use courtesy of the Fabrikam Furniture Script Center. Fabrikam: The most trusted name in home furniture is now the most trusted name in system administration scripting.

Note. OK, don’t send any angry emails to Microsoft. We made this all up; we have not sold out and become the Fabrikam Furniture Script Center.

Although we are willing to listen to offers ….


How Can I Tell Whether a Specified Table Exists in a SQL Server Database?

Hey, Scripting Guy! How can I tell whether a specified table exists in a SQL Server database?

-- LL

Hey, LL. Well, as it turns out it’s absolutely impossible to tell whether a specified table exists in a SQL Server database. So, sorry, time to go back to watching the Major League Baseball playoffs. By the way, who else out there hates the Dodgers? Don’t be shy; raise your hands.

Hmmm …. OK, if you say so. Um, we’ve just been informed of two things. First, Microsoft is paying us to answer scripting questions, not to watch the Major League Baseball playoffs. Second, those of us here at Microsoft don’t hate the Dodgers; in fact, we love everyone and everything.

Well, OK, maybe not the Oakland Raiders. But everyone else.

To tell you the truth, answering scripting questions isn’t quite as much fun as watching baseball. (It’s close, but ….) On top of that, if you saw our last paycheck you might not think that Microsoft is paying us at all, let alone paying us to answer scripting questions. Still, duty calls, right? With that in mind, here’s a script that can tell you whether a database named Fabrikam (located on the computer atl-sql-01) contains a table named EmployeeData:

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=atl-sql-01;" & _
        "Trusted_Connection=Yes;Initial Catalog=Fabrikam"

objRecordSet.Open "SELECT * FROM sysobjects Where Name= 'EmployeeData' AND xType= 'U'", _
        objConnection, adOpenStatic, adLockOptimistic

If objRecordset.RecordCount = 0 Then
    Wscript.Echo "The table is not in the database."
Else
    Wscript.Echo "The table is in the database."
End If

Before we begin we should note that we won’t be able to explain all the intricacies of connecting to databases in today’s column; that’s a bit more than we have room for. (Or time for: the Mets-Dodgers game starts in less than 20 minutes.) If you’re new to writing database scripts then we encourage you to check out our on-demand webcast Database Scripting for System Administrators. (OK, not one of our more inspired titles. But we were probably distracted by a baseball game or something at the time.)

As for our sample script, we start out by defining a pair of constants, adOpenStatic and adLockOptimistic; we’ll use this constants to assign the cursor and lock types when retrieving records from the database. (Like we said, take a peek at the webcast if you aren’t familiar with terms like cursor and lock types.) After creating instances of the ADODB.Connection and ADODB.Recordset objects we then use this line of code to connect to SQL Server on the computer atl-sql-01:

objConnection.Open _
    "Provider=SQLOLEDB;Data Source=atl-sql-01;" & _
        "Trusted_Connection=Yes;Initial Catalog=Fabrikam"

And yes, as a matter of fact it is easy to modify this script for your own use. Just replace atl-sql-01 with the name of the SQL Server machine you want to connect to, then replace Fabrikam with the name of the database you want to check. (Don’t be confused by the label Initial Catalog; we’re really just talking about a database here.)

What do you know: Nick Swisher just led off the top of the ninth with a double.

Uh, or so we heard someone mention as they walked by. Too bad those people aren’t busy working like the rest of us.

After we’ve made the connection to the SQL Server computer we then issue the following command in order to return a recordset consisting of all the tables in the Fabrikam database that have the name EmployeeData:

objRecordSet.Open "SELECT * FROM sysobjects Where Name= 'EmployeeData' AND xType= 'U'", _
        objConnection, adOpenStatic, adLockOptimistic

What do you mean you don’t see how we did that? Why it’s as plain as the nose on your – oh, maybe it’s not so plain after all, is it? Well, let’s see what we can do to help clear things up. To begin with, all SQL Server databases have a table named sysobjects. Sysobjects has one function: it keeps track of all the objects (tables, logs, constraints, stored procedures, etc.) that have been created within the database. As you can probably guess, that’s why we query the sysobjects table: if anyone knows whether or not a table exists sysobjects does. We then include the Where clause Where Name = 'EmployeeData' for obvious reasons: that’s the name of the table we’re interested in.

That leaves us with this additional clause:

AND xType= 'U'

As it turns out, sysobjects includes a field named xType. This field is used to keep track of the object type; a user table has an xType of U (system tables have an xType of S). This AND clause leaves us with a query that says, “Bring me back a collection of all the objects in the database that not only have the Name EmployeeData but also (AND) happen to be a user table.”

To determine whether or not the database really does contain a user table named EmployeeData all we have to do is take a look at the recordset’s RecordCount property, a property which – logically enough – tells us how many records are in the recordset. RecordCount can be only one of two values: 0, meaning no such tables exist; or 1, meaning that the EmployeeData table does exist. (The RecordCount can never be greater than 1 simply because table names must be unique. That means you can only have, at most, one table named EmployeeData.) In this block of code we examine the value of the RecordCount property and then echo back the appropriate message:

If objRecordset.RecordCount = 0 Then
    Wscript.Echo "The table is not in the database."
Else
    Wscript.Echo "The table is in the database."
End If

Whoa, 5-2 Oakland, bottom of the ninth. That is, for those of you interested in that sort of thing.

Editor’s Note: Update, Oakland wins 5-2. Go Dodgers.


Can I Lock a Workstation Using a Script?

Hey, Scripting Guy! I know how I can shut down a computer using a script, and I even know how I can log a user off using a script. But is there any way I can lock a workstation using a script?

-- TO-R

Hey, TO-R. Most likely you’re familiar with the Win32Shutdown method found in the WMI class Win32_OperatingSystem; that method allows you to shutdown or reboot a computer, or log off the current user. For example, this script shuts down a computer:

Const SHUTDOWN = 1

strComputer = "."
Set objWMIService = GetObject_
    ("winmgmts:{impersonationLevel=impersonate,(Shutdown)}\\" & _
        strComputer & "\root\cimv2")

Set colOperating Systems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

For Each objOperatingSystem in colOperatingSystems
    objOperatingSystem.Win32Shutdown(SHUTDOWN)
Next

What the Win32Shutdown method won’t let you do is lock a computer; as near as we can tell nothing within WMI allows you to lock a computer. In fact, the only way we could figure out how to lock a computer using a script was to write a script that did nothing more than use Rundll32.exe to call the LockWorkStation method of user32.dll:

On Error Resume Next

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "%windir%\System32\rundll32.exe user32.dll,LockWorkStation"

This works great for the local computer, but doesn’t do you much good if you’re trying to lock a remote computer; you can’t specify a remote computer name when running Rundll32.exe. But that’s OK: if this script only runs locally, then all we have to do is make sure it runs locally on the remote computer. Can we do such a thing? You bet we can.

First, save the preceding script – the one that actually locks the computer – as Lock_workstation.vbs and – for simplicity’s sake – save it to C:\Scripts. Next, create a second script – give it any name you want – and paste this code into it:

Const OverwriteExisting = TRUE

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.CopyFile "C:\Scripts\lock_workstation.vbs", _
    "\\atl-ws-01\C$\Scripts\", OverWriteExisting

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")

Error = objWMIService.Create _
    ("cscript c:\scripts\lock_workstation.vbs", null, null, _
        intProcessID)

What does this code do? Well, to begin with, it copies our first script – C:\Scripts\Lock_workstation.vbs – to the C:\Scripts folder on the remote computer atl-ws-01. Two things to keep in mind here. First, the script assumes that you haven’t disabled the administrative share (C$) on the remote computer; second, it assumes that the folder C:\Scripts exists in the remote computer. (Of course, there’s no reason why the file has to be copied to C:\Scripts; you can copy it to any folder you want.)

After the first part of the script executes, the file Lock_workstation.vbs will be sitting in the C:\Scripts folder on the remote computer. In the second part of the script, we connect to the WMI service on the remote computer, and then call the Create method (which happens to be part of the Win32_Process class). And what exactly do we do with the Create method? Well, we simply run the script Lock_workstation.vbs. What’s cool about that, however, is that the Create method will cause the script to run on the remote machine. And because the script is running on the remote machine, Rundll32.exe will work, and the remote computer will be locked. Give it a try, and you’ll see for yourself.

Note. One caveat: this script runs under the security context of the user logged on the remote computer. If that user doesn’t have the right to lock the computer, then the script will fail.

Of course, this is bound to raise at least two questions. Let’s take the easy one first: can you write a script that unlocks a workstation? As far as we know, you can’t; in fact, as far as we know the only way to unlock a workstation is to have someone go to the computer, press Ctrl-Alt-Delete and log on. On the bright side, though, you can still run scripts against a locked workstation; after all, a locked computer is still up and running.

The second question is a bit trickier: is there any way to tell whether or not a computer actually is locked? Well, there’s no foolproof way, at least not that we’re aware of. But here’s a script that can work, as long as:

1. The remote computer is configured to use a screensaver.

2. The workstation is automatically locked any time the screensaver is running.

3. The “timeout” period has elapsed and the screensaver is actually running. (For example, by default the screensaver doesn’t start up until a computer has been inactive – or locked – for ten minutes.)

If all the above are true, this script can tell you if the computer is locked; it does that by grabbing the executable file name for the screen saver, and then checking to see if that executable file is running (by looking at instances of the Win32_Process class). If the screensaver is running then, by definition, the computer must be locked:

On Error Resume Next
HKEY_CURRENT_USER = &H80000001

strComputer = "crowtrobot2"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Control Panel\Desktop"
ValueName = "ScrnSave.exe"
objWMIService.GetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue
strValue = Replace(strValue, "\", "\\")

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where ExecutablePath ='" & strValue & "'")

If colItems.Count > 0 Then
    Wscript.Echo "The computer is locked."
Else
    Wscript.Echo "The computer is not locked."
End If

Note that this script isn’t quite as complicated as it looks; the one weird part is this line of code:

strValue = Replace(strValue, "\", "\\")

That’s needed because the ExecutablePath will be something like this:

C:\WINDOWS\System32\logon.scr

However, when you use a path in a WMI query, you have to double up all your slashes, like so:

C:\\WINDOWS\\System32\\logon.scr

That’s what the Replace function is doing: replacing each \ with \\.

Like we said, it’s not exactly foolproof, but it’s the best we’ve been able to come up with so far. If you wanted to make it a little bit more bulletproof, you could add code that ensures that the computer is actually using a screensaver (ScreenSaverActive) and that the workstation automatically locks any time the screensaver is running (ScreenSaverSecure). If you wanted to get really fancy, you could even determine the screensaver timeout value (ScreenSaverTimeout), pause the script for that amount of time, and then check to see if the screensaver is running.

Of course, even that wouldn’t suffice in all cases: after all, this code won’t distinguish between a computer that’s locked and a computer where no one is logged on; it just tells us that the screensaver is running. Is there a way to tell whether or not any one is logged on to a computer? We’re not really sure, to tell you the truth, but that’s something we’re looking into.


How Can I Change the Caption for the My Computer Icon?

Hey, Scripting Guy! Instead of having the My Computer icon on the desktop read My Computer, I’d like to replace that with the name of the computer. Can I do that using a script?

-- TG

Hey, TG. We get asked this question quite a bit; apparently a lot of you find it convenient to have the computer name right on the desktop, making it easier for users to know which computer they’re logged on to. If you’d like to automate the process of changing the caption for the My Computer icon, all you need to do is run a script like this one:

Const MY_COMPUTER = &H11&

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(MY_COMPUTER)
Set objFolderItem = objFolder.Self
objFolderItem.Name = strComputer

This script begins my declaring a constant named MY_COMPUTER and setting the value to &H11&; this is the value used by the Windows Shell object to locate My Computer. We then use these two lines of code to determine the name of the local computer and store that name in the variable strComputer:

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName

So far, so good, right? Next, we create an instance of the Shell Application object, and use the Namespace method to bind to the special folder that corresponds to My Computer. We then bind to My Computer’s FolderItem object using this line of code:

Set objFolderItem = objFolder.Self

After we bind to the FolderItem, we have access to folder properties like, well, the folder’s name. To set the caption for My Computer to the name of the computer we simply set the name property to strComputer, the variable containing the computer name:

objFolderItem.Name = strComputer

Just like that, the caption will change, not only for the desktop icon, but also for the My Computer link in the Start menu.

If you decide later on to change the caption back to My Computer, you can always run this script:

Const MY_COMPUTER = &H11&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(MY_COMPUTER)
Set objFolderItem = objFolder.Self
objFolderItem.Name = "My Computer"

Remember, this does not change the name of your computer, it only changes the caption for the My Computer special folder. Renaming a computer is whole ‘nother story, one we’ll have to deal with some other time. You might note, too that even though we change the caption from My Computer to, say, atl-ws-01, our Shell Application scripts can still find the My Computer special folder without any problem. That’s because the scripts don’t locate My Computer using the string value My Computer, but instead locate the special folder using the constant &H11&.


How Can I Change the Description for a Computer?

Hey, Scripting Guy! When I use Windows Explorer to connect to a remote computer, I can see a description of that computer in the Details pane. How can I change the description for a computer?

-- GF

Hey, GF. Just to make sure everyone is clear what we’re talking about, we are not talking about the Description attribute in Active Directory; instead, we’re talking about the computer description that gets broadcast across the network. (If you’d rather know how to change the Description attribute in Active Directory, see this Hey, Scripting Guy! column.)

For example, in Windows XP you can get to the computer description by right-clicking My Computer, clicking Properties, and then looking on the Computer Name tab in the System Properties dialog box:

And, as you noted, if you connect to this computer using Windows Explorer, the description will appear in the Details pane as well:

We thought it was pretty exciting, too.

So how can you change the description for a computer? Well, you could open up Regedit.exe and manually change the registry value HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\lanmanserver\parameters\srvcomment. Or, you could just run a script like this one:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject _
    ("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "System\CurrentControlSet\Services\lanmanserver\parameters"
strValueName = "srvcomment"
strDescription = "Description changed programmatically"

objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strDescription

Of course it’s easy; after all this time did you really think we’d suddenly start giving you complicated and convoluted answers to your questions? We begin by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; in a minute we’ll use this constant to tell the script which registry hive we want to work with. We then connect to the WMI service (in this case on the local computer, though we can just as easily modify the registry on a remote machine) and bind to the StdRegProv class. (Which, as we never tire of telling people, happens to be found in the root\default namespace.)

Next we assign values to three variables:

strKeyPath = "System\CurrentControlSet\Services\lanmanserver\parameters"
strValueName = "srvcomment"
strDescription = "Description changed programmtically"

The variable strKeyPath represents the path within the HKEY_LOCAL_MACHINE portion of the registry; strValueName represents the registry value (srvcomment) we’re about to change; and strDescription – that’s right: strDescription represents the new computer description. That’s a very astute observation.

Note. We’d tell you that you guys are way better at this stuff than we are, but we don’t want our manager to get any ideas. And yes: getting an idea would be a first for a Microsoft manager!

All we have to do now is call the SetStringValue method, passing HKEY_LOCAL_MACHINE and our three variables as the method parameters:

objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strDescription

Scripts like that really do make life worth living, don’t they?

Note. Be forewarned that even though this change is made in the registry the new description might not take effect until the computer has rebooted. Just something to keep in mind.


How Can I Change the Desktop Wallpaper to One of Six Pre-defined Selections?

Hey, Scripting Guy! How can I change the desktop wallpaper to one of six pre-defined selections?

-- SB

Hey, SB. You know, when the Scripting Guy who writes this column first sat down to tackle this problem he thought he had a really cool solution for you. Recently this Scripting Guy read about Johannes Trithemius, a Benedictine monk from the 1400s who was one of the fathers of modern cryptography. That’s all well and good, but what really caught the Scripting Guy’s attention was Johannes’ system for communicating by having angels pass messages from one person to another. Imagine how cool that would be: someone wants to change their desktop wallpaper, and an angel comes down from heaven and makes the change for them. Talk about making an impression on your users, eh?

Unfortunately, though, the Scripting Guy who writes this column could never quite figure out how to conjure up a heavenly angel. On two separate occasions he did manage to conjure up outfielders from the Los Angeles Angels of Anaheim, but neither of them would agree to go out and change people’s wallpaper. Of course, that could be due to the fact that the Scripting Guy who writes this column never actually asked them about changing the wallpaper, but instead spent the whole time making fun of their team name.

Still, he’s a little surprised they wouldn’t do it. After all, they’ll have plenty of spare time in October, when other teams are in the playoffs.

Note. Why the slam on the Angels, and totally out of nowhere? No reason, really, other than the fact that the team keeps changing its name, and the Script Guy who writes this column gets dizzy trying to keep up with them. After all, first they were the Los Angeles Angels, then they were the California Angels, then the Anaheim Angels, now the Los Angeles Angels of Anaheim. The Scripting Guy who writes this column figured he could go ahead and make fun of them because, by the time this column is published, the team will likely have changed its name again and none of the players will realize he’s talking about them.

Regardless, the Scripting Guy who writes this column was unable to get angels of any kind to change the wallpaper. Instead, the best he could come up with was a script that could do this:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

intLowNumber = 1
intHighNumber = 6

Randomize

intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

Select Case intNumber
    Case 1
        strValue = "C:\WINDOWS\System32\Wallpaper1.bmp"
    Case 2
        strValue = "C:\WINDOWS\System32\Wallpaper2.bmp"
    Case 3
        strValue = "C:\WINDOWS\System32\Wallpaper3.bmp"
    Case 4
        strValue = "C:\WINDOWS\System32\Wallpaper4.bmp"
    Case 5
        strValue = "C:\WINDOWS\System32\Wallpaper5.bmp"
    Case 6
        strValue = "C:\WINDOWS\System32\Wallpaper6.bmp"
End Select

strKeyPath = "Control Panel\Desktop"
ValueName = "Wallpaper"

objReg.SetStringValue HKEY_USERS, strKeyPath, ValueName, strValue

One quick note before we explain how this script works. It’s actually quite easy to change the wallpaper: you just assign the wallpaper path to a value in the registry (HKEY_CURRENT_USER\Control Panel\Desktop\Wallpaper). The only problem is that the change does not take place immediately; in fact, it doesn’t take place until the user logs off and then logs back on. (Why not? Because, without downloading a third-party utility from somewhere, there’s no straightforward and reliable way to refresh desktop settings using a script.) That doesn’t mean that you can’t change the wallpaper, it just means that you can’t make dynamic changes during the current logon session. Consequently, you might want to assign this script as a logoff script. That way, when the user logs off the script will run and the wallpaper will change. In turn, the change will take effect the next time the user logs on.

At any rate, let’s see if we can figure out how the script works. We start out by defining a constant named HKEY_CURRENT_USER and assigning it the value &H80000001; we’ll use this constant to tell the script which registry hive to work with. We then connect to the WMI service on the local computer (although we could also perform this task remotely), taking care to bind to the root\default namespace:

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Now it’s time to randomly select one of our 6 wallpapers. Admittedly, generating random numbers in VBScript can be a little confusing. Our advice? Don’t worry too much about the details. Just follow the basic template we’ve used here and you’ll be fine.

Note: The Scripting Guy who writes the Sesame Script column, which apparently the Scripting Guy who writes the Hey, Scripting Guy! column never reads, would tell you that if you really want to learn about generating random numbers, take a look at the Sesame Script column Working with Numbers in VBScript. And if you want to see a much more fun (or at least a little more fun) use of random numbers than simply assigning screen savers, take a look at the Dr. Scripto Concentration Game.

With that in mind we start out by assigning values to two variables, variables which represent the high and low numbers in our number range. Because we have 6 wallpapers to choose from that means we want our numbers to range from 1 to 6; hence we assign those values (1 and 6) to our variables:

intLowNumber = 1
intHighNumber = 6

Next we call the Randomize function, a function that “seeds” the random number generator with the current system time. Is that important? You bet it is: if you don’t use the Randomize function the script will simply generate the same “random” number each time it runs. We then use this line of code and the Rnd function to randomly select a number between 1 and 6:

intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

Note. Again, don’t worry too much about how this works. If you want to generate random numbers just use the preceding code as a template, and change the values of intHighNumber and intLowNumber as needed.

At this point the variable intNumber contains one of the following values: 1, 2, 3, 4, 5, or 6. To convert this random number into desktop wallpaper we use the following Select Case statement, in which we assign the variable strValue a different wallpaper path based on the value of intNumber:

Select Case intNumber
    Case 1
        strValue = "C:\WINDOWS\System32\Wallpaper1.bmp"
    Case 2
        strValue = "C:\WINDOWS\System32\Wallpaper2.bmp"
    Case 3
        strValue = "C:\WINDOWS\System32\Wallpaper3.bmp"
    Case 4
        strValue = "C:\WINDOWS\System32\Wallpaper4.bmp"
    Case 5
        strValue = "C:\WINDOWS\System32\Wallpaper5.bmp"
    Case 6
        strValue = "C:\WINDOWS\System32\Wallpaper6.bmp"
End Select

See how that works? If intNumber is equal to 1 the variable strValue gets assigned the path C:\WINDOWS\System32\Wallpaper1.bmp. If intNumber is equal to 2 the variable strValue gets assigned the path C:\WINDOWS\System32\Wallpaper2.bmp. And so on.

At this point we’re almost done. After assigning the wallpaper path to strValue we then use these two lines of code to assign values to variables that represent the appropriate registry key within HKEY_CURRENT_USER (Control Panel\Desktop) and the appropriate registry value within that key (Wallpaper):

strKeyPath = "Control Panel\Desktop"
ValueName = "Wallpaper"

We then call the SetStringValue method to write the changes to the registry:

objReg.SetStringValue HKEY_USERS, strKeyPath, ValueName, strValue

That will change the registry although, again, the user won’t actually see the new wallpaper until he or she logs off and logs back on.

As we noted, in addition to trying to find a way to use angels to deliver messages Johannes Trithemius was also interested in more mundane forms of cryptography, particularly steganography, which involves hiding secret messages in plain sight. (For example, you might read the message by taking every other letter of every other word.) That, of course, leads to an interesting question: have the Scripting Guys been including secret messages in the things that they write?

Well, we can’t speak for the other Scripting Guys. But if you can find any kind of message or meaning in a Hey, Scripting Guy! column (let alone a hidden one) we can only assume that you must have made a mistake.


How Can I Change the Legal Warning Message Using a Script?

Hey, Scripting Guy! How can I use a script to change the legal warning message on a computer?

-- RB

Hey, RB. As you all probably know, when you press Ctrl-Alt-Delete to log on to Windows, you’re typically presented with a logon box. You type your user name, domain, and password in the box, click OK, and away you go.

However, Windows includes a provision for displaying a legal message dialog box after you press Ctrl-Alt-Delete but before you’re presented with the logon box. This gives you an opportunity to present users with a legal disclaimer of some kind. If these users click OK and proceed to logon, then they’ve agreed to abide by whatever rules and regulations you specified in your disclaimer. In turn, that gives you at least some legal protection against any kind of … mischief … the user might engage in while logged-on to your computer.

Typically the caption and message text for the legal warning dialog box is controlled using Group Policy; that makes it easy to apply that warning message to every computer in your domain. On the other hand, you might not want to use Group Policy; you might not be able to use Group Policy (after all, Group Policy works only if you’re running Active Directory); or you might want to use different messages for different computers. In cases such as those, scripts might be just what the doctor ordered.

Well, assuming the doctor is Dr. Scripto, of course.

As it turns out, LegalNoticeCaption and LegalNoticeText are a pair of registry values found in the HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon portion of the registry. To enable a legal warning message on a computer, all you have to do is configure values for these two items. And you can do that using a script very similar to this:

Const HKEY_LOCAL_MACHINE = &H80000002
strComputer = "."

Set objReg=GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
strValueName = "LegalNoticeCaption"
strValue = "Fabrikam, Inc. Legal Notice"
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
strValueName = "LegalNoticeText"
strValue = "By logging on to this computer you agree to abide by the
"
strValue = strValue & "computer usage rules and regulations of
Fabrikam, Inc."
strValue = strValue & vbCrLf & vbCrLf
strValue = strValue & "For more information, phone
(425)-555-1289."
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

The preceding script sets the caption (LegalNoticeCaption) of the warning message dialog box to Fabrikam, Inc. Legal Notice. It then sets the actual message itself (LegalNoticeText) to this:

By logging on to this computer you agree to abide by the computer usage rules and regulations of Fabrikam, Inc.

For more information, phone (425)-555-1289.

You might notice in the script that we use several lines of code to configure the message. We don’t necessarily have to do that; we could instead use one sprawling line of code that spanned several screens. Code like that is difficult to read and to modify, however, so we configured our message in several steps as opposed to one big step. To begin with, we set the value of a variable named strValue using this line of code:

strValue = "By logging on to this computer you agree to abide by the "

Now, take a look at the very next line of code in the script:

strValue = strValue & "computer usage rules and regulations of Fabrikam, Inc."

As you can see, we now assign strValue the existing value of strValue (By logging on to this computer you agree to abide by the) plus this: computer usage rules and regulations of Fabrikam, Inc. Our variable strValue now equals to this:

By logging on to this computer you agree to abide by the computer usage rules and regulations of Fabrikam, Inc.

We then tack on a couple of carriage return-linefeeds (using the VBScript constant vbCrLf) and then the last line of the message. With the entire message now stashed in the variable strName, we’re ready to call the SetStringValue method and write this new value to the registry.

Now, what if you change your mind and decide you don’t want to use a warning message after all? If that happens just use a script and set both LegalNoticeCaption and LegalNoticeText to empty strings. Thus:

Const HKEY_LOCAL_MACHINE = &H80000002
strComputer = "."

Set objReg=GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
strValueName = "LegalNoticeCaption"
strValue = ""
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
strValueName = "LegalNoticeText"
strValue = ""
objReg.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

By the way, if you’re interested in learning a bit more about manipulating registry values using scripts, watch the Things the Scripting Guys Never Told You webcast.


How Can I Determine the Current Screen Resolution Being Used on a Computer?

Hey, Scripting Guy! How can I determine the current screen resolution being used on a computer? You know, things like 1600x1200 resolution, 16-bit color, that kind of information.

-- FE

Hey, FE. Remember when you were a kid and your mom would have that annoying habit of beginning a discussion by answering the question you didn’t ask? For example, suppose you said, “Mom, can I go over to Billy’s?” Inevitably she would answer, “No, you cannot go down to the lake without an adult present. But you can go over to Billy’s as long as you two stay away from the lake.”

Remember how much fun that was? Well, in case you’ve forgotten, we’ll refresh your memory: “No, you cannot use a script to change the screen resolution on a computer. You can use a script to retrieve information about the current screen resolution, but that information is read-only.”

Oh, and stay away from the lake; after all, you have work to do!

We know you didn’t ask about changing screen resolution using a script, but we felt we’d better mention that lest we get millions of emails asking that very question. (We’ll probably still get millions of emails asking that very question, but at least we tried.) For better or worse (and yes, we agree that this qualifies as “for worse”) there’s no built-in way to change the screen resolution using a script. Sorry.

But if all you want to do is retrieve the current resolution settings the news is a bit better:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_DisplayConfiguration")

For Each objItem in colItems
    Wscript.Echo "Name: " & objItem.DeviceName
    Wscript.Echo "Color depth: " & objItem.BitsPerPel
    Wscript.Echo "Horizontal resolution: " & objItem.PelsWidth
    Wscript.Echo "Vertical resolution: " & objItem.PelsHeight
    Wscript.Echo
Next

Of course it’s a simple little script; the Scripting Guys are simple little guys! All we do is connect to the WMI service and then use this query to retrieve all the instances of the Win32_DisplayConfiguration class:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_DisplayConfiguration")

We then echo back the values of various properties, including DeviceName, BitsPerPel (the color depth), PelsWidth (the horizontal resolution), and PelsHeight (the vertical resolution). In other words, property values similar to this:

Name: RADEON IGP 345M
Color depth: 32
Horizontal resolution: 1024
Vertical resolution: 768

Scripting Guys Trivia. The term pels is short for picture elements, more commonly referred to as pixels. Hey, when you’re a Scripting Guy you’re supposed to know stuff like that!

So there you have it: a way to retrieve the current screen resolution on a computer. We’re glad we could help, but this does not mean you can go anywhere near that lake! (Yes, we are mean, aren’t we? But you’ll thank us for that someday.)


How Can I Determine the Default Buffer Size for the Command Window?

Hey, Scripting Guy! How can I determine the default buffer size for the command window?

-- GO

Hey, GO. You know, some of you might be wondering, “Why a daily scripting column? Why not, say, a daily scripting TV show?” That’s a good question, and there are a number of reasons for that. For one thing, there’s the fact that even Microsoft couldn’t afford the makeup required to get the Scripting Guys ready to appear on TV each day. Perhaps more important, however, is the fact that, to be on live TV, you need to be the kind of person who doesn’t make many mistakes. If you’re not, you end up looking silly. Needless to say, the Scripting Guys would never do anything that might compromise their dignity and sense of professionalism.

Where did all this come from? Well, the other day the Scripting Guy who writes this column was watching basketball on TV when the announcer mentioned someone who had “recently suffered a fatal death.” Admittedly, the Scripting Guy who writes this column is no doctor (something the rest of the world is eternally grateful for). Nevertheless, that left him wondering: if you’re going to suffer death, would it be better to suffer a non-fatal death? A fatal death sounds far more serious.

Not to mention the fact that it makes the speaker sound far sillier.

At any rate, that’s why the Scripting Guys don’t do live TV: on live TV there’s no Undo command enabling them to erase their mistakes. For example, when putting together the script for today’s column the Scripting Guy who writes this column mistyped the command Wscript.Echo. But you’d never know that by looking at the finished version, would you:

HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Console"
strValueName = "HistoryBufferSize"

objRegistry.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

Wscript.Echo dwValue

As it turns out, GO, the default buffer size for the command window (along with a number of other default command window settings) is stored in the registry; to get at this information all we have to do is read the appropriate registry value.

So let’s go ahead and do just that. To begin with, we define a constant named HKEY_CURRENT_USER and set the value to &H80000001; this tells the script which registry hive we want to work with. We then connect to the WMI service on the local computer. And yes, as is usually the case with WMI scripts, we could just as easily perform this task on a remote computer; all we have to do is assign the name of that computer to the variable strComputer:

strComputer = "atl-fs-01"

So far so good, right? After connecting to the WMI service (note that we connect to the root\Default namespace, rather than the root\cimv2 namespace) we then assign values to a pair of variables, strKeyPath and strValueName:

strKeyPath = "Console"
strValueName = "HistoryBufferSize"

We’ll use strKeyPath to indicate the registry path within the HKEY_CURRENT_USER hive; because the registry value we want to read resides in HKEY_CURRENT_USER\Console we assign the value Console to strKeyPath. Meanwhile, we assign the name of the registry value we’re interested in to the variable strValueName. Because we want to read the registry value named HistoryBufferSize we – that’s right, we assign HistoryBufferSize to strValueName.

At this point we’re ready to go ahead and retrieve the value from the registry, something we do using the GetDWORDValue method:

objRegistry.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

As you can see, we simply call GetDWORDValue, passing four parameters: the constant HKEY_CURRENT_USER; the variables strKeyPath and strValueName; and an “out” parameter we named dwValue. An out parameter is simply a variable (of our choosing) that the method will assign a value; in this case that means GetDWORDValue will read HistoryBufferSize and assign that value to dwValue. Once that’s been done all we have to do is echo back the value of dwValue:

Wscript.Echo dwValue

That’s all we have to do.

By default the command history buffer size is set to 50; however, you can modify the buffer size, giving it a value between 1 and 999. How can you modify the buffer size? Well, here’s one way to do it:

HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer
& "\root\default:StdRegProv")

strKeyPath = "Console"
strValueName = "HistoryBufferSize"
dwValue = 250

objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

We won’t discuss this bonus script in any detail, but this little bit of code sets the history buffer size to 250. Would you prefer some other history buffer size? No problem; just change this line accordingly:

dwValue = 250

And there you have it.

By the way, we know that a number of you are already busy composing emails pointing out that, even though this isn’t a live TV show, the Hey, Scripting Guy! column is often riddled with statements that make no sense whatsoever. We’d just like to take this opportunity to note that any silliness or nonsense that might appear in this column is solely the responsibility of the Scripting Editor. We apologize for any inconvenience she might have caused you.


How Can I Determine the Name of the Local Computer Using WMI?

Hey, Scripting Guy! How can I determine the name of the local computer using WMI?

-- BM

Hey, BM. As you noted in your email we Scripting Guys always tell people that, in WMI, you can simply use a dot (.) to refer to the local computer. For example, the following mini-script connects you to the WMI service on the local computer:

strComputer = "."

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

Cool, huh? Of course, as you pointed out that’s really only half the story: yes, we can use a dot to refer to the local computer, but what if we need to know the name of that computer? When it comes to doing that, when it comes to helping you determine the name of the machine that the dot represents, well, in that case the Scripting Guys haven’t been much help.

Note. To be honest, it doesn’t really sound like the Scripting Guys to do half a job and then just plain give up on it. Usually we don’t make it even that far before giving up on something!

Before we go any further we should probably take a moment to address the skeptics in the audience. No doubt some of you are looking at this question and thinking, “So what’s the big deal here? If the script is running on the local computer, well, then why would you even care about the name of the local computer? And if the script is running remotely, well, wouldn’t you already know the name of the computer where the script is running? Wouldn’t you simply set the value of the variable strComputer to the name of that remote machine?”

The answer to all those questions is the same: maybe. But, then again, maybe not. For example, suppose our code is running as part of a logon script or a computer startup script, and suppose we need to log collected data to a database. In one of those scenarios we need to know the name of the computer. Yes, the script will run just fine by using a dot to represent the computer name, but we can’t rely entirely on the dot; if we do, our database will end up looking something like this:

Computer | Number of Processors
. | 1
. | 2
. | 4
. | 1

Cute, but not too terribly informative. Needless to say, a table like this would be a bit more useful:

Computer | Number of Processors
atl-ws-01 | 1
atl-fs-02 | 2
atl-dc-03 | 4
atl-ws-04 | 1

Likewise there might be times when we do need get the computer name when dealing with remote machines. With WMI you can connect to a remote computer by specifying the computer name; however, you can also connect to a remote computer by specifying an IP address:

strComputer = "192.168.1.1"

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

Because very few of us have memorized the IP addresses of all our computers (and because, thanks to DHCP, many of these IP addresses are wont to change anyway) a table like is also only marginally useful:

Computer | Number of Processors
192.168.1.1 | 1
192.168.1.2 | 2
192.168.1.3 | 4
192.168.1.4 | 1

In other words, even in this high-tech day and age of ours something as old-fashioned as a name can still come in handy. (Which sounds funny coming from a group that hides behind the collective moniker “The Scripting Guys,” doesn’t it?)

So can we use WMI to determine the name of the local computer (or, say, a remote machine where all we know is the IP address)? You bet we can. And when we tell you that there are hundreds and hundreds of different ways to do that, well, we aren’t kidding. For example, suppose you have a burning desire to use the Win32_BIOS class to determine the computer name. Hey, why not:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_BIOS")

For Each objItem in colItems
    Wscript.Echo objItem.Path_.Server
Next

As you can see, this is a standard WMI script, one that binds to the WMI service on the local computer. We then use this line of code to retrieve BIOS information for that machine:

Set colItems = objWMIService.ExecQuery("Select * From Win32_BIOS")

Why did we pick the Win32_BIOS class? No reason; we could have picked any WMI class. That’s because each and every WMI class returns an SWbemObjectPath object; the Path_ property for that object serves as the unique address for the item in question. For example, the Path_ to the BIOS on a computer might look something like this:

\\ATL-WS-01\root\cimv2:Win32_BIOS.Name="EPP runtime BIOS - Version 1.1 ",
SoftwareElementID="EPP runtime BIOS - Version 1.1 ",SoftwareElementState=3,
TargetOperatingSystem=0,Version="HP     - 22110520"

That path is unique: no other object on the computer atl-ws-01 will have that same path.

As you can see, the name of the computer (ATL-WS-01) is embedded in the beginning portion of that path. (Which makes sense: not including the name of the computer in the path would be about as useful as having an address of 111 Main Street that didn’t include the name of the city.) Admittedly, we could do some fancy string manipulation in order to try and tease out the computer name from the path. Or, we could simply ask for the value of the Server property, like we do here:

For Each objItem in colItems
    Wscript.Echo objItem.Path_.Server
Next

It’s that easy. And, best of all, any WMI object you get back from a script will include an SWbemObjectPath object and the Server property. Are you already using the Win32_ParallelPort class to connect to a machine? That’s fine; while you’re there you can get the value of the Server property using Win32_ParallelPort:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_ParallelPort")

For Each objItem in colItems
    Wscript.Echo objItem.Path_.Server
Next

Connecting to a machine using the Win32_PrinterConfiguration class? Then you can get at the value of the Server property using Win32_PrinterConfiguration. Connecting to a machine using the Win32_LogicalDisk class? Then you can get at – well, you get the idea.

But what if you don’t like all this talk of SWbemObjectPaths? Well, in that case you can always rely on the Win32_ComputerSystem class to get the computer name:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_ComputerSystem")

For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

That works just fine; it’s just nowhere near as cool as using the Win32_TimeZone class to determine the computer name. And just think of how many bar bets you’re likely to win now; after all, from here on you’ll be able to walk into a crowded bar and tell people, “Give me the name of a WMI class – any WMI class – and I’ll use just that class to determine the name of a computer.” If people aren’t impressed by that then maybe they aren’t worth impressing.

We hope that answers your question, BM. After all, we Scripting Guys take pride in our motto: Leave no job undone. We don’t necessarily live up to that motto, mind you. But we do take pride in it.


How Can I Determine the Size of All the Items Currently in the Recycle Bin?

Hey, Scripting Guy! How can I determine the size of all the items currently in the Recycle Bin?

-- T D-L

Hey, T D-L. You know, if we wanted to, the Scripting Guys could probably change our name to the Recycle Bin Guys; that’s because we get so many questions about scripting and the Recycle Bin. So why haven’t we changed our name? Well, for one thing, we wouldn’t know what to do with all these leftover Scripting Guys T-shirts. Perhaps more important, though, is the fact that we usually don’t have a very good answer when it comes to Recycle Bin questions. Can you send a file to the Recycle Bin rather than deleting it altogether? Well, kind of, but you still have to respond to the “Are you sure you want to send this file to the Recycle Bin?” prompt that appears. How about emptying the Recycle Bin? Same thing: you can do it, but you have to answer the “Are you sure you want to empty the Recycle Bin?” prompt that appears. It’s not very elegant, and it’s anything but fully automated.

However, this question we can help you with. Again, we don’t have the absolute perfect solution; for one thing, the script works only on the local computer. But when it comes to Recycle Bin questions, this is about as good as it gets:

Const
RECYCLE_BIN = &Ha&
Const FILE_SIZE = 3

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(RECYCLE_BIN)

Set colItems = objFolder.Items

For Each objItem in colItems
strSize = objFolder.GetDetailsOf(objItem, FILE_SIZE)
arrSize = Split(strSize, " ")
intSize = intSize + CLng(arrSize(0))
Next

Wscript.Echo intSize & " KB"

Our script starts out by defining a pair of constants. RECYCLE_BIN (which is assigned the hexadecimal value &Ha&) will be used to tell the script which special folder to bind to. Meanwhile, FILE_SIZE (with a value of 3) will be used to tell the script which folder item property we want to retrieve. That might not make much sense to you right now, but keep your fingers crossed and it should all become clear in a minute or two.

With these two constants in tow we create an instance of the Shell.Application object and then use the Namespace method to bind to the Recycle Bin. That’s what takes place here:

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(RECYCLE_BIN)

Now, in a perfect world, once we made a connection to the Recycle Bin we could simply say, “Hey, Recycle Bin: what exactly is the size of all your current items?” Unfortunately, though, this is not a perfect world. (If it was, the Scripting Guy who writes this column would actually be the Scripting Guy who plays centerfield for the Yankees.) Because of the special nature of the Recycle Bin folder (as well as some of the eccentricities of the Shell object) we have to manually calculate the size, something we can do by adding up the size of each and every item in the Recycle Bin. Before we can do that, however, we need to grab a collection of all the items currently in the Recycle Bin. That’s what this line of code is for:

Set colItems = objFolder.Items

As soon as we have our collection we can then set up a For Each loop to walk through all the items (that is, all the items in the Recycle Bin) and add up the file sizes:

For Each objItem in colItems

This is usually about the time where we say, “And from here on the rest of is easy.” But remember, this is the Recycle Bin we’re dealing with: nothing is ever easy when it comes to the Recycle Bin. Once we have a collection of items we can use the GetDetailsOf method to retrieve individual property values for each item in the collection. Because all we want is the file size (which, as far as the Shell object is concerned, has a property value of 3) we can use this line of code to return the file size for each item in the Recycle Bin (incidentally, this includes folders as well as files):

strSize = objFolder.GetDetailsOf(objItem, FILE_SIZE)

On paper, that looks pretty good. But, as you probably guessed, we a have problem: all our file sizes come back like this, with the string KB attached to the end:

100 KB
200 KB
300 KB

Why is that a problem? Well, try adding 100 KB + 200 KB + 300 KB and you’ll see why it’s a problem. (Hint: You’ll get a “Type mismatch” error because you’re trying to add together string values.)

See, that’s another reason we haven’t switched over and become the Recycle Bin Guys. Dealing with the Recycle Bin simply takes too much effort!

Fortunately we can easily take care of the KB tacked onto the end of each file size. How? Well, this line of code uses the VBScript Split function to divide each file size into an array:

arrSize = Split(strSize, " ")

By splitting on the blank space we end up with an array consisting of two items. The first item (with an index number of zero) is the file size itself; the second item is the string KB. For example, if the file size is 100 KB then the array arrSize will consist of these two items:

· 100

· KB

Now, at long last, we’re in business. With the file size safely tucked away in the first item in the array we can use this line of code to start tallying up the sizes of all the items in the Recycle Bin:

intSize = intSize + CLng(arrSize(0))

What we’re doing here is using a variable named intSize to keep track of the total size of all the items. In this line of code we assign intSize the value of whatever is currently in intSize plus the value of the first item in the array (which, as you recall, has an index number of 0).

For example, the first time through the loop intSize will be equal to 0. If our first file size is 100 then intSize will be assigned this value:

0 + 100

The next time through the loop intSize will be equal to 100. If the size of the second file is 200 then intSize will be assigned this value:

100 + 200

If those are the only two files in the Recycle Bin then the size of the Recycle Bin is 300 KB, a fact that we echo after exiting the For Each loop.

Good question: why did we use the CLng function when working with the file size? Well, the CLng function is designed to “cast” a variable as a long integer. We left CLng out of the first iteration of this script only to discover that VBScript still wanted to treat the file size (e.g., 100) as a string variable. Because of that we got back a Recycle Bin size that looked like this:

1,2407,6407,0125,20410,06423,204 KB

Interesting, but not quite the same as the actual size: 54364 KB. By using the CLng function we simply ordered VBScript to treat arrSize(0) as a long integer, and everything worked just fine.

And now it’s off to the batting cages. After all, seeing as how it is possible to write a script that works with the Recycle Bin, well, then maybe it’s also possible that this Scripting Guy can play centerfield for the Yankees. We’ll let you know how that turns out.


How Can I Determine the Size of the My Documents Folder?

Hey, Scripting Guy! How can I determine the size of the My Documents folder?

-- SC

Hey, SC. The My Document folder is actually just another folder on the hard disk, one with a path similar to this: C:\Documents and Settings\kenmyer\My Documents. Of course, the actual path will vary depending on the name of the logged-on user; in addition, the My Documents folder can be redirected to another location, which means it isn’t necessarily in the C:\Documents and Settings folder either. What we’re trying to say is that the tricky part here is actually finding the My Documents folder; once you find it you can then use the FileSystemObject to determine the folder size (and we can do that because My Documents is, as we noted, just another folder on the hard disk).

So how do we find the My Documents folder? Well, there are a couple ways to do this, but we’re going to use the Shell object; that’s because the Shell object can be used to find the path to all sorts of special folders, including My Pictures, My Music, My Network Places, etc. Here’s a script that reports the size of the My Documents folder:

Const MY_DOCUMENTS = &H5&

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("Shell.Application")

Set objFolder = objShell.Namespace(MY_DOCUMENTS)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

Set objFolder = objFSO.GetFolder(strPath)
Wscript.Echo objFolder.Size

We begin by defining a constant named MY_DOCUMENTS and assigning it the value &H5&, the value used by the Shell object to reference the My Documents folder. We then create two separate objects: objFSO (an instance of the FileSystemObject) and objShell (an instance of the Shell object).

Now the fun begins. We use the Namespace method to bind to the My Documents folder and then use the Self property to create an object reference to the FolderItem object that represents My Documents. (We won’t worry about why we have to do that; it’s just something that the Shell object requires.) Finally, we store the value of the Path property (e.g., C:\Documents and Settings\kenmyer\My Documents) into a variable named strPath.

Still with us? Now that we know the path to the My Documents folder we can use the FileSystemObject and the GetFolder method to bind to My Documents. Yes, we already did this once, but that was with the Shell object. Now we have to repeat the process with the FileSystemObject. Why? Well, the Shell object can locate the My Documents folder for us, but can’t report the folder size. The FileSystemObject can report the folder size, but can’t locate the My Documents folder for us. Thus we have to use both objects to complete this single task.

After we bind to My Documents using the FileSystemObject we can then echo the size of the folder using this line of code:

Wscript.Echo objFolder.Size

Granted, not the most straightforward process in the world but at least it returns the size of the My Documents folder. And that’s all we really care about.


How Can I Expand the Width of the Windows PowerShell Console?

Hey, Scripting Guy! How can I expand the width of the Windows PowerShell console? When I display certain kinds of data – like information from the event logs – that data gets cut off because the console window isn’t wide enough.

-- TB

Hey, TB. Wow, what a coincidence: we were just about to ask you that very same question! Now what do we do?

No, just kidding: the Scripting Guys never ask anyone for help. Not that we couldn’t use help, mind you; it’s just that, when you work at Microsoft, you quickly learn never to ask anyone anything.

Is that because they might give you the wrong answer? We wish; after all, a wrong answer is at least an answer. No, ask a Microsoft person a question and, instead of giving you an answer, they’ll decide to set up a meeting to talk about the issue. In turn, they’ll go back to their office, notify their manager and your manager (and all the managers’ managers as well as their managers) of the question, then invite scores of “key stakeholders” to attend this suddenly-crucial meeting.

Which, by the way, will often be scheduled for a ridiculous hour like 6:00 PM. (Who the heck wants to be at work at 6:00 PM?) And, of course, they won’t actually schedule the meeting; instead, they’ll send everyone a “Sched+”, Sched+ being a reference to a Microsoft product (Schedule+) that was last used in 1847. But the memory lingers on.

Note. Good question: what is a key stakeholder? As near as we can tell, if you agree to go to a meeting but then don’t bother to show up you’re a key stakeholder. And because you can’t reach a decision unless the key stakeholders are present, their inevitable absence means that there will always be another meeting after this one. Remember the Greek myth of Sisyphus, doomed for eternity to roll a rock up a hill and then, just before he reaches the top, have the rock roll down over him, forcing him to start all over again? Sisyphus has an office just down the hall from the Scripting Guy who writes this column.

After the meeting has been postponed several times, then moved to several different conference rooms, everyone finally gets together, with the only item on the agenda being this: should we go ahead and schedule another meeting or not? After several such false starts we’ll eventually set up a “virtual team” that assigns “action items” to the key stakeholders. (We use a lot of quotation marks here at Microsoft, don’t we?) We’ll then spend several months drawing up policies and procedures for how to best address the question. And then, just before we’re ready to come up with an answer, we’ll decide that maybe we don’t really “own” the question after all, and agree that someone else should provide the answer.

And yes, let’s hope that your original question wasn’t “Where’s the bathroom?”

But things are very different when you ask the Scripting Guys a question. Granted, they probably won’t give you an answer, either. But at least there won’t be any paperwork or meetings involved:

$pshost = get-host
$pswindow = $pshost.ui.rawui

$newsize = $pswindow.buffersize
$newsize.height = 3000
$newsize.width = 150
$pswindow.buffersize = $newsize

$newsize = $pswindow.windowsize
$newsize.height = 50
$newsize.width = 150
$pswindow.windowsize = $newsize

Before we go much further we should note that you don’t have to use a script to change the size of the Windows PowerShell console window. Instead, you can always do this:

· Start Windows PowerShell.

· Right-click the icon in the upper left corner of the title bar and then click Properties.

· On the Layout tab set the desired width under the Window Size heading

· Click OK.

That works just fine. But you’re right: that is kind of boring, at least for a scripting column. Therefore, we’ve given you a programmatic way to modify the size of the current Windows PowerShell console. And, as an added bonus, before we’re done we’ll show you how to change the window title and window colors as well.

No thanks necessary; turns out that was an action item that the Scripting Guys, as key stakeholders, agreed to do.

As for the script itself, we start out by creating an object reference (named $pshost) to the Windows PowerShell console object; that’s something we do by calling the Get-Host Cmdlet:

$pshost = get-host

We then use this line of code to create a reference to the console’s UI.RawUI child object:

$pswindow = $pshost.ui.rawui

Now we need to take a brief detour. On our test computer both the Windows PowerShell screen width and the buffer size are set to 120. Does that matter? Yes, it does: you can’t have a screen width that’s bigger than the buffer size. Therefore, before we can increase our window size we need to increase the buffer size. That’s what this block of code does:

$newsize = $pswindow.buffersize
$newsize.height = 3000
$newsize.width = 150
$pswindow.buffersize = $newsize

It’s a tad bit confusing, but what we’re really doing here is constructing a brand-new buffer size in memory, then applying the properties of that virtual buffer to the actual console window. To do that we first assign the BufferSize properties to a variable named $newsize:

$newsize = $pswindow.buffersize

After we’ve done that we set the values of the buffer size Height (3000 lines, the default value) and Width (150 characters). We then use this line of code to assign these values to the actual console window:

$pswindow.buffersize = $newsize

With the buffer size adjusted we then repeat the process with the WindowSize property, giving our new window a Height of 50 lines and a Width of 150 characters:

$newsize = $pswindow.windowsize
$newsize.height = 50
$newsize.width = 150
$pswindow.windowsize = $newsize

Run the script and your Windows PowerShell window will instantly resize itself. (Although we should note that the effect is temporary: the window will revert to its previous size should you close this session and restart it. To permanently change the size of the window you can, among other things, use the UI to change the window properties or put this script in your PowerShell profile.)

Now, as promised, here’s some bonus code that will change the title of the console window (to My PowerShell Session) as well as the foreground and background colors (yellow text on a black background). We won’t discuss the whys and wherefores of the bonus code, but you can probably figure it out on your own (hint: see this article for more information on the colors available to you when configuring the PowerShell UI):

$pshost = get-host
$pswindow = $pshost.ui.rawui

$pswindow.windowtitle = "My PowerShell
Session"
$pswindow.foregroundcolor = "Yellow"
$pswindow.backgroundcolor = "Black"

cls

See, what did we tell you? With the Scripting Guys you not only get the answer to your question, but you also get answers to questions you didn’t even ask!

Of course, in all fairness, we should point out that the Scripting Guys tend to exaggerate a little when it comes to discussing life here at Microsoft. Needless to say, questions do get answered around here, all the time.

Or at least that’s what they told us at our last virtual meeting, where we agreed – sometime after the holidays, of course – to get together and try to decide whether or not we should try to make decisions in the future.

Well, not this year, of course, but during the next fiscal year.

We’ll keep you posted.


How Can I List All the Files on a User’s Desktop?

Hey, Scripting Guy! How can I list all the files on a user’s desktop?

-- BT

Hey, BT. So you thought you could fool the Scripting Guys, did you? Well, you did, at least for awhile. As is so often the case with the Scripting Guys we took one look at your question and thought, “Let’s answer this one; this one is easy!” Three tries – and another false start or two – later, we finally came up with an answer for you. So maybe it wasn’t that easy after all (although, for the Scripting Guys, it was fairly typical).

Why did we struggle with this particular question? Well, setting aside the fact that there are days (many of them) in which we struggle with just about everything, we made a couple of misguided assumptions early on. For one, we assumed that connecting to the user’s desktop folder would return us all the information we needed. With that in mind we wrote a script similar to this:

Const DESKTOP = &H10&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(DESKTOP)
Set objFolderItem = objFolder.Self

Set colItems = objFolder.Items
For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

Did this script work? Pretty much, although it also shows you the folders on the desktop as well as the files (we never got around to filtering out folders). Unfortunately, though, it didn’t show us all the files in the user’s desktop. Why not? Well, as you probably know, the items that appear on the desktop are actually drawn from two places: the user’s Desktop folder and the All Users Desktop folder. Our initial try at solving this problem retrieved the files from only the user’s desktop folder.

Well, if at first you don’t succeed, right? And so we added this code to the original script:

Const ALL_USERS_DESKTOP = &H19&

Set objFolder = objShell.Namespace(ALL_USERS_DESKTOP)
Set objFolderItem = objFolder.Self

Set colItems = objFolder.Items
For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

Did that script work? You bet it did: now we could get back a list of all the files that appear on the user’s desktop.

Well, check that: all the files that appear on the local user’s desktop. What if we needed to get this information for a remote machine? That was a problem: we used the Shell object in our script, and the Shell object doesn’t do us much good when working with remote computers.

At that point we were ready to give up and tell you that we could list the desktop files only for the local user, when it occurred to us that you can find the paths to the Desktop and All Users Desktop folders in the registry. That implied that we could read these values from the registry on a remote computer, and then use WMI to return the list of files. Will that really work? Well, it seems to:

Const HKEY_LOCAL_MACHINE = &H80000002
Const HKEY_CURRENT_USER = &H80000001

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

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
strValueName = "Common Desktop"
objRegistry.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strAllUsers

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strAllUsers & "'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In colFileList
    Wscript.Echo objFile.Name
Next

Wscript.Echo

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
strValueName = "Desktop"
objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strUser

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strUser & "'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In colFileList
    Wscript.Echo objFile.Name
Next

Don’t be intimidated by the length of this script: it’s a little longer than most Hey, Scripting Guy! scripts simply because we need to access two different registry keys and then retrieve the list of files from two different folders. As you’re about to see, there’s nothing the least bit complicated about the script.

We start off by defining a pair of constants: HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER. We’ll use these constants to connect to the appropriate registry hives: we use HKEY_LOCAL_MACHINE to get information about the All Users Desktop folder and HKEY_CURRENT_USER to get information about the current user’s Desktop folder. We then use the following two lines of code to connect to the WMI service on the local computer (or, more correctly, whichever computer we assign to the variable strComputer):

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

Our next step is to retrieve the path to the All Users Desktop folder. To do that we make a second WMI connection, this time to the System Registry provider. (Could we have written this script using just one WMI connection? Sure, but it seemed a little easier to follow if we made one connection for retrieving the list of files and a second connection for reading information from the registry). After making the second connection we then assign values to a pair of variables:

· strKeyPath is assigned the path to the appropriate registry key. In this case, that’s Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders.

· strValueName is assigned the name of the actual registry value we want to read (Common Desktop).

That brings us to this line of code:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strAllUsers

Here we’re using the GetStringValue method to read the Common Desktop registry value. We pass GetStringValue four parameters: the constant HKEY_LOCAL_MACHINE; the variables strKeyPath and strValueName; and an “out” parameter named strAllUsers. Notice that we’ve never assigned a value to strAllUsers. That’s because we don’t have to: with an out parameter the method will assign a value to the variable. And, in this case, that value will be the path to the All Users Desktop folder.

Once we have the path to the All Users Desktop folder we can then use the following WMI query to retrieve a collection of all the files found in that folder:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strAllUsers & "'} Where " _
        & "ResultClass = CIM_DataFile")

And once we have that collection we can set up a For Each loop to walk through the list of files, echoing back the local path for each one:

For Each objFile In colFileList
    Wscript.Echo objFile.Name
Next

The net result looks something like this:

c:\documents and settings\all users\desktop\desktop.ini
c:\documents and settings\all users\desktop\security configuration wizard.lnk

All that’s left now is to repeat the process, this time getting the path to and echoing the files in the current user’s Desktop folder. After that, we’re done.

Which means, of course, that we were right all along: this was an easy question to answer. Or at least it was once we finally figured out how to answer it.


How Can I List All the Items in the Run Key in the Registry?

Hey, Scripting Guy! How can I list all the items in the Run key in the registry?

-- JW

Hey, JW. How can you list all the items in the Run key in the registry? Well, to be honest, we’re not going to tell you. Nope, sorry. Not because we don’t like you, but because we think we have a better answer for you. (If we’re wrong, well, let’s just say it won’t be the first time.)

Because you’re interested in the Run key we’re assuming that what you really want to know is how to find out which programs are configured to automatically run each time Windows starts. You can definitely read that information from the Run key, do doubt about that. Well, actually, you can read that information from the Run keys, seeing as how both HKEY_CURRENT_USER and HKEY_LOCAL_MACHINE have Run keys. And they both have RunOnce keys. And then there’s the Startup folder, and the All Users Startup folder, and ….

You get the idea. The problem with trying to figure out which programs are configured to automatically run each time Windows starts is that this information might be stored in any one of a million different places. Could we write a script that checks each and every one of those million places? You bet we could. But we think this is a better way:

strComputer = "."

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

Set colStartupCommands = objWMIService.ExecQuery _
    ("Select * from Win32_StartupCommand")

For Each objStartupCommand in colStartupCommands
    Wscript.Echo "Command: " & objStartupCommand.Command
    Wscript.Echo "Description: " & objStartupCommand.Description
    Wscript.Echo "Location: " & objStartupCommand.Location
    Wscript.Echo "Name: " & objStartupCommand.Name
    Wscript.Echo "User: " & objStartupCommand.User
    Wscript.Echo
Next

Instead of poking around in every nook and cranny trying to find out where Windows keeps information about auto-run programs we’ve decided to let WMI do all that work for us. The WMI class Win32_StartupCommand is designed to ferret out information about auto-run programs, regardless of whether that information is stored in the registry, in a Startup folder, or in some other location. For example, when we run this script we’ll get back information similar to this:

Command: Microsoft Office OneNote 2003 Quick Launch.lnk
Description: Microsoft Office OneNote 2003 Quick Launch
Location: Startup
Name: Microsoft Office OneNote 2003 Quick Launch
User: FABRIKAM\kenmyer

Command: C:\WINDOWS\System32\ctfmon.exe
Description: ctfmon.exe
Location: HKU\S-1-5-21-1987391165-1004336648-1605550848-8553\SOFTWARE\Microsoft\
Windows\CurrentVersion\Run
Name: ctfmon.exe
User: FABRIKAM\kenmyer

As you can see, we have two different auto-run programs here: one that has a shortcut in the Startup folder, the other is an application listed in Run in the HKEY_USERS portion of the registry. (Yet another location on the computer where auto-run information might be stored.) And our one simple little script can go out and, in a matter of seconds, retrieve information about both these programs, as well as any other auto-run applications. That’s why we consider a script that uses Win32_StartupCommand to be the better approach.

So how does the script itself work? Well, it’s about as simple a WMI script as you’ll ever write. We start off by connecting to the WMI service on the local computer. Needless to say, we could also use this script to connect to the WMI service on a remote computer (which would return a list of programs configured to auto-run on that machine). We then call the ExecQuery method and issue the following query, one that goes out and grabs a collection of all the auto-run programs it can find:

Set colStartupCommands = objWMIService.ExecQuery _
    ("Select * from Win32_StartupCommand")

All that’s left now is to set up a For Each loop and walk through the collection of programs, echoing back such data as the application name and the location where the auto-run information can be found. It’s fast, it’s easy and – best of all – it returns a lot more information than you could get just by reading values filed under a single registry key. What’s not to like, huh?

We hope that helps, JW. If it doesn’t, if you really did need to just read the Run key and be done with it, well, let us know and we’ll see what we can do for you. Any more questions? Do we know who ate the last piece of cake, the one you were saving for yourself? Well, we could answer that, JW. But we’re not going to.


How Can I List All the User Profiles on a Computer?

Hey, Scripting Guy! How can I list all the user profiles on a computer?

-- GN

Hey, GN. You want to know a secret? We hate questions about user profiles. It’s not that we have anything against user profiles. The problem is that user profiles represent an important part of a system administrator’s duties, and because of that everyone assumes that there must be all sorts of really cool scripting technologies that make it easy to manage user profiles. In turn, that means everyone ends up disappointed when we Scripting Guys have to break the bad news: there aren’t any really cool scripting technologies that make it easy to manage user profiles.

Hey, no need to be embarrassed: we cried the first time we heard the news, too. But listen, all is not lost. There might not be a really cool scripting technology dedicated to user profiles, but there are workarounds that enable us to accomplish some of the more common management tasks related to user profiles. And one of those workarounds allows us to list all the user profiles found on a computer.

As it turns out, you can find a list of all the user profiles in the HKEY_LOCAL_MACHINE portion of the registry; more specifically, you can find them listed under this registry key:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList

That’s the good news. The bad news is that you don’t get a nice, neat list of user profiles; instead, you see a series of separate registry subkeys, each one named after a user’s SID (Security Identifier). To determine the actual user profiles found on a computer we need to iterate through each of those individual subkeys and read the value of ProfileImagePath; as shown in the following screenshot, ProfileImagePath gives us the path to the profile and, by extension, the name of the user that the profile belongs to:

For a larger, easier-to-read version of this graphic click here.

So how do we iterate through all these subkeys and read the value of ProfileImagePath? Well, as we almost always do any time we’re faced with a scripting issue that involves the registry, we turn to WMI:

On Error Resume Next

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & _
    strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
objRegistry.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubkeys

For Each objSubkey In arrSubkeys
    strValueName = "ProfileImagePath"
    strSubPath = strKeyPath & "\" & objSubkey
    objRegistry.GetExpandedStringValue HKEY_LOCAL_MACHINE,strSubPath,strValueName,strValue
    Wscript.Echo strValue
Next

Don’t worry: we’ll explain how this all works. We begin by defining a constant named HKEY_LOCAL_MACHINE and assigning it the value &H80000002; we’ll use this constant later on to indicate the part of the registry we want to work with. We then connect to the WMI service, specifying the path to the StdRegProv class (note that this is found in the root\default namespace).

After that we store the registry path (SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList) in the variable strKeyPath, then use the EnumKey method to return a collection of all the subkeys found under ProfileList (as we’ve already determined, these subkeys – each one named after a user SID – represent all the user profiles found on the computer). Note that the EnumKey method takes three parameters:

· HKEY_LOCAL_MACHINE, the constant that tells the script which portion of the registry to work with.

· strKeyPath, the variable containing the path to the ProfieList registry value.

· arrSubkeys, an “out” parameter that will hold the results returned by EnumKey. When we call EnumKey, the collection of registry subkeys found under ProfileList will be stored in arrSubkeys.

If we wanted to we could list all the subkey names by using code similar to this:

For Each objSubkey In arrSubkeys
     Wscript.Echo objSubkey
Next

So why don’t we do that? Well, remember, these subkeys are named after user SIDs; simply listing the subkey names will return data similar to this:

S-1-5-18
S-1-5-19
S-1-5-20
S-1-5-21-1229272821-920026266-1060284298-1003
S-1-5-21-1229272821-920026266-1060284298-1005
S-1-5-21-1229272821-920026266-1060284298-1009
S-1-5-21-1454471165-1004336348-1606980848-8183

Interesting, but not very informative. (Unless, of course, you usually call people by their SID rather than by their name.)

Instead, what we need to do is connect to each of these registry subkeys and read the value of ProfileImagePath; that’s what we do with these lines of code:

For Each objSubkey In arrSubkeys
    strValueName = "ProfileImagePath"
    strSubPath = strKeyPath & "\" & objSubkey
    objRegistry.GetExpandedStringValue HKEY_LOCAL_MACHINE,strSubPath,strValueName,strValue
    Wscript.Echo strValue
Next

No, it’s not too hard. We start off by defining a variable named strValueName and assigning it the value ProfileImagePath; this is the name of the individual registry value we want to read. We then take our original registry path (SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList) and tack on a \ and the name of the first subkey in our collection (for example, S-1-5-18). That will give us a path to that first subkey, something similar to this:

SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\S-1-5-18

Now that we know the path to the registry value we can use the GetExpandedStringValue method to read the value of ProfileImagPath. Why GetExpandedStringValue? Well, ProfileImagePath happens to be an expanded string value; it contains information similar to this:

%SystemDrive%\Documents and Settings\TestUser

The GetExpandedStringValue method will automatically replace a variable like %SystemDrive% with the actual value (e.g., C:). Thus we end up with output similar to this:

C:\Documents and Settings\TestUser

As long as we’re on the subject, note that GetExpandedStringValue requires four parameters:

· HKEY_LOCAL_MACHINE, the constant that – again – represents the HKLM portion of the registry.

· strSubPath, the path to the first user profile subkey (for example, SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\S-1-5-18).

· strValueName, a variable representing the registry value (ProfileImagePath) we want to read.

· strValue, another out parameter, this one containing the value of ProfileImagePath. The last thing we do in our For Each loop is echo the value of this out parameter, which will coincide quite nicely with the path to the user profile.

When we run the script we’ll get back data similar to this:

C:\WINDOWS\system32\config\systemprofile
C:\Documents and Settings\LocalService
C:\Documents and Settings\NetworkService
C:\Documents and Settings\kenmyer
C:\Documents and Settings\TestUser
C:\Documents and Settings\packerman

Much better.

Incidentally, if all we wanted was the user name (for example, kenmyer) we could get fancy and use the VBScript Split function to separate the user name (the last item in the path) from the rest of the output. But that’s something we’ll leave for you to try on your own. (But if you need a hint for how to work with the Split function take a peek at this Hey, Scripting Guy! column.)


How Can I Lock a Workstation After Five Minutes of Inactivity?

Hey, Scripting Guy! How can I lock a workstation after five minutes of inactivity, something recommended to us by our auditors?

-- NM

Hey, NM. To tell you the truth, we don’t know. Locking a computer remotely can be a bit of a problem; for more information on that, take a look at one of our columns from a year ago. On top of that, you face the problem of deciding whether or not a computer is idle. Because you can’t monitor for keystrokes or mouse clicks, about the best you can do is check the CPU usage and make a guess as to whether the computer is idle. That might work, but you’d have to constantly monitor the computer and continually make a determination as to whether the machine is idle or not. There has to be a better way.

And there is. After all, the computer can already monitor and lock itself after 5 minutes of inactivity: all you have to do is password-protect the screensaver and set the screensaver timeout value to 5 minuets. If the computer sits idle for 5 minutes the screensaver will kick in and the computer will be locked, which sounds like the very thing recommended by your auditors.

Best of all, you can use scripts that directly modify the registry in order to configure the appropriate screensaver settings on all your computers. For example, here’s a script that enables password-protection on the screensaver:

HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Control Panel\Desktop"
ValueName = "ScreenSaverIsSecure"
strValue = "1"

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

This script starts off by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this lets the script know which registry hive we want to deal with. We then use this line of code to connect to the WMI service and to the Standard Registry Provider class (StdRegProv):

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Next we assign values to three different variables:

· strKeyPath. The path – within HKEY_CURRENT_USER – to the registry key that needs to be changed. For the screensaver, that path happens to be Control Panel\Desktop.

· ValueName. The registry value to be changed. To enable password protection we need to change the ScreenSaverIsSecure value.

· strValue. The new value to be assigned to ScreenSaverIsSecure. Set ScreenSaverIsSecure to 1 to enable password protection; set it to 0 to disable password protection.

All we have to do then is call the SetStringValue method and pass it all the defined parameters:

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

And there you go. The next time the user logs on, the screensaver will be password-protected.

Note. Yes: although the registry is changed immediately the change won’t take effect until the next time the user logs on. Because of that, you might want to include this code in a logon script; in that case, the registry gets configured before the user logs on. Alternatively, you can make the changes and then forcibly log the user off; check the Microsoft Windows 2000 Scripting Guide for information about logging a user off a computer.

Of course, before you log the user out you might also want to configure the screensaver timeout value to 5 minutes. Here’s a script that does just that:

HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Control Panel\Desktop"
ValueName = "ScreenSaveTimeout"
strValue = "300"

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

You’re right: it’s almost exactly like the first script we showed you. In fact, there are only two differences:

· The registry value we’re changing is ScreenSaveTimeout (still found in the Control Panel\Desktop key).

· The new value being assigned to ScreenSaveTimeout is 300. Screensaver timeouts are stored as seconds: 300 seconds equals 5 minutes (300 divided by 60 = 5). If the auditors recommend a three-minute timeout value then set ScreenSaveTimeout to 180 (60 seconds times 3).

And there you have it. Your computers will be locked after five minutes of inactivity, and you don’t have to write any complicated monitoring/remote locking scripts.

And since you asked, yes, the Scripting Guys are very grateful that they don’t get locked out after 5 minutes (or even 5 hours) of inactivity.


How Can I Retrieve a List of the Commands Typed Into the Run Dialog Box?

Hey, Scripting Guy! Sometimes I type commands in the Run dialog box that I’d like to retrieve later on. I know the most recent commands I used are cached somewhere; that’s because they show up when I start typing in the Run dialog box. How can I use a script to retrieve these commands?

-- KJ

Hey, KJ. You know, our first thought upon reading your question was this: why didn’t we think of that? Needless to say, the Scripting Guys have been using the Run dialog box for years, and we, too are well aware that the most recent commands (the last 26, if you’re scoring at home) are cached somewhere on the computer. And yet, it never occurred to us to write a script that could retrieve this list. How could we have overlooked something so obvious?

Note. Actually, it’s not all that surprising we overlooked something so obvious. For example, the Scripting Guys have been in their current building for about a year now, yet it was only a few weeks ago that the Scripting Guy who writes this column found out that there was a stairway just down the hall from his office.

After poking around a bit we discovered that this information is stored in the registry; more specifically, it’s stored as individual registry values within the registry key HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU. Is that good? You bet it is; after all, it allows us to write a script like this:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU"
objRegistry.EnumValues HKEY_CURRENT_USER, strKeyPath, arrValueNames, arrValueTypes

For Each strValue in arrValueNames
    If Len(strValue) = 1 Then
        objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValue,strRunCommand
        intLength = Len(strRunCommand)
        strRunCommand = Left(strRunCommand, intLength - 2)
        Wscript.Echo strRunCommand
    End If
Next

This script connects to the RunMRU key and then enumerates the values of all the values found there. (And, yes, we know: values of values? Such is the joy of registry terminology.) To accomplish that feat, the script starts off by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this constant is used later on to tell the script which registry hive to work with. We then connect to the WMI service on the local computer, taking care to bind to the root\default namespace, home of the WMI registry provider.

Note. Can we use this same script to retrieve the most-recently used commands from a remote computer? Of course we can; just assign the name of the remote computer to the variable strComputer.

After connecting to the WMI service, we assign the value Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU to a variable named strKeyPath. We then use the EnumValues method to grab a collection of all the registry values found in the RunMRU key:

objRegistry.EnumValues HKEY_CURRENT_USER, strKeyPath, arrValueNames, arrValueTypes

As you can see, we pass EnumValues four parameters:

Parameter | Description
HKEY_CURRENT_USER | The registry hive where the information can be found.
strKeyPath | The path to the RunMRU key within the HKCU hive.
arrValueNames | “Out” parameter that serves as place to store all the registry value names. All we do is supply EnumValues with a variable name; EnumValues will then populate this variable with all the value names found in RunMRU.
arrValueTypes | Another out parameter, this one containing data types corresponding to each value found in RunMRU. This is a required parameter, but because the values found in RunMRU all have a data type of REG_SZ we don’t actually use it in the script.

As it turns out, each command typed into the Run dialog box is given its own value in the registry; these values are assigned names using the letters A through Z (which explains why only the 26 most-recently used commands are tracked in the registry). In the registry RunMRU looks something like this:

After executing the EnumValues method we’ll get back a collection of all these value names; in other words, our collection will consist of the letters A through Z. That’s good, but what this collection doesn’t contain are any of the actual commands themselves. To get the commands (which is the whole idea in the first place) we need to connect to and read each of the 26 values in the registry.

Can we do that, can we easily connect to and read each of the 26 values in the registry? Of course we can; in fact, that’s what we do in this block of code:

For Each strValue in arrValueNames
    If Len(strValue) = 1 Then
        objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValue,strRunCommand
        intLength = Len(strRunCommand)
        strRunCommand = Left(strRunCommand, intLength - 2)
        Wscript.Echo strRunCommand
    End If
Next

You’re right: at first glance that is a little forbidding, isn’t it? Tell you what, let’s show you a simplified version of this For Each loop, then we’ll explain why we added some additional code to the loop. Here’s the simplified loop:

For Each strValue in arrValueNames
    objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValue,strRunCommand
    Wscript.Echo strRunCommand
Next

What we’re doing here is setting up a loop that runs through all of the registry values. To read each of those values we simply call the GetStringValue method:

objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValue,strRunCommand

GetStringValue gets passed four parameters: the constant HKEY_CURRENT_USER; the variable strKeyPath; the variable strValue (which represents the individual value name, e.g., A, B, or C); and an out parameter named strRunCommand. With this out parameter we simply specify a variable name and the GetStringValue method assigns it the value of the registry value (that is, the appropriate Run command). After calling GetStringValue we echo back strRunCommand, loop around, and tackle the next value in the collection.

So much for the simplified For Each loop; now what about all that extra code in our real For Each loop? The extra code is there primarily to give us slightly-nicer output. For example, in the RunMRU key there’s a registry value named MRUList. This doesn’t represent an actual command; instead, it represents the order in which the most recently-used commands are presented. That’s not important to us (at least not today), so we’d just as soon skip the MRUList value altogether. That’s what we do here:

If Len(strValue) = 1 Then

In this line of code we use the Len function to check the number of characters in the value name. If the number of characters (length) is equal to 1 we go ahead and read the value. If the length is not equal to 1 (which, obviously, is the case with MRUList, which has 7 characters) then we simply skip that value and move on to the next item in the collection.

The other little touch we added was this:

intLength = Len(strRunCommand)
strRunCommand = Left(strRunCommand, intLength - 2)

If you look in the registry, you’ll see that our commands all have a \1 appended to the end. We could leave that if we wanted to, but it’s easy enough to get rid of. All we do is determine the length of the command, then use the Left function to return the first x characters in the string. And what is x equal to? It’s equal to the total number of characters minus 2. That simply means we’re going to grab all the characters except the last 2 (the \1) and echo those to the screen.

And there you have it: a script that returns the most-recently used commands typed into the Run dialog box. We still haven’t figured out where that mysterious stairway leads, but first things first.


How Can I Retrieve the Path to the Program Files Folder on a Computer?

Hey, Scripting Guy! How can I retrieve the path to the Program Files folder on a computer?

-- CC

Hey, CC. How can you retrieve the path to the Program Files folder on a computer? Well, to be honest, not as easily as we thought you could. If you’re trying to retrieve the Program Files path on the local computer, well, that’s easy; retrieving the Program Files path on a remote machine turned out to be a bit of a sticky wicket. But, we did find a way to do this.

Let’s start by looking at the easy scenario. Here’s a script that uses the Shell object to retrieve the path to the Program Files folder on the local computer:

Const PROGRAM_FILES = &H26&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(PROGRAM_FILES)
Set objFolderItem = objFolder.Self
Wscript.Echo objFolderItem.Path

The script begins by defining a constant named PROGRAM_FILES and setting the value to &H26&; we’ll use this constant to tell the script which “special folder” we want to work with. (Are there other special folders we can work with? You bet there are, and the Script Center Script Repository has a few dozen scripts that show how to do this.) We create an instance of the Shell.Application object, and then use the Namespace method to bind to the Program Files folder (note that we pass the constant PROGRAM_FILES to the Namespace method):

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(PROGRAM_FILES)

Now we encounter one of the little idiosyncrasies of the Shell object. Although we have bound ourselves to the Folder object that represents the Program Files folder, we can’t directly retrieve the path to the folder. Instead, we have to create a FolderItemObject for the folder; that’s something we can do using the Self method (which basically just binds the folder to itself):

Set objFolderItem = objFolder.Self

Weird, but once we have a FolderItemObject we can echo back the value of the Path property:

Wscript.Echo objFolderItem.Path

As you might expect, the value of the Path property will be the path to the Program Files folder.

Now that’s great … as long as you’re trying to find the Program Files folder on the local computer. If you’re trying to determine the path to the Program Files folder on a remote computer, however, it’s a different story: that’s because the Shell object won’t run against remote machines. On top of that, we couldn’t find a WMI method or property that we can use to retrieve the path. (We thought for sure there’d be such a property, but ….) And while there is a %programfiles% environment variable, we can’t readily retrieve the value of that environment variable from a remote computer either. In other words, we can’t use the Shell object, we can’t use WMI, and we can’t use the %programfiles% environment variable. Uh-oh.

All we can say about this is, “Thank goodness for the registry.” (We never thought we’d say that, either.) As it turns out, the path to the Program Files folder is stored in the following registry value:

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ProgramFilesDir

That means that we can use WMI to read the registry on a remote computer and return the path:

HKEY_LOCAL_MACHINE = &H80000002

strComputer = "atl-ws-01"
Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion"
ValueName = "ProgramFilesDir"

objReg.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, ValueName, strValue
Wscript.Echo strValue

So what are we doing with this script? Well, we begin by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; that tells our script which registry hive to work with. We then use these two lines of code to connect to the WMI service on a remote computer named atl-ws-01:

strComputer = "atl-ws-01"
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Still with us? Good. Next we assign values to two variables, strKeyPath and ValueName:

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion"
ValueName = "ProgramFilesDir"

It should come as no surprise that strKeyPath represents the path within the HKLM registry hive, while ValueName represents the name of the registry value we want to read (ProgramFilesDir).

Now we’re ready to call the GetStringValue method and read the registry:

objReg.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, ValueName, strValue

Notice that we pass GetStringValue four parameters: HKEY_LOCAL_MACHINE, strKey, ValueName, and strValue. The first three parameters are a constant and two variables that we defined earlier. But what the heck is strValue?

Well, strValue is an “out parameter.” We don’t specify a value for an out parameter; instead, the method supplies the value. Think of strValue as being a container of sorts; we give GetStringValue the container and GetStringValue then fills that container with the information it reads from the registry. In other words, the answer to our problem – the path to the Program Files folder – will be stored in the out parameter strValue.

Which, in turn, means all we have to do now is echo back that value:

Wscript.Echo strValue

And there you have it. It might not have been the most obvious and intuitive way to get the path to the Program Files folder but, hey, any port in a storm, right?


How Can I Tile the Windows on the Desktop?

Hey, Scripting Guy! I have a script that opens nine different command windows in order to monitor performance. How can I tile those windows horizontally after they’ve all been opened?

-- AM

Hey, AM. You know, one of the Scripting Guys might not be much when it comes to scripting, but he is a master of one thing: he has a near-monopoly on totally useless knowledge. For example, he knows all nine verses of My Darlin’ Clementine; heck, he can sing O Come All Ye Faithful in Latin. He knows the first – and last – names of all the castaways on the original Gilligan’s Island; he even knows Paul McCartney’s first name. (And, no, it’s not Paul.) Every day this Scripting Guy comes to work hoping someone will ask him who came up with the story of Rudolph the Red-Nosed Reindeer. Every day he goes home disappointed.

But today is different. Many, many years ago this same Scripting Guy learned how to tile windows using a script. Not being particularly imaginative, he couldn’t think of a scenario in which this might prove useful. Therefore, he decided he’d just wait until someone asked him how to tile windows using a script. He’s been waiting a long, long time.

AM, you’ve at last made this Scripting Guy one happy camper. You want to know how to horizontally tile windows? All you had to do was ask:

Set objShell = CreateObject("Shell.Application")
objShell.TileHorizontally

Good point: after all these years you’d expect something more dramatic than just two lines of code. But that’s all it takes. You simply create an instance of the Shell.Application object and then call the TileHorizontally method. If you want to tile your windows vertically, you can use this code:

Set objShell = CreateObject("Shell.Application")
objShell.TileVertically

And if you want to cascade your windows, use this code:

Set objShell = CreateObject("Shell.Application")
objShell.CascadeWindows

Pretty easy but, then again, no one said you had to be Professor Roy Hinkley in order to tile windows. (Yes, Roy Hinkley was the Professor on Gilligan’s Island.)

Two minor caveats here. First, these methods work only on the local computer; that’s because you can’t instantiate the Shell object on remote machines. But unless you were planning on having some fun by tiling and re-tiling the windows on someone else’s computer that shouldn’t make much difference.

In addition, we should point out that this script will tile all the windows on the computer, not just the nine command windows you started with your script. But that’s true even if you tile windows manually (by right-clicking the Taskbar and then clicking Tile Windows Horizontally or Tile Windows Vertically). Something you’ll just have to live with.

Just to show you what this might look like in real life, we’ve created a script that opens nine command windows and then tiles the desktop. Note that we added a Wscript.Sleep statement and paused the script for 5 seconds (5,000 milliseconds) before tiling the windows; we did that to make sure all nine command windows were open and visible onscreen before we tiled them.

Here’s what the code looks like:

Set objWSHShell = CreateObject("Wscript.Shell")

For i = 1 to 9
    objWSHShell.Run "%comspec% /k"
Next

Wscript.Sleep 5000

Set objShell = CreateObject("Shell.Application")
objShell.TileHorizontally

And here’s what the desktop looks like after running the script:

We’ve waited a long time to see a picture like that.

Oh, and one more thing: it’s James. James Paul McCartney. Thanks for asking!


How Can Tell Whether the NumLock Key is On or Off?

Hey, Scripting Guy! How can I tell whether the NumLock key on a computer is on or off?

-- KA

Hey, KA. You know, there are two problems with being a Scripting Guy. (Three, if you count the amount we get paid for doing this.) For one thing, you always have to be on your guard whenever Dean is around. Why? Well, to name one example, as we were busy enjoying the Dinner With the Scripting Guys promotion a few months ago, Jean glanced away from her plate for just a moment. When she looked back, she discovered that Dean was eating not only his food, but her food as well. (Dean says he got confused because of all the dishes and plates that were arrayed on the table.)

For another, people always assume that you’re joking, even when you’re not. For example, suppose you asked a question like “How can I tell whether the NumLock key on a computer is on or off?” and suppose we replied “Well, the only way we know how to do that is to write a script that uses Microsoft Word.” “Oh you Scripting Guys,” you’d say, “always with the jokes.” But this time, we’re absolutely serious.

No, really; we mean it. In yet another of those Unfathomable Mysteries of Scripting, you can’t use a technology like WMI, Windows Script Host, or the Shell object to determine whether or not the NumLock key is on. And yet, you can use Microsoft Word to carry out that task:

Set objWord = CreateObject("Word.Application")
Wscript.Echo "NumLock key is on: " & objWord.NumLock
objWord.Quit

Granted, it’s a little weird; on the bright side, it’s also a very simple little script. To begin with, we create an instance of the Word.Application object. Notice, however, that – unlike most of our Microsoft Office scripts – we don’t set the Visible property to True; that’s because there’s no reason to make Word visible. That’s due, at least in part, to the fact we need Word for only one thing: to check the value of the NumLock property. To determine whether or not the NumLock key is on we only have to execute this line of code:

Wscript.Echo "Numlock key is on: " & objWord.NumLock

After that we simply call the Quit method and terminate our instance of Word.

Weird, but effective.

Incidentally, you can also use this same basic approach to determine whether or not the CapsLock key is on; the only difference is that you echo back the value of the CapsLock property:

Set objWord = CreateObject("Word.Application")
Wscript.Echo "CapsLock key is on: " & objWord.CapsLock
objWord.Quit

Now, admittedly, you probably don’t really need a script to tell whether the NumLock key (or even the CapsLock key) on your local computer is on or off; a quick glance at the keyboard will probably suffice. But that’s OK: this script can also be used to determine whether the NumLock (or CapsLock) key on a remote computer is on or off. All you have to do is pass the name of the remote computer as the second parameter to the CreateObject method. For example, this script determines the status of the NumLock key on the remote computer atl-fs-01:

Set objWord = CreateObject("Word.Application", "atl-fs-01")
Wscript.Echo "NumLock key is on: " & objWord.NumLock
objWord.Quit

Note. You say you didn’t even know that CreateObject accepted an optional second parameter? Then it’s high-time you paid a visit to Sesame Script, isn’t it?

The one thing that this script can’t do is change the status of the NumLock key: it can tell you that the NumLock key is on but it can’t turn it off for you. (That’s because the NumLock property is read-only.) About the only way we know to toggle the NumLock key on and off is to use the SendKeys method:

Set objShell = CreateObject("WScript.Shell")
objShell.SendKeys "{NUMLOCK}"

This works, although it won’t work on remote computers (SendKeys works on only the local machine). However, this might be useful in a logon script: in such a script you could check the status of the NumLock key and then, if needed, turn it on or off.

As for us, having disposed of today’s column, it’s now time for breakfast. That’s funny: we had a muffin sitting here just a moment ago. Wait a second: where is Dean?


How Can I Append a Location to the Path Environment Variable?

Hey, Scripting Guy! How can I append a location to the Path environment variable?

-- AC

Hey, AC. You know, there’s a TV commercial out in which a guy is watching a baseball game when his wife suddenly decides that the two of them should do something else. Talk about a dilemma: after all, according to the narrator, “In life there’s only room for one great passion … unless you’re really clever.” So does this guy do something really clever to solve his problem? You bet he does: giving we TV viewers a sly look, he uses his VCR to tape the baseball game.

We know: we never would have thought about using a video cassette recorder to record video, either. But that’s the sort of thing that separates the Really Clever Guys from the Scripting Guys.

Note. Well, that and the fact that – with at least one Scripting Guy – you never interrupt him when he’s watching a baseball game. You know the old advice about never trying to take a dog’s food while it’s eating? The same thing applies to this Scripting Guy and baseball.

Fortunately, though, you don’t have to be really clever to add a location to your Path variable. Instead, you just have to know how to use the WMI class Win32_Environment:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Environment Where Name = 'Path'")

For Each objItem in colItems
    strPath = objItem.VariableValue & ";C:\Scripts\"
    objItem.VariableValue = strPath
    objItem.Put_
Next

You’re right: this script is so simple it probably will leave you time for 2 or 3 additional great passions in life, won’t it? As you can see, we start out by connecting to the WMI service on the local computer. Of course, we aren’t limited to modifying the Path on the local computer. Instead, we can get the script to modify the Path on a remote computer simply by assigning the name of that computer to the variable strComputer:

strComputer = "atl-fs-01"

After making the connection to the WMI service we then use this line of code to retrieve a collection of all the environment variables that have the name Path:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Environment Where Name = 'Path'")

Could we retrieve other environment variables using this type of query? You bet. For example, this query returns information about the environment variable Comspec:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Environment Where Name = 'Comspec'")

Or, leave out the Where clause and get back information about all the environment variables on the computer:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Environment")

After retrieving the collection we set up a For Each loop to walk us through each item in that collection. (And don’t worry, that won’t take too long: because environment variable names must be unique we’ll only have, at most, one variable named Path.) Inside the loop we then execute these three lines of code:

strPath = objItem.VariableValue & ";C:\Scripts\"
objItem.VariableValue = strPath
objItem.Put_

What’s going on here? Well, in the first line we’re appending a new value to the VariableValue property and assigning the resulting string to strPath; as you probably guessed, VariableValue contains the value of the environment variable. So what new value are we assigning strPath? We’re assigning it the existing value of the VariableValue property plus a semicolon (;) plus the new location we want added to the Path: C:\Scripts\. In other words, suppose this is the current value of the Path environment variable:

C:\Program Files\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;
C:\WINDOWS\system32\WindowsPowerShell\v1.0

After executing this line of code the variable strPath will contain this value, which is simply the old Path plus the new location:

C:\Program Files\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;
C:\WINDOWS\system32\WindowsPowerShell\v1.0;C:\Scripts\

Get the idea? All we did was add a semicolon (because individual locations in the Path are separated by semicolons) followed by the additional location.

In line 2 we assign the value of strPath to the VariableValue property. After we’ve done that all we have to do is call the Put_ method to officially write the value of strPath to the property and, in the process, add C:\Scripts\ to our Path. Like we said, not very clever. But definitely quick and easy.

Note. How can you verify that this actually worked? Here’s one easy way. After the script finishes running open up a new command window and type path. If the script worked you’ll see C:\Scripts\ at the very end of the output.

Incidentally, that TV commercial we mentioned happens to be for a pharmaceutical company and one of its most popular products. As part of the required disclaimer the commercial features a doctor who states, somewhat matter-of-factly, “If you should experience a sudden decrease in vision, stop taking the drug and call your doctor immediately.” Which simply means this: if you take one of these pills and go blind, you probably shouldn’t take another one to see if that will help you get your vision back.

In the interest of public safety we’d like to add a similar disclaimer to today’s column: if you should experience a sudden decrease in vision, stop reading this column and call your doctor immediately.

Either that, or turn the light on. Remember, if you’re in the northern hemisphere it gets darker earlier and earlier these days.


How Can I Retrieve the Path to the %windir% Folder on a Computer?

Hey, Scripting Guy! How can I retrieve the path to the %windir% folder on a computer?

-- DS

Hey, DS. Interesting that you should ask this question. This same question happened to be asked on an internal Microsoft mailing list just the other day, and the answer given (not by one of the Scripting Guys we hasten to add) was an extremely complicated one, an answer involving environment variables, redirected output, the WSHController object, human sacrifice, illegal Swiss bank accounts, and ground-up unicorn horn (fresh, not frozen). As near as we could tell, you gather all the above items, mix well, and then call the user and ask them, “Hey, could you type echo %windir% at the command prompt and tell me what shows up?”

So is it really that hard to figure out which folder happens to be the Windows folder? Thankfully, no; all you really need to do is query the WMI class Win32_OperatingSystem and echo back the value of the WindowsDirectory property:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_OperatingSystem")

For Each objItem in colItems
    Wscript.Echo objItem.WindowsDirectory
Next

As you can see, there’s not much to this script. We begin by connecting to the WMI service on the local computer; if we want to determine the Windows folder on a remote computer, all we have to do is change the value of the variable strComputer to the name of that remote computer. For example, these two lines of code will connect us to the WMI service on the remote computer atl-fs-01:

strComputer = "atl-fs-01"

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

After making the connection we use the ExecQuery method to retrieve all the instances of the active operating system on the computer. Why do we emphasize the word active? Well, the Win32_OperatingSystem class returns information only about the operating system currently in use. What if you have a dual-boot machine, one that can boot up in either Windows XP or Windows Server 2003? In that case, Win32_OperatingSystem returns information only about the operating system currently up and running. If you happen to be running Windows XP, you will get back information only about Windows XP; Win32_OperatingSystem will not even acknowledge the presence of the inactive Windows Server 2003 operating system.

Just something to keep in mind.

After that we set up a For Each loop to loop through all the operating systems in the collection (again, there will always be one and only one). Inside that loop we echo the value of the WindowsDirectory property, which just happens to be the same thing as the value of %windir%.

By the way, there are at least two other properties of the Win32_OperatingSystem class that might be of interest to you: SystemDrive, which returns just the drive letter where the operating system is installed, and SystemDirectory, which returns the path to the System folder (typically C:\Windows\System32).

Cool, huh? Now if only we’d thought of trying WMI first. Anyone interested in several pounds of freshly-ground unicorn horn? Just let us know.


How Can I Use Windows PowerShell to Retrieve Environment Variables and Special Folder Paths?

Hey, Scripting Guy! How can I use Windows PowerShell to retrieve the value of environment variables and the path to a special folder?

-- SB

Hey, SB. Well, it’s Thursday, which means that the Scripting Guy who writes this column is currently in Walla Walla, WA, which, as Daffy Duck fans all know, is the home of the Wishy Washy Washing Machine Company; various divisions of the Acme Corporation; and who-knows-what-else.

Oh, we know what else: Walla Walla was also immortalized in the song lyrics “Oo ee oo-ah-ah, ting tang walla walla bing bang.” Walla Walla is even featured in the Wikipedia entry on inherently funny words. Pretty good for a town of 30,000 people, wouldn’t you say?

Of course, the Scripting Guy who writes this column isn’t spending his time in Walla Walla sitting around doing nothing. (That’s what he does when he’s at work.) Instead, he’s busy doing all sorts of things, including

· Writing this column.

· Getting ready for Game 1 of the Walla Walla Invitational Baseball Tournament.

· Watching the thermometer creep past the 750 degree mark.

OK, so maybe it’s not quite 750 degrees here, at least not 750 degrees Fahrenheit. However, the temperature is expected to climb above 100 degrees, and some of this Script Guy’s fellow baseball parents are already slathering themselves with sunscreen, drinking tons of water, and complaining about the heat.

And that’s a bad sign; after all, most of them haven’t even gotten out of their air-conditioned cars yet.

Having grown up in the nearby Tri-Cities, however, the Scripting Guy who writes this column isn’t going to be deterred by a little heat; after all, he once played a baseball game in 114 degree heat. (And, oddly enough, had to walk 12 miles uphill, through the snow, in order to get to the game.) He’s not complaining about the heat, taking water intravenously, or even wearing sunscreen; on top of that, he’s about to answer a question about retrieving environment variable values and special folder paths using Windows PowerShell.

Yes, he is quite a man, isn’t he?

For starters, let’s see how we can retrieve the values of an environment variable. As SB noted, in VBScript you can retrieve the value of the %logonserver% environment variable by using code similar to this:

Set oShell = CreateObject("Wscript.Shell")

Set WshSysEnv = oShell.Environment("PROCESS")
sLogonServer = WshSysEnv("LOGONSERVER")

Pretty straightforward: we create an instance of the Wscript.Shell object, grab the collection of process environment variables, then store the value of the logonserver in a variable named sLogonServer. So how do we get this same information using Windows PowerShell? Why, like this, of course:

$a = $env:logonserver

Yes, that is easy, isn’t it? As it turns out, Windows PowerShell has an environment provider that makes it a snap to access information about all the environment variables on a computer. As you can see, all we have to do is specify the $env variable followed by a colon and the name of the environment variable we’re interested in. Want to store the value of the %username% environment variable in a variable named $a? Then use this syntax:

$a = $env:username

Or maybe you’d like to know the computer name:

$a = $env:computername

If it wasn’t for the baseball game, we could sit here and retrieve environmental variables all day long.

You know, that’s a good question: what if you don’t know which environment variables are available on a computer? Fortunately that’s no big deal: to view all the environment variables and their values use this command (note that, in this case, we leave the $ off and reference the env: drive instead):

dir env:

Try this command in Windows PowerShell and see what you get back.

OK, now what about the path to special folders? As SB pointed out, this is a pretty easy task to perform in VBScript; for example, the following VBScript code retrieves the path to the user’s desktop folder:

Set oShell = CreateObject("Wscript.Shell")

Set oSFolders = oShell.SpecialFolders
sDesktop = oSFolders("Desktop")

As you can probably see for yourself, the preceding script creates an instance of the Wscript.Shell object, creates an object reference to the SpecialFolders collection, then retrieves the path to the desktop folder. We could employ this exact same approach using Windows PowerShell; alternatively, we could use the .NET Framework’s System.Environment class (and the GetFolderPath method) to retrieve the path to the desktop folder. All things considered, however, we decided that it’s easier (and more powerful) to use the the Shell.Application object to retrieve special folder paths:

$a = New-Object –com Shell.Application
$b = ($a.namespace(0x10)).Self.Path

OK, granted, this might not look all that easy. With that in mind, let’s see if we can explain how it works. In line 1 we simply use the New-Object cmdlet to create an instance of a new COM object (hence the –com parameter). And what new COM object are we creating? You got it: Shell.Application.

Actually, that was pretty easy; line 2 is a different story. But don’t panic: line 2’s bark is far worse than its bite. In fact, line 2 only looks complicated because we’re actually performing a couple different tasks with a single line of code. To begin with, we’re taking the object reference to the Shell.Application object ($a) and using the Namespace method to bind to a particular folder. That’s what this portion of the script is for:

($a.namespace(0x10))

What’s the 0x10 for? That’s a hexadecimal value that represents the Desktop folder. And how did we know that 0x10 represents the Desktop folder? To tell you the truth, we didn’t; that’s why we popped over to the Script Repository and took a peek at the special folder scripts. As it turns out, the script titled List Items in the Desktop Folder looks like this:

Const DESKTOP = &H10&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(DESKTOP)
Set objFolderItem = objFolder.Self
Wscript.Echo objFolderItem.Path

Set colItems = objFolder.Items
For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

Note. Do you want to use Windows PowerShell to list all the items in the desktop folder? Then just add a third line to your script: Get-ChildItem $b.

Notice the first line of code, where we assign the hexadecimal value &H10& to a constant named Desktop. The &H10& (which uses VBScript syntax for hexadecimal values) is the value that represents the Desktop folder; all we have to do is convert that to 0x10, the Windows PowerShell syntax for hexadecimal values. Suppose we were interested in the Internet Cookies folder instead. Well, the VBScript script for working with that folder includes this line of code:

Const COOKIES = &H21&

Now, guess what the PowerShell equivalent to &H21& is? That’s right: 0x21.

Etc., etc.

You might also have noticed that we placed parentheses around the call to the Namespace method:

$b = ($a.namespace(0x10)).Self.Path

Why did we do that? That’s easy: Windows PowerShell always takes care of anything in parentheses before it performs any other tasks. That means that Windows PowerShell is going to first use the Namespace method to bind to the Desktop folder; it’s only after that task is complete that PowerShell retrieves the value of the Self.Path property, which just happens to be the path to the Desktop folder. The parentheses simply make sure that Windows PowerShell first gets the folder and only then tries to retrieve the folder path.

Granted, this can be a tad bit confusing. But remember, all you have to do is replace the 0x10 with the appropriate value and you can easily bind to any special folder on the computer.

We hope that helps, SB; the truth is, it’s going to have to, because it’s almost time for the game to start. You know, watching high school baseball in Walla Walla is like a trip down memory lane for the Scripting Guy who writes this column; back when he was in high school he actually played a few games in Walla Walla. For example, when he played here as a sophomore the umpires were prisoners on work release from the Walla Walla State Penitentiary. Were these inmates good umpires? Let’s put it this way: nobody argued with them, that’s for sure.


How Can I Count the Number of Times a User has Logged on to a Computer?

Hey, Scripting Guy! How can I count the number of times a user has logged on to a computer?

-- DE

Hey, DE. How can you count the number of times a user has logged on to a computer? Good question and, ultimately, the answer might be: you can’t. But let’s at least explore some possible solutions and see if any of them will help.

First of all, let’s draw a distinction between logging on to a computer and logging on to a domain. If you’re running Active Directory, it’s possible to determine the number of times a user has logged on to the domain; that’s because the user account object includes a property – LogonCount – that keeps track of this very thing.

The one catch here is that the LogonCount property is not replicated between domain controllers. Does that matter? Well, if you have only one domain controller then, no, it doesn’t matter at all. If you have more than one domain controller, however, you’ll need to bind to each of these computers, retrieve the logon count, and then add those numbers together to find out how many times a user has logged on to a domain. In other words, Ken Myer might have been authenticated 5 times by domain controller A and 3 times by domain controller B. You need to add those two numbers – 5 and 3 – to determine that Ken has logged on to the domain 8 times.

Incidentally, here’s a script that binds to the domain controller atl-dc-01 and echoes the LogonCount value for the user Ken Myer:

Set objUser = GetObject _
    ("LDAP://atl-dc-01/cn=ken myer, ou=Finance, dc=fabrikam, dc=com")
Wscript.Echo objUser.LogonCount

Again, you’ll need to repeat this script for each of your domain controllers in order to get an accurate count of Ken Myer’s logons.

Of course, you specifically asked about counting the number of times a user has logged on to a computer. That’s a bit trickier; unfortunately, the WinNT provider – which is used to manage local user accounts and Windows NT 4.0 domain accounts – doesn’t support the LogonCount property. Because of that, the only way we know of to count user logons is to query the Security event log. If you have enabled auditing for user logons, each time a user successfully logs on to a computer an event (with an event code of 528) is recorded in the Security event log. To find out how many times Ken Myer has logged on to a computer we simply need to query the Security event log for all events where the EventCode is equal to 528 and the User is fabrikam\\kmyer (and yes, both \’s are required in the query):

strComputer = "."
Set objWMIService = GetObject("winmgmts:{(Security)}\\" & _
        strComputer & "\root\cimv2")

Set colEvents = objWMIService.ExecQuery _
    ("SELECT * FROM Win32_NTLogEvent WHERE LogFile = 'Security' AND " & _
        "EventCode = 528 AND User = 'fabrikam\\kmyer'")

Wscript.Echo colEvents.Count

This will work, although there are some caveats. First of all, if you haven’t enabled auditing these records won’t get written to the Security event log. Second, any time you clear the event log all events are, well, cleared. As a result, all logon counts for all users will effectively get set back to 0. If you want to keep a running tally of user logons, you’ll either have to never clear your event logs (not recommended) or you’ll have to keep track each time you do clear the event logs. It’s not an impossible task, but it’s also not as easy as it probably could be. But it’s about the only option open to us.


How Can I Get Just the User Name When Using the Win32_ComputerSystem Class?

Hey, Scripting Guy! I’m using the Win32_ComputerSystem class to get the name of the logged-on user. The name comes back formatted like this: Domain\Name. How can I extract just the user name?

-- KW

Hey, KW. As our much-beloved and highly-respected editor reminded us the other day, instead of spending all our time telling people how to extract paragraphs formatted with a specific style from Word documents we promised that every now and then we’d take time to address more basic questions. Having made that promise we then just as quickly forgot about it. (But, in our defense, it’s awfully hard to resist the thrill and excitement that comes with extracting paragraphs formatted with a specific style from Word documents.)

So we’ll make amends today. As many of you know, you can use a simple little script like this to determine the user currently logged-on to a computer:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_ComputerSystem")

For Each objItem in colItems
    Wscript.Echo objItem.UserName
Next

Note. One caveat. This works pretty good on Windows XP and Windows Server 2003. On Windows 2000, however, it always returns Null unless an administrator is logged on to the computer. Therefore, this is not the answer to the age-old question, “How can I determine who’s logged on to a particular computer?” (Unless, of course, all your users happen to be local administrators.) Unfortunately, there’s really not a good answer to that question.

As KW noted, though, the value of the UserName property comes back looking like this:

FABRIKAM\kenmyer

That’s fine if you want both the domain name (FABRIKAM) and the user name (kenmyer). But what if you want only the user name? How can you separate the user name from the domain name?

Believe it or not, this is pretty easy. With the UserName property the domain name and the user name will always be separated by a \. This, by the way, is true even if the user is logged on locally; in that case the computer name and the user name will be separated by a \:

ATL-WS-01\kenmyer

Because of that we can use the VBScript Split function to split the UserName into an array; all we have to do is tell the Split function to split the name at the \. That will give us an array consisting of two pieces: the domain name and the user name. All we have to do then is echo back the second piece – the user name – and we’re in business.

Confused? Let’s take a look at the script and then we’ll walk you through it:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer &
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From
Win32_ComputerSystem")

For Each objItem in colItems
arrName = Split(objItem.UserName, "\")
Wscript.Echo "Domain: " & arrName(0)
Wscript.Echo "Name: " & arrName(1)
Next

We begin by connecting to the WMI service on the local computer. (Although we could just as easily connect to the WMI service on a remote computer. To do that just change the value of the variable strComputer to the name of the remote computer.) We then use the ExecQuery method to retrieve all instances of the Win32_ComputerSystem class (there will be only one such instance).

Next we set up a For Each loop to walk through all the instances of the Win32_ComputerSystem class. In the first script we showed you – the one that echoed both the domain name and the user name – all we did in this loop was echo the value of the UserName property. This time we’re going to do things a little different: we’re going to use the Split function to create an array out of the UserName property. This line of code splits the UserName value at the \ mark, and creates an array named arrName:

arrName = Split(objItem.UserName, "\")

Incidentally, the Split function will split a string as many times as needed; that is, as many times as it finds the target character (in this case, the \). For example, suppose we had the following string:

element_1\element_2\element_3\element_4

If we run the Split function against this string our resulting array will actually have four elements:

element_1
element_2
element_3
element_4

With the UserName property we’ll end up with a two-element array, something like this:

FARBRIKAM
kenmyer

In an array, the first element (FABRIKAM) is always element 0; the second element (kenmyer) is always element 1. To echo back the domain name, we simply echo back the value of element 0; to echo back the user name, we simply echo back the value of element 1. That’s what we do here:

Wscript.Echo "Domain: " & arrName(0)
Wscript.Echo "Name: " & arrName(1)

That’s all we have to do: we can now echo back just the domain name, just the user name, or both. And best of all, our editor will quit bugging us about addressing basic scripting questions.

At least for awhile anyway.


How Can I Create a Logon Script That Will Open a User’s Mapped Drives?

Hey, Scripting Guy! How can I create a logon script that will open a user’s mapped drives?

-- KZ

Hey, KZ. You know, a lot of people think we decided to write this column because we wanted yet another way to tell people about scripting. That’s partially true; however, another reason there’s a Hey, Scripting Guy! column is because the Scripting Guy who writes the column has a habit of stumbling across little bits of trivia that he finds absolutely fascinating. The problem with that? No one else ever seems to be as fascinated as he is. And that’s really why he writes this column. In a face-to-face conversation people can sigh, roll their eyes, or spray him with Mace when he takes off on one of his boring tangents. In print, however, he can get away with boring people all he wants. (No; that’s true; haven’t you ever read anything by William Shakespeare?)

With that in mind, you probably know that, on the night that the Titanic sank, the band heroically played on, trying to calm and comfort the doomed passengers as best they could. But here’s something you might not have known. How did the White Star Line, owners of the Titanic, reward those musicians for their courage and their heroism? That’s right: by billing their surviving family members for the lost band uniforms! True story.

Did you hear that? It sounded like several thousand people sighing through the computer speakers. Weird.

Note. In case you’re wondering, no, Microsoft would never bill the Scripting Families for lost uniforms should the Scripting Guys be lost at sea. Granted we don’t actually wear uniforms, but they wouldn’t do that anyway.

Probably.

As a matter of fact, KZ, we do have plenty more anecdotes we could relate to you. But, on the off chance that someone has figured out a way to Mace daily columnists via the Internet, maybe we should take a moment to show you a script that can open a user’s mapped drives:

On Error Resume Next

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_MappedLogicalDisk")

Set objShell = CreateObject("Wscript.Shell")

For Each objItem in colItems
    strDrive = objItem.DeviceID
    objShell.Run(strDrive)
Next

Let’s begin at the beginning. The very first line in the script is On Error Resume Next. Typically we don’t use any kind of error-handling in our sample scripts; that’s because we want to keep our scripts as short and sweet as possible (just like Greg). In this case, however, we decided to make an exception. We don’t know how it is in the rest of the world, but here at Microsoft networks occasionally go down, meaning that network resources (like mapped drives) aren’t always available. By using On Error Resume Next we keep the script from crashing should one of the user’s mapped drives not be available.

Good question: how do we even know which of the user’s drives are mapped drives? Let’s put it this way: if you’ve ever wondered what the WMI class Win32_MappedLogicalDisk is used for, well, now you know: this class returns a collection of all the mapped drives for the logged-on user. To get at this information all we have to do is connect to the WMI service on the local computer (because this is a logon script, we don’t care about connecting to remote computers) and then use this line of code to retrieve the collection of mapped drives:

Set colItems = objWMIService.ExecQuery("Select * From Win32_MappedLogicalDisk")

Pretty easy, huh?

Best of all, opening each of these drives is just as easy. What we’re going to do is open the drives exactly the same way we would open these drives from the Run dialog box. For example, to open drive Z: from the Run dialog box we’d simply type the following and then press ENTER:

Z:

We’re going to do the exact same thing here, only programmatically.

To do that, we first create an instance of the Wscript.Shell object; this is the scripting object that allows us to run executable files (like Explorer.exe) from within a script. We then set up a For Each loop that walks through the collection of mapped drives (we need to use a For Each loop regardless of whether the user has one mapped drive, 10 mapped drives, or even 0 mapped drives). Inside that loop we use this line of code to grab the DeviceID for each of these mapped drives; fortunately for us, the DeviceID just happens to be the drive letter for each mapped drive (e.g., Z:):

strDrive = objItem.DeviceID

Once we know the drive letter we can then use the Run method to open the mapped drive:

objShell.Run(strDrive)

That’s all we have to do. We then loop around and repeat the process with any other mapped drives in the collection.

Hmmm, that’s odd: the White Star Line just sent us a bill for that script, even though we were the ones who wrote the script.

Now here’s a bonus script for you, KZ. Depending on your setup, you might not want to open all the mapped drives associated with a user; instead, you might only want to open that user’s home drive (as configured in Active Directory). If that’s the case, here’s a script that will open just the home drive for the logged-on user:

On Error Resume Next

Set objShell = CreateObject("Wscript.Shell")

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://"
& strUser)

strDrive =  objUser.homeDrive
objShell.Run(strDrive)

This time around we use the ADSystemInfo object, which provides basic information about the logged-on user and the computer he or she is logged on to. After we create the ADSystemInfo object we can then use this line of code to retrieve the user’s distinguished name (e.g., cn=Ken Myer, OU=Finance, dc=fabrikam, dc=com), a name we can get from the UserName property:

strUser = objSysInfo.UserName

With the distinguished name in hand we can then use the GetObject method to bind to the user account in question. At that point we simply need to retrieve the value of the homeDrive property and then, again, use the Run method to open the drive:

strDrive =  objUser.homeDrive
objShell.Run(strDrive)

And there you have it, KZ. Now, back to the amusing anecdotes. Although everyone has heard of Genghis Khan, most people aren’t aware that –

Oh; OK. Sorry, folks, but the White Star Line has said we’ve used up our quota of Web space for today. We’re afraid we won’t be able to tell you any more interesting little stories, at least not for now.

Note. Wow, a standing ovation; we didn’t know you could do that over the Internet. Thanks, guys! (Even if it was for the White Star Line and not us.)


How Can I Determine the Drive Label of a Drive?

Hey, Scripting Guy! Given the drive letter, how can I determine the drive label?

-- PM

Hey, PM. You know, after sitting around all winter, checking his watch every 15 minutes or so just to verify the date and time, the Scripting Guy who writes this column is pleased to announce that baseball season is officially here. Once more he can enjoy the Great American Pastime. Once more he can help the youth of America learn the value of teamwork and sportsmanship. Most important of all, once more he can leave work an hour or two early every day either to go coach his team or to watch his son play for the local high school team.

And you wondered why he likes baseball so much.

In other words, our Scripting Columnist is in a good mood today. Of course, that might not last long: after all, this year’s Kirkland Fire team is unlikely to annihilate all the other teams in the league, like they did last season. Today, however, we’re feeling good, and to celebrate we’ve decided to not only answer your question, but to answer a second question, one you didn’t even ask. Is that service or what?

First things first. You’d like to know how, given a drive letter, you can determine the drive label for a drive. Well, how about by using a script like this one:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DeviceID = 'D:'")

For Each objItem in colItems
    Wscript.Echo objItem.VolumeName
Next

And, yes, it is a very simple little script. (Hey, it’s baseball season: we don’t have time for writing scripts and otherwise doing our work!) We start off by connecting to the WMI service on the local computer; we could also perform this same chore on a remote machine just by assigning the name of that computer to the variable strComputer. We then use this line of code to return a collection of all the drives on the computer that have a DeviceID of D:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DeviceID = 'D:'")

Because drive letters must be unique on a computer we know that our collection will have, at most, just one item: drive D (which is just what we wanted). To determine the drive label all we have to do is set up a For Each loop to walk us through this one-item collection; inside that loop we echo back the value of the VolumeName property:

Wscript.Echo objItem.VolumeName

That’s it. If you want to switch this around – that is, given a drive label determine the DeviceID – well, all you have to do is run this script:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where VolumeName = 'Data Drive'")

For Each objItem in colItems
    Wscript.Echo objItem.DeviceID
Next

Cool, huh?

But wait: don’t go yet. After all, that was just a third bonus answer on top of the bonus answer we promised to give you (you know, the answer to the question you never even asked). Having solved the problem of determining the drive label for a drive the next logical question is this: can I use a script to change the drive label for a drive? If it wasn’t baseball season this would probably be our answer: “No, you can’t. Now go away and leave us alone.” But seeing as how this is baseball season our answer is a little different: “Of course you can do this. And we’d be more than happy to show you how.”

In fact, here’s how.

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DeviceID = 'D:'")

For Each objItem in colItems
    objItem.VolumeName = "Data Drive"
    objItem.Put_
Next

As you can see, this script is remarkably similar to the first script we showed you. In fact, the only difference is what goes on inside the For Each loop:

objItem.VolumeName = "Data Drive"
objItem.Put_

What are we doing this time around? Well, this time around we’re assigning a new drive label (in this case, Data Drive) to the VolumeName property; we can do that because VolumeName happens to be a read-write value. After that we simply call the Put_ method (note the underscore at the end of the method name) to write the changes to the drive. Drive D will now have a brand-new label: Data Drive. It just doesn’t get any better than that, does it?

And now, if you’ll excuse us, we have a … meeting … to go to. Right. A meeting ….


How Can I Determine the Next Available Drive Letter on a Computer?

Hey, Scripting Guy! How can I determine the next available drive letter on a computer?

-- TW

Hey, TW. Interesting question, and one that crops up from time-to-time. In the old days, when users only had drives A, B, and C, this was easy; you could pretty much guess that any drive letter other than those three would be available. Nowadays, though, things are different: not only does increased networking means it’s more likely that users already have a drive or two mapped, but the proliferation of CD-ROM and DVD drives, USB drives, portable hard drives and other similar devices almost ensures that a computer has more than just drives A, B, and C. So in the midst of this high-tech alphabet soup, how can you determine which drive letters are available and which ones aren’t?

Actually, this is easier than you might think. To begin with, there are only 23 letters we need to worry about; we can forget about drives A, B, and C. (Drives A and B, of course, are reserved for floppy disks, and the first hard drive in a computer will always be drive C.) To find the next available drive letter, we can start with drive D, then drive E, then drive F, continuing down the line until we either find an available letter, or until we reach drive Z.

Of course, to do that, we also need to know which drive letters are in use on a computer. Fortunately, that’s really easy: we can return a collection of in-use drive letters by using the Win32_LogicalDisk class and checking the DeviceID property. Thus:

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

Set colDisks = objWMIService.ExecQuery("Select * from Win32_LogicalDisk")

For Each objDisk in colDisks
    Wscript.Echo objDisk.DeviceID
Next

On our test computer, we get back the following:

C:
D:
E:
G:
Z:

Obviously we can look at this data and easily determine the next available drive letter. But why should we have to do all that work? Didn’t someone say something about getting the script to determine the next available drive letter?

You bet we did. Here’s a script that can determine the next available drive letter on a computer. Let’s take a look at the script, and then we’ll explain how it works:

Set objDictionary = CreateObject("Scripting.Dictionary")

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

Set colDisks = objWMIService.ExecQuery("Select * from Win32_LogicalDisk")

For Each objDisk in colDisks
    objDictionary.Add objDisk.DeviceID, objDisk.DeviceID
Next

For i = 67 to 90
    strDrive = Chr(i) & ":"
    If objDictionary.Exists(strDrive) Then
    Else
        Wscript.Echo strDrive & " is the next available drive letter."
        Wscript.Quit
    End If
Next
Wscript.Echo "There are no available drive letters on this computer."

We start out by creating an instance of the Dictionary object, a scripting object that makes it easy to store data in memory and – equally important – makes it easy to determine which pieces of data are stored in memory and which ones aren’t. (For a more detailed discussion of the Dictionary object, see the Scripting Runtime Primer in the Microsoft Windows 2000 Scripting Guide.) We then use the Win32_LogicalDisk class to retrieve a list of all the drive letters (DeviceIDs) currently in use on the computer. However, this time we’re not going to echo the information back to the screen; instead, we’re going to store it in our Dictionary object. That’s what this line of code does:

objDictionary.Add objDisk.DeviceID, objDisk.DeviceID

Elements in a dictionary are composed of two parts: the key and the item. For this particular script we really only need the key (which provides a way for us to quickly determine whether information exists in the dictionary), but the Dictionary object requires both pieces. Therefore, we simply set both the key and the item to the drive letter; hence the identical objDisk.DeviceID parameters passed to the Add method.

When the script finishes looping through the collection of drive letters, we’ll have a dictionary consisting of the following keys (and items):

C:
D:
E:
G:
Z:

Now that we have a list of all the drive letters currently in use on the computer, all that’s left to do is to find the next available drive letter. That’s what this block of code does:

For i = 67 to 90
    strDrive = Chr(i) & ":"
    If objDictionary.Exists(strDrive) Then
    Else
        Wscript.Echo strDrive & " is the next available drive letter."
        Wscript.Quit
    End If
Next
Wscript.Echo "There are no available drive letters on this computer."

Don’t worry; we’ll explain it. As we noted earlier, we need to check letters D through Z to see if any of these babies are available for drive mapping. A really easy way to do this is to create a For Next loop that loops through the ASCII values of these letters. As it turns out, the ASCII value of an uppercase D is 67 and the ASCII values of an uppercase Z is 90. Thus our For Next loop runs from 67 to 90:

For i = 67 to 90

ASCII values are perfect for a For Next loop, but not so perfect for working with drive mappings (very few computers have, say, a drive 81:). Therefore, in our next line of code we use the Chr function to convert the ASCII value back to its character equivalent. For example, when the loop first runs, i (our loop variable) will be equal to 67. The Chr function will take this value (67) and convert it to an uppercase D. We tack a colon on the end, and our variable strDrive will then hold the value D:, something which looks an awful lot like a drive letter.

The rest is child’s play. (Well, assuming your child knows something about VBScript.) We use the Dictionary object’s Exists method to see if the first drive letter – D: -- exists in the dictionary. Remember, the dictionary contains a list of all the drive letters that are not available for use. If D: is found in the dictionary, then that means a drive is already using that letter. In that case, we loop around and try the next drive letter (E:).

What happens if we find a drive letter that isn’t in the dictionary? Well, that means that the drive letter is available to us. So, we do two things. First, we echo a message indicating that this drive letter is available. Second, we use the Quit method to exit the script. Why exit the script at this point? Well, we’ve found the next available drive letter, and that’s all we wanted; there’s no reason to keep checking letters. So we exit and get on with our lives.

And what if no drive letter is available? If that happens, we leave the For Next loop and then run the last line of the script, which simply says that no drive letter is available. Because we exit the script as soon as an available drive letter is found, this line of code executes only if our search for a drive letter has failed.


How Can I Determine Which Drives are Mapped to Network Shares?

Hey, Scripting Guy! How can I determine which drives on a computer are mapped to network shares?

-- CP

Hey, CP. OK, you better sit down, because what we’re about to say will shock you: if you want to determine which drives on a computer are mapped to network shares, don’t use the Win32_MappedLogicalDisk class.

Oh, look: you spilled coffee on everything. Didn’t we tell you to sit down? Here, here’s some paper towels.

While you clean up – don’t forget that little puddle by the phone – we’ll explain why. The truth is, you can use the WMI class Win32_MappedLogicalDisk to determine which drives are mapped to network shares. The problem, however, is that while this class will return the drive letters for these drives, it won’t tell you which shares the drives are mapped to. Weird, but true.

And so it’s Win32_LogicalDisk to the rescue. With this class not only can we determine which drives are mapped to network shares, but we can also determine which network shares they map to. And to do that requires nothing more than a script like this:

strComputer = "."

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

Set colDrives = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DriveType = 4")

For Each objDrive in colDrives
    Wscript.Echo "Drive letter: " & objDrive.DeviceID
    Wscript.Echo "Network path: " & objDrive.ProviderName
Next

Stop us if you’ve heard this one before, but the script starts out by connecting to the WMI service on the local computer. (As usual, you can modify this script to run against a remote computer simply by assigning the name of that machine to the variable strComputer.) We then use this line of code to return a collection of all the mapped network drives:

Set colDrives = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DriveType = 4")

The key here – as you might have guessed – lies in our Where clause. We’re asking for all instances of the class where the DriveType is equal to 4; needless to say, a DriveType equal to 4 represents a mapped network drive. (For other DriveType values, see the WMI SDK on MSDN.) The query returns a collection of all the mapped drives; we then set up a For Each loop to walk through that collection. For each mapped drive we echo the value of two properties: DeviceID, which returns the drive letter for the drive; and ProviderName, which returns the network share the drive is mapped to.

In other words, we get back information similar to this:

Drive letter: E:
Network path: \\atl-fs-01\public
Drive letter: F:
Network path: \\atl-fs-01\finance
Drive letter: G:
Network path: \\atl-fs-01\users\kenmyer

That’s OK, we understand: as useful as this script is, it still comes as a bit of a shock that we didn’t use the Win32_MappedLogicalDisk class. Listen, this has been kind if rough on you: why don’t you take the rest of the day off? Just tell your boss the Scripting Guys said it was OK.

Legal disclaimer. On the advice of our attorneys (and, trust us, the Scripting Guys need their own attorneys), we did not say it was OK. (But if you want to try it anyway, well, good luck.)


How Can I Find All the Drives Mapped to a Share and Remap Them?

Hey, Scripting Guy! How can I find all the drives mapped to \\server1\share and remap them to \\server2\share?

-- H T-S

Hey, H T-S. You know, not too long ago Malcolm Gladwell published a book called The Tipping Point. Loosely speaking, the book posited that something could be ignored for the longest time, at least until it reached the so-called tipping point; upon doing so, this thing that nobody cared about would suddenly became a veritable craze. Seemingly overnight it would go from being something you never heard of to something that seems to show up everywhere you look.

It’s an interesting conjecture, and we seem to be seeing this phenomenon when it comes to scripts that map and unmap network drives. For over a year we published the Hey, Scripting Guy! column without mentioning network drives, and nobody seemed to notice. And then, suddenly, we were deluged with questions about mapping and unmapping network drives. We answered the first such question a couple weeks ago; now we’re back to answer another one, and we have an Inbox full of additional questions about network drives. First hula hoops, then bell-bottom pants, now network drives. Go figure.

So what about remapping network drives? Well, for better or worse there’s no RemapNetworkDrive method that can take care of thus automatically; therefore, we have to come up with a minor workaround. But that’s not too bad: we can carry out this task by locating any drives that meet the criteria, unmapping those drives, then remapping each drive to the new location.

Sure, it sounds complicated, but it’s really pretty easy. Here’s a script that looks for any drive mapped to \\server1\share and remaps it to \\server2\share:

Set objNetwork = CreateObject("Wscript.Network")

Set colDrives = objNetwork.EnumNetworkDrives

For i = 0 to colDrives.Count-1 Step 2
    If colDrives.Item(i + 1) = "\\server1\share" Then
        strDriveLetter = colDrives.Item(i)
        objNetwork.RemoveNetworkDrive strDriveLetter
        objNetwork.MapNetworkDrive strDriveLetter, "\\server2\share"
    End If
Next

The script starts off by creating an instance of the Wscript.Network object. We should note that we need to use Windows Script Host any time we want to map or unmap network drives; that’s because WMI doesn’t have any methods for mapping or unmapping drives. That’s fine, except it means that our script must be run on the local computer: WSH methods typically can’t be used against remote machines. That’s a limitation you’ll just have to deal with. (One way to deal with it: run the script as a logon script. Logon scripts always run locally.)

After creating the Network object we then call the EnumNetworkDrives method in order to return a collection of all the mapped network drives on the computer:

Set colDrives = objNetwork.EnumNetworkDrives

That brings us face-to-face with that curious little animal known as the mapped network drives collection. We won’t detail the architecture of this collection today; for that, see our previous column on network drives. Suffice to say that each mapped drive actually takes up two items in the collection: the first item for the drive letter, the second for the UNC path. If we have three mapped drives on a computer our collection will look something like this:

X:
\\server1\share1
Y:
\\server2\share2
Z:
\\server3\share3

That’s why we have to use such a crazy-looking For Next loop in order to walk through the collection; this line of code causes us to skip every other item in the collection, in turn ensuring that we land only on the drive letters:

For i = 0 to colDrives.Count-1 Step 2

For each drive letter we then need to determine whether the corresponding UNC path is equal to \\server1\share1. Remember, if we’re looking at item 0 in the collection (the first item in the collection has an index number of 0) then we’re looking at a drive letter; the corresponding UNC path will be that index number (0) plus 1. Therefore, we use code like this to determine whether or not the first drive happens to be mapped to \\server1\share1:

If colDrives.Item(i + 1) = "\\server1\share" Then

Let’s assume that it is. In that case, we need to grab the drive letter (item 0) and store that value in a variable named strDriveLetter. We then call the RemoveNetworkDrive method to unmap the drive, then call the MapNetworkDrive method to remap that same drive letter to the new share:

objNetwork.MapNetworkDrive strDriveLetter, "\\server2\share"

And no, they don’t call it the “tipping point” because you just tipped over trying to follow all this. We know it’s a bit confusing, but that’s because of the strange way in which the mapped network drives collection is constructed. If this doesn’t make any sense to you, try drawing it out in a flowchart of some kind; you should see that there is a logic to the whole thing. A somewhat twisted logic, perhaps, but a logic nonetheless.

Because this is confusing we went with the simplest possible example: a share named \\server1\share is remapped to a share named \\server2\share. It’s also possible to remap any share on server1 to any similarly-named share on server2. We thought that was a bit much for today, however. If that’s of interest to you, though, just let us know, and we’ll revisit the topic in the near future.


How Can I Map a Drive Based on a User’s Name and OU?

Hey, Scripting Guy! I need to create a logon script that maps a network drive based on the logged-on user’s user name and OU. How can I do that?

-- KB

Hey, KB. You know, KB, this is probably the toughest day of the year for the Scripting Guy who writes this column. For the past four days – Thursday, Friday, Saturday, and Sunday – he’s done nothing but watch college basketball. And not only has he seen some incredible games (Ohio State vs. Xavier and Washington State vs. Vanderbilt, to name just two) but he also got to witness both Duke and Arizona being eliminated in the opening round. For awhile there, he assumed that he must have died and gone to heaven.

Note. In case you’re wondering, most theologians agree that heaven would not be heaven if either the Duke or Arizona basketball teams were there. The same is true of Oregon, although that goes without saying.

But all his fun came to a crashing halt come Monday morning and time to go back to work.

That’s why this is such a tough day for the Scripting Guy who writes this column; as difficult as this is to believe, he doesn’t find work anywhere near as interesting and exciting as he finds college basketball. Of course, you’re probably thinking, “We understand how he feels. On the other hand, the Scripting Guy who writes that column is a true professional: somehow he’ll reach down deep inside and find a way to do his work even though he’d much rather be sitting at home watching even more college basketball. Are we right or are we right?”

Well, to be honest, you’re wrong: the Scripting Guy who writes this column is anything but a true professional. On the other hand, he does have a mortgage payment due at the end of the month. With that in mind, here’s a logon script that can map a network drive based on the logged-on user’s user name and OU:

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser =
GetObject("LDAP://" & strUser)

strUserName = objUser.samAccountName

strOUPath = objUser.Parent
arrContainers = Split(strOUPath,
",")
arrOU = Split(arrContainers(0),
"=")
strOU = arrOU(1)

strDrive = "\\Mission\Apps\Timesheets\"
& strOU & "\" & strUserName

Set objNetwork
= CreateObject("Wscript.Network")
objNetwork.MapNetworkDrive "K:", strDrive

Can we explain how this script works? That’s a good question; before we answer that let’s check and see how much money is still owed on the Scripting House …. Holy smokes!

OK, it’s safe to say that, yes, we can explain how this script works. In fact, by the looks of things we’ll be explaining how scripts work for the next 100 years or so.

As you can see, we start off by creating an instance of the ADSystemInfo object; this is a handy little ADSI object that directly – and instantly – retrieves information about the logged-on user and the computer he or she is using. What kind of information does the ADSystemInfo object retrieve? Well, for one thing, it can – via the UserName property – retrieve the distinguished name of the logged-on user; that’s going to be a value similar to this:

CN=kenmyer, OU=Finance, dc=fabrikam, dc=com

Is that a useful piece of information to have? You bet it is; once we know the user’s distinguished name we can then use this line of code to bind to that user’s Active Directory account:

Set objUser = GetObject("LDAP://" & strUser)

And once we bind to the user account we can then determine both the user’s logon name and his or her OU. Which just happens to be the very information we need in order to map the drive.

Let’s do the easy part first: determine the user’s logon name. That’s something we can do by grabbing the value of the samAccountName property (e.g., kenmyer) and storing it in a variable named strUserName, like so:

strUserName = objUser.samAccountName

Like we said, that was pretty easy. Unfortunately, retrieving the name of the user’s OU is a little bit trickier; that’s because there is no OU attribute that tells us which OU an account lives in. About the best we can do is use the Parent attribute to determine the full ADsPath to that OU. In other words, the Parent attribute returns a value similar to this:

LDAP://OU=Finance, dc=fabrikam, dc=com

But that doesn’t help us, does it?

Oh, wait: as a matter of fact it does help us. As you can see, the name of the user’s OU – Finance – is embedded in the ADsPath. We just need to figure out a way to extract that OU name.

Although there are probably a number of different ways we could do that, here’s the approach we took. To begin with, we use this line of code and the VBScript Split function to split the OU’s ADsPath on the comma:

arrContainers = Split(strOUPath, ",")

Why do we do that? Well, what the Split function does is look for the specified delimiter (in this case, the comma) and separate the string value into an array of values, creating a new array value each time it encounters the delimiter. That means that a value like LDAP://OU=Finance, dc=fabrikam, dc=com will be magically transformed into an array consisting of the following elements:

· LDAP://OU=Finance

· dc=fabrikam

· dc=com

As you can see, we’re getting closer to our goal. With that in mind, our next step is to create a second array by splitting the very first item (item 0) on the equals sign. In other words, we’re going to take the value LDAP://OU=Finance and create a teeny-tiny ltitle array consisting of the following two items:

· LDAP://OU

· Finance

That’s what this line of code does for us:

arrOU = Split(arrContainers(0), "=")

Amazingly enough, we now know which OU the user account resides in: it lives in the Finance OU. And guess what? The name of that OU just happens to be the second time (element 1) in the array arrOU. That means we can then use this line of code to store the user’s OU name in the variable strOU:

strOU = arrOU(1)

At this point we only have to do two more things: we need to piece together the path to the shared folder we want to map, then we need to go ahead and map the drive. To piece together the path we use this line of code:

strDrive = "\\Mission\Apps\Timesheets\" & strOU & "\" & strUserName

It shouldn’t be too hard to see what’s going on here. In KB’s case, users have folders that reside on the server \\Mission, in the Apps\Timesheets folder. Within the Timesheets folder are separate folders for each OU, and within those folders are separate folders for each user. Therefore we’re simply combining the following values and storing the resulting string in a variable named strDrive:

· \\Mission\Apps\Timesheets\

· Finance

· \

· kenmyer

In other words, strDrive is going to be equal to this:

\\Mission\Apps\Timesheets\Finance\kenmyer

And as soon as we have the folder path we can then use this little block of code to map drive K to the appropriate folder:

Set objNetwork = CreateObject("Wscript.Network")
objNetwork.MapNetworkDrive "K:", strDrive

That’s all we need to do.

No doubt you’re wondering whether the Scripting Guy who writes this column really spent the entire weekend doing nothing but sitting in front of the TV watching basketball. The answer to that is yes, he did. We realize that that’s an almost-unbelievable feat of stamina and dedication; it’s amazing that a human being could do absolutely nothing for so long. In all fairness, however, it must be admitted that the Scripting Guy who writes this column cheated a little bit. After all, he lives in the Seattle area, and with it raining all day Saturday and most of the day Sunday, well, staying inside and watching TV wasn’t all that hard to do.

By the way, did we mention that Kentucky has been eliminated, too? You know, maybe this is heaven after all. And if the Scripting Guys are still here and are still sitting in their same offices that can only mean one thing: Microsoft, having passed on YouTube, decided to buy heaven instead.

Note. Feel free to insert your own joke about Microsoft, buying heaven rather than buying … well, some other place ….


How Can I Map a Drive, Copy a File to That Drive, and Then Unmap the Drive?

Hey, Scripting Guy! How can I map a drive, copy a file to that drive, and then unmap the drive?

-- ZK

Hey, ZK. You know, sometimes these questions are difficult for us to answer, simply because we don’t usually find ourselves performing the task being asked about. Remove every other line in a text file? Well, we’ve never done that, but we can probably figure it out given a little time and effort. Take all the data found in a column in an Excel spreadsheet and convert that to an array? Same thing: we don’t have an answer for you right off the top of our heads, but we’re pretty sure we can sit down and come up with something.

Ah, but this question is different. Perform some chore and then almost immediately have to undo it? That’s something we have plenty of experience with:

Set objNetwork = CreateObject("Wscript.Network")
objNetwork.MapNetworkDrive "Z:", "\\atl-fs-01\Public"

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.CopyFile "C:\Scripts\Test.txt", "Z:\"

objNetwork.RemoveNetworkDrive "Z:"

As you can see, this is probably much easier than you thought it would be. (You have to assume that if the Scripting Guys know how to do it it must not be too-terribly complicated.) We start out by creating an instance of the Wscript.Network object, the scripting object needed to map a drive on a computer. (We should add that the Network object lets you map a drive on only the local computer; you can’t modify this script to map a drive on a remote machine.) We then use this line of code to map drive Z to the UNC path \\atl-fs-01\Public:

objNetwork.MapNetworkDrive "Z:", "\\atl-fs-01\Public"

That’s a good question: what if drive Z is already mapped to some other location? Well, in that case, the script will fail; you can’t map a drive to a drive letter that’s already in use. But don’t worry; if that’s a concern just take a look at this Hey, Scripting Guy! column, which shows you how you can determine (and use) the next available drive letter in a script.

After the drive is mapped we’re ready to copy the file to that location. To do that we create an instance of the Scripting.FileSystemObject, then use the CopyFile method to copy the file C:\Scripts\Test.txt to drive Z. That’s what we do with these two lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.CopyFile "C:\Scripts\Test.txt", "Z:\"

And that’s a good point: if the file Test.txt already exists on drive Z then the script will fail here, too; that’s because, by default, the FileSystemObject won’t overwrite an existing file. But, again, that’s nothing to worry about: just add the optional parameter True to enable the FileSystemObject to overwrite an existing copy of the file. If it’s OK to copy over Test.txt then just change the CopyFile command to look like this:

objFSO.CopyFile "C:\Scripts\Test.txt", "Z:\", True

Believe it or not, we’re almost done. (Man, we really do like answering questions like this!) All that’s left is to unmap drive Z, something we can do by calling the RemoveNetworkDrive method and specifying the drive letter of the drive to be removed:

objNetwork.RemoveNetworkDrive "Z:"

And that’s it. If you take a look at your local computer you won’t see a drive Z. And if you take a look at \\atl-fs-01\Public you should see a copy of Test.txt. We ended up exactly where we wanted to end up.

Which, now that you mention it, is a bit unusual for us, isn’t it?

So do the Scripting Guys really find themselves undoing things they just did or were we just being facetious? Let’s put it this way. Awhile back the Scripting Guy who writes this column sat down to answer a question. Not only did he know the answer to the question, but he found the column amazingly easy to write: the words just flowed. Moments before he published the article he discovered why the column was so easy to write: just a few weeks before he had answered that very same question and written that exact same column. Isn’t that … amusing ….

Fortunately the Scripting Guy who writes this column is used to stuff like that. Does something need new batteries? Then it’s a pretty safe bet that we will take the cover off the thing, remove the old batteries, and carefully replace the cover. It’s only then that he’ll realize the new batteries are still sitting there. Does he need to run to the store and get eggs (and only eggs)? You can probably guess how many trips to the store he’ll end up making before he comes back with eggs rather than, say, milk or bread. (Hint: One is not the correct answer.)

In other words, all in a day’s work.


How Can I Map Drive X on a Computer, But Only If Drive X isn’t Already in Use?

Hey, Scripting Guy! How can I map drive X on a computer, but only if drive X isn’t already in use?

-- PN

Hey, PN. You know, this is almost too good to be true (but, sadly, it is true). The Scripting Guy who writes this column was busy writing this column. And because today just happens to be Friday the 13th the theme for the day was, as you might expect, Friday the 13th.

In typical Hey, Scripting Guy! fashion, he began by recounting a little bit of the history of both Friday and of the number 13. For example, in both Hindu and Norse culture, it’s unlucky to invite 13 people over for dinner (especially if you’re the one who’s supposed to do all the cooking). In the Middle Ages 13 was considered bad because witches supposedly met in groups of 13: 12 witches plus the Devil.

Note. The Scripting Editor tells us that this is ridiculous; after all, her coven hardly ever has 13 people at a meeting.

Likewise, Fridays have long been viewed with a jaundiced eye. For example, tradition holds that Adam and Eve first tasted the forbidden fruit on a Friday; needless to say, that didn’t work out too well for the two of them. In ancient Rome Fridays served as Execution Day, something that was apparently so popular that the British adopted the idea and turned every Friday into Hangman’s Day.

Here at Microsoft we’ve carried on that tradition in equally-ominous fashion: Friday is a popular day for team meetings.

At any rate, the Scripting Guy who writes this column went on to relate that it was inevitable that Friday – the worst day of the week – would eventually be paired with 13, the unluckiest number. The net result? Friday the 13th, a day when something terrible is bound to happen. At that point, of course, the Scripting Guy who writes this column was about to dismiss this all as superstitious nonsense when it happened: his screen went black, the computer rebooted itself, and, upon restarting, displayed this message:

Non-system or disk error.

Uh-oh.

Believe it or not, in the middle of writing a column debunking the idea of Friday the 13th being unlucky our computer died. If it wasn’t our computer we’d find that kind of funny. (The Scripting Editor found it somewhat amusing.)

Note. No, we don’t believe this was karma, either. However, we aren’t going to say that just in case it was karma. We’re having enough problems today without adding bad karma to the mix.

So do we still think that this notion about Friday the 13th being unlucky is nothing but superstitious nonsense? Of course. After all, Friday the 13th might not have been particularly lucky for us (or at least for our computer), but it turned out to be plenty lucky for PN:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DeviceID = 'X:'")

If colItems.Count = 0 Then
    Set objNetwork = CreateObject("Wscript.Network")
    objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\public"
End If

As you can see, this is a pretty easy little script to write. (Which is good, seeing as we no longer have a computer on which to write it.) We start out, as we so often do, by connecting to the WMI service on the local computer. Could we run this same script on a remote computer? Well, maybe. It’s easy enough to determine whether drive X is in use on a remote machine; unfortunately, it’s a bit harder to map a drive on a remote machine. That’s because Windows Script Host, and the WSH Network object, are designed to work locally rather than remotely. (And yes, it is interesting that something named the Network object can’t function over the network, isn’t it?) On the other hand, you can always find workarounds for these things; for one suggestion, see this previous Hey, Scripting Guy! column.

After we make the connection we then use the ExecQuery method, and the following line of code, to return a collection of all the drives on the computer that have a DeviceID equal to X:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_LogicalDisk Where DeviceID = 'X:'")

Why do we do that? Well, this is a quick and easy way of determining whether drive X is already in use. If drive X is already mapped (to either a physical drive or a network drive) then we’ll end up with one item in our collection. If drive X is not in use (meaning that the drive letter is available to us) then we’ll have zero items in our collection.

With that in mind, our next step is to check the collection Count to determine whether any drive Xs are in use:

If colItems.Count = 0 Then

If the Count is equal to 0, then drive X is available. Therefore, we go ahead and create an instance of the Wscript.Network object and then use the MapNetworkDrive method to map the folder \\atl-fs-01\public to drive X:

Set objNetwork = CreateObject("Wscript.Network")
objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\public"

What if the Count isn’t equal to 0? Well, that can only mean one thing: drive X must already be in use. Therefore, and per PN’s wishes, we don’t do anything at all.

Note. If drive X is in use wouldn’t it be better to go ahead and map the drive to the next available drive letter? Maybe. If that’s something you’d like to do then you’re in luck, too: we just happen to have another column on drive mapping, this one showing how you can determine the next available drive letter on a computer.

Anyway, we hope that helps, PN. And like we said, don’t worry too much about it being Friday the 13th; theScripting Guys don’t believe that Friday the 13th is any worse than any other Friday. After all, being a Friday (13th or otherwise) meant that we had to come in to work today. That pretty much ruled out this being a lucky day right there, regardless of whether our not any of our computers died.


How Can I Map Drives Based on Membership in a Group?

Hey, Scripting Guy! How can I map drives in a logon script based on a security group each user belong to?

-- RO

Hey, RP. Considering the fact that this question has been asked by approximately 3,755,392 of our readers – oops, there’s another one; make that 3,755,393 of our readers – we decided it might be a good idea for us to answer it. So here goes.

There are two issues here. First, you need to determine which groups a user belongs to; second you need to map a drive based on the user’s membership in a particular group. Because there are two issues to deal with, let’s take our usual tack and show you how to do step 1, show you how to do step 2, and then bring them both together for a grand finale that actually performs the task in question.

For starters, you need to determine which groups the user belongs to; before you can actually do that however, you need to determine who the user is in the first place. Because you’re running this as a logon script, that’s actually pretty easy: you can use the ADSystemInfo object to determine the distinguished name of the user who just logged on:

Set objSysInfo = CreateObject("ADSystemInfo")
Wscript.Echo strUser.UserName

Why do we want the distinguished name as opposed to, say, the user’s logon name? Well, the distinguished name (which looks like CN=Ken Myer, OU=Finance, DC=fabrikam, DC=com) gives us a nearly-complete path to the user’s Active Directory user account; in fact, all we have to do is tack LDAP:// to the front and we’re on our way. A logon name like kmyer isn’t nearly as useful; if that’s all we have, we’d have to do an Active Directory search to determine the ADsPath for this account. Starting off with the distinguished name lets us bypass all those steps.

As soon as we tack on LDAP:// and construct an ADsPath we then bind to the user account in Active Directory and report back the groups the user belongs to; this can be done simply by enumerating the values in the MemberOf attribute. Thus:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")
Set objNetwork = CreateObject("Wscript.Network")

strUserPath = "LDAP://" & objSysInfo.UserName
Set objUser = GetObject(strUserPath)

For Each strGroup in objUser.MemberOf
    strGroupPath = "LDAP://" & strGroup
    Set objGroup = GetObject(strGroupPath)
    Wscript.Echo objGroup.CN
Next

Note that what we do here is construct an ADsPath for the group using this line of code:

strGroupPath = "LDAP://" & strGroup

We then bind to the group itself and echo the value of the CN. Why? Well, the MemberOf attributes returns the distinguished name of each group the user belongs to; thus you get names like CN=Finance Users, OU=Finance, DC=fabrikam, DC=com. That’s OK, but we’d rather just have a group name (i.e., a CN) like Finance Users. So we go ahead and bind to the group and grab the CN.

As for step 2, mapping a network drive, that’s pretty easy; this code maps drive X to the share \\atl-fs-01\finance:

Set objNetwork = CreateObject("Wscript.Network")
objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\finance"

All we have to do is create an instance of the WSH Network object, then call the MapNetworkDrive method, passing the method two parameters: the drive letter, and the file share we want to map to.

So here – at last! – is a script that determines the groups a user belongs to, and then maps drive X to the appropriate network share based on group membership. To do this, we return a list of all the groups, and then use a Select Case statement to see if the user belongs to one of our target groups. For example, this code check to see if the user belongs to the Finance Users group; if he or she does, then the script maps drive X to the share \\atl-fs-01\finance:

Case "Finance Users"
        objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\finance"

Here’s the finished script:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")
Set objNetwork = CreateObject("Wscript.Network")

strUserPath = "LDAP://" & objSysInfo.UserName
Set objUser = GetObject(strUserPath)

For Each strGroup in objUser.MemberOf
    strGroupPath = "LDAP://" & strGroup
    Set objGroup = GetObject(strGroupPath)
    strGroupName = objGroup.CN

    Select Case strGroupName
        Case "Finance Users"
            objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\finance"

        Case "Human Resource Users"
            objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\hr"

        Case "Manufacturing Users"
            objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\manufacturing"

        Case "Shipping and Receiving Users"
            objNetwork.MapNetworkDrive "X:", "\\atl-fs-01\shipping"
    End Select
Next

A couple of caveats regarding this script. First, the script assumes users belong to only one of the groups in question. Suppose a user belongs to both Manufacturing Users and Shipping and Receiving Users. In that case, drive X will be mapped to \\atl-fs-01\manufacturing, but an error will occur when the script tries to map drive X to \\atl-fs-01\shipping; that error occurs because the drive is already in use. If your users might belong to multiple groups, you’ll have to account for that, perhaps by allowing for multiple drive mappings (for example, map drive X and then, if drive X is taken, map the drive for the next group to drive Y). We’ll save that lesson for another day.

In addition, this script assumes that users are listed by name in the security groups. However, what if user Ken Myer happens to be a member of the Accounting Group, and the group (not the individual user) is a member of Finance Users? In that case, the drive will not be mapped properly, because this script cannot account for nested groups (groups inside of groups). That’s a more complicated script, and something will show you how to do in the near future.


How Can I Map Network Drives Based on a Computer’s Site?

Hey, Scripting Guy! How can I parse information in a text file, and then use that information to map network drives based on a computer’s site?

-- BH

Hey, BH. We’ll be with you in a second – we’re still replaying last night’s baseball game, in which the Kirkland Fire dropped a tough 7-5 decision to a very good team from Bothell. Bottom of the sixth, runners on second and third, nobody out, and we failed to score: a missed bunt and then a strikeout, a popup to the infield, and a weak grounder to second base. And all the Scripting Coach could do was sit there and watch.

Which, come to think of it, is pretty much what he does at work all day, too.

Still, what’s done is done, and it’s time to move on to more important things, things like Sunday’s game with Issaquah. But because we have some time to kill between now and then, we decided to go ahead and see if we could answer your question as well.

To begin with, let’s provide everyone with a little background information. According to your email the text file that contains the drive mapping information looks something like this:

Site1 X: \\atl-fs-01\public
Site1 Y: \\atl-fs-01\users
Site2 X: \\atl-fs-01\users
Site2 Y: \\atl-fs-02\managers
Site3 X: \\atl-fs-02\users
Site3 Y: \\atl-fs-03\reports

The basic idea here is that after you’ve determined the site that the computer is in (something we won’t deal with today, seeing as how we don’t have a copy of the code you’re using to determine the site) you then want to read the text file and map the drives appropriate for that site. For example, if a computer is in Site2 you’ll want to map these two drives:

· Drive X, mapped to \\atl-fs-01\users

· Drive Y, mapped to \\atl-fs-02\managers

How do you do that in a script? Here’s one way:

Const
ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objNetwork = CreateObject("Wscript.Network")

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Do Until objFile.AtEndOfStream
strLine = objFile.ReadLine
arrItems = Split(strLine, " ")
If arrItems(0) = "Site1" Then
objNetwork.MapNetworkDrive
arrItems(1), arrItems(2)
End If
Loop

objFile.Close

Good question: how does this script work? Well, it starts off by defining a constant named ForReading and setting the value to 1; we’ll use this constant when we read from the text file. We then create two objects: the Scripting.FileSystemObject (which we use to work with the text file) and the Wscript.Network object, which we use to map the drives.

Note. This is probably a good time to point out that this script needs to run on the local computer; that’s because neither the FileSystemObject nor the Network object are designed to work against remote machines. That shouldn’t be a problem for you, BH, because it sounds like this script is running as a logon script, in which case it always runs on the local machine.

Can you feel the excitement in the air as we make our way through the code? Well, then hold onto your hat, because now we are – drumroll, please – ready to open our text file (C:\Scripts\Test.txt) for reading:

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Once the text file is open we are, in turn, open for business. To begin with, we set up a Do Until loop that we’ll use to read the text file line-by-line, continuing until there are no more lines left to read (or, as we script writers like to put it, until the AtEndOfStream property is true). Inside that loop we call the ReadLine method to read the first line from the text file and store that value in the variable strLine. That means that, the first time through the loop, strLine will be equal to this:

Site1 X: \\atl-fs-01\public

Another good question: now what do we do? Well, as you can see, all the information we need – site name, drive letter, network path – is included in strLine; the only problem is that these three values have all been glommed together in a single string. But that’s OK; all we have to do is un-glom them using the VBScript Split function:

arrItems = Split(strLine, " ")

All we’re doing here is asking VBScript to take the value in strLine and split that value into separate elements (with each element being stored in an array named arrItems). How does the Split function know where one element ends and the next one begins? Because we tell it: the second parameter we pass to Split is simply a blank space (“ “). That tells the function to “split” the string each time it encounters a blank space. The net result? The array arrItems ends up with the following three elements, which correspond quite nicely to the three pieces of information we need to map drives:

· Site1

· X:

· \\atl-fs-01\public

The rest is easy. In a VBScript array the first item in an array is always given an index number of 0. By checking the value of item 0 we can determine whether the information we just read in from the text file happens to be for Site1:

If arrItems(0) = "Site1" Then

In this case, it is, which means we go ahead and use the remaining two elements in the array to map the drive. The MapNetworkDrive method requires two parameters: the drive letter and the network path. Because we know the drive letter (it’s the second item in the array, which has an index number of 1) and the network path (it’s item 3 in the array, with an index number of 2) we can map the drive with just a single line of code:

objNetwork.MapNetworkDrive arrItems(1), arrItems(2)

And there you have it. We loop around, read the next line in the text file, and repeat the process. Because only two lines in our text file are appropriate for Site1, when the script finishes we should have mapped two drives, one to \\atl-fs-01\public (drive X) and one to \\atl-fs-01\users (drive Y). That’s all there is to it.

As for the more important matter – baseball – well, easy come, easy go. Did we mention that the Scripting Son went 4-for-4 with 3 runs batted in? And yet, even though he reached base four times, he never once made it as far as third, let alone score a run. It was that kind of night. But, hey, we’ll get ‘em on Sunday. Guaranteed.

Note. Guarantee not guaranteed. After all, when you’re dealing with 15- and 16-year-olds you should never guarantee anything.


How Can I Remap Network Drives So They Use an IP Address Rather Than a Computer Name?

Hey, Scripting Guy! How can I remap network drives so they point to the IP address of the remote computer rather than the computer name?

-- YM

Hey, YM. You know, yesterday we started off this column by saying, “Never mind,” referring to the fact that, in a previous article, the Scripting Guy who writes this column lamented the fact that nothing interesting ever happened to him. So how are we going to start off today’s column? Like this: never mind.

Yesterday we noted that the Scripting Guy who writes this column was the … joyful … owner of a car that had experienced a “failure to proceed.” According to the preliminary diagnosis, the car had a broken ECM (which, as it turns out, is actually an ECU), and the Scripting Guy who writes this column appeared well on his way to being “the Scripting Guy who just spent several thousand dollars getting his stupid car fixed.”

Now, we should make clear that there was absolutely no question that the car was broken: even the mechanics were unable to get it to start, or even to get it to come close to starting. However, when the mechanics ran their computerized diagnostic tests everything came up just fine: apparently there was absolutely nothing wrong with the car whatsoever. (Well, aside from the fact that it wouldn’t actually start.) No matter what the mechanics tried, they couldn’t find a problem. There was nothing wrong with the car whatsoever.

Well, aside from the fact that it wouldn’t actually start.

At some point in the process one of the mechanics discovered a fuse that appeared to have burned out. This fuse is responsible for displaying warning lights on the dashboard; it has nothing to do with starting the car. And yet, once the fuse was replaced the car started right up. “It doesn’t make any sense,” said the mechanic. “I can’t tell you why that seemed to fix the problem. In fact, there’s no way that it could fix the problem. It doesn’t make any sense at all.”

And yet, so far so good. Not that the Scripting Guy who writes this column is complaining. Instead of paying out several thousands dollars, he paid $124.25: $123.00 for labor and $1.25 for parts.

Which, if you do the math, is way better than spending $2000 - $3000 to get your car fixed.

Needless to say, the Scripting Guy who writes this column felt like celebrating his good fortune. And what better way to celebrate good news than to write a script that remaps network drives so these drives point to the IP address of the remote computer rather than the computer name:

On Error Resume Next

Set objNetwork = CreateObject("Wscript.Network")
Set colDrives = objNetwork.EnumNetworkDrives

For i = 0 to colDrives.Count-1 Step 2
    strDriveLetter = colDrives.Item(i)
    strNetworkPath = colDrives.Item(i + 1)

    strNetworkPath = Replace(strNetworkPath, "\\", "")
    arrPath = Split(strNetworkPath, "\")

    strComputerName = arrPath(0)
    strFolderName = arrPath(1)

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

    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

    For Each objItem in colItems
        For Each objAddress in objItem.IPAddress
            strIPAddress = objAddress
        Next
    Next

    objNetwork.RemoveNetworkDrive strDriveLetter

    Wscript.Sleep 3000

    strNewPath = "\\" & strIPAddress & "\" & strFolderName
    objNetwork.MapNetworkDrive strDriveLetter, strNewPath
Next

Note: If you’re running this script on Windows Vista, you need to with elevated privileges for the script to work correctly. Open a command prompt by right-clicking and selecting Run As Administrator, then run your script from the command prompt.

Let’s see if we can figure out how this script works. To begin with, we should note that we started things off with the On Error Resume Next statement. Typically we don’t do that in these articles; as a general rule we leave out error-handling in order to keep the scripts a little shorter and a little easier to read. In this case, however, we occasionally got some weird errors when calling the RemoveNetworkDrive method, errors that would cause the script to crash. Why did we get those errors? Well, to tell you the truth, we aren’t really sure. But adding On Error Resume Next seemed to solve the problem, so ….

Although we assume that replacing a fuse in the Scripting Car would have solved the problem, too.

As for the real code, our next step is to create an instance of the Wscript.Network object, then use the following line to return a collection of all the mapped network drives on the computer:

Set colDrives = objNetwork.EnumNetworkDrives

The EnumNetworkDrives method does a perfectly fine job of returning all the mapped drives on a computer. However, it does so in slightly-quirky fashion: the returned information comes back as a collection that looks like this:

X:
\\atl-fs-01\public
Y:
\\atl-fs-01\users
Z:
\\atl-fs-01\scripts

As you can see, each mapped drive encompasses two items in the collection; one item (e.g., X:) representing the drive letter, the other item (\\atl-fs-01\public) representing the network path. That’s why we resort to this odd-looking For Next loop when we start iterating through the collection:

For i = 0 to colDrives.Count-1 Step 2

As you can see, we start the loop with the counter variable i equal to 0; that’s because the first item in any array or collection always has the index number 0. We end the loop when we reach the number of items in the collection (Count) minus 1. Why minus 1? Because the very last item in the collection will be the network path; for purposes of the loop, we only care about the drive letter. That’s also why we added the Step 2 parameter, which causes us to hit every other item in the collection. That way we grab only the drive letters – X:, Y:, Z: – and avoid the problem of grabbing the same drive twice (first by drive letter, the second time by network path).

Inside the loop, we start out by assigning the drive letter to a variable named strDriveLetter:

strDriveLetter = colDrives.Item(i)

We then use this line of code to assign the network path to a variable named strNetworkPath:

strNetworkPath = colDrives.Item(i + 1)

Note the index number we used here: i + 1. That’s because the network path associated with a particular drive letter will always have an index number 1 more than the drive letter. If drive Y: has the index number 2 then the path \\atl-fs-01\users will have the index number 3.

A little weird, but that’s the way it works.

Of course, the Scripting Guys know that, in order to map a drive using a computer’s IP address, it’s helpful if you actually know the computer’s IP address. One way to determine the IP address is to use the Win32_NetworkAdapterConfiguration class. Before we can use that class, however, we need to parse the computer name from the network path. That’s what this block of code is for:

strNetworkPath = Replace(strNetworkPath, "\\", "")
arrPath = Split(strNetworkPath, "\")

strComputerName = arrPath(0)
strFolderName = arrPath(1)

Let’s see if we can explain what’s going on here. To begin with, we use the VBScript Replace function to replace any instances of \\ with, well, nothing. That’s going to turn a network path like \\atl-fs-01\public into this:

· atl-fs-01\public

We then use the Split function to split that value on the \ character. That results in an array named arrPath, an array consisting of the following two items:

· atl-fs-01

· public

We next assign item 0 (atl-fs-01) to a variable named strComputerName, then assign item 1 (public) to a variable named strFolderName. We now know two things: the name of the computer where the mapped folder resides, as well as the name of that mapped folder.

Wait a second. As it turns out, we actually know three things: we also know that we now have enough information to connect to the remote computer atl-fs-01 and determine its IP address. That’s what this block of code is for:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

All we’re doing here is connecting to the WMI service on the remote computer, then retrieving a collection of network adapters (using the Win32_NetworkAdapterConfiguration class). Take careful note of the Where clause we included in our query:

Where IPEnabled = True

If you query the Win32_NetworkAdapterConfiguration class without using this Where clause you’re likely to get back 10 or more network adapters; for better or worse, most of these will be virtual adapters, adapters that aren’t tied to an actual network card and don’t have an IP address. By limiting returned data to network adapters where the IPEnabled property is True we’ll only get back network adapters that do have an IP address.

That brings us to this block of code:

For Each objItem in colItems
    For Each objAddress in objItem.IPAddress
        strIPAddress = objAddress
    Next
Next

As it turns out, a network adapter can have more than one IP address; because of that the IPAddress property stores values as an array. In turn, that means that we need to use a For Each loop to get at each of these values. This block of code loops through the returned set of IP-enabled network adapters and then, for each adapter, loops through the assigned IP addresses. We have no idea of knowing which IP address is the “best” so we simply take the last address we find and assign it to a variable named strIPAddress:

strIPAddress = objAddress

At long last, we’re now ready to remap our first network drive. Except for one thing: there’s no RemapNetworkDrives method. Instead, we have to disconnect the existing drive (the one mapped using the computer name) and then create a new network drive (using the same drive letter) that maps to the IP address instead. Step 1 in that process? Disconnecting the existing drive:

objNetwork.RemoveNetworkDrive strDriveLetter

Next we use the Sleep method to pause the script for 3 seconds (3,000 milliseconds); that helps ensure that the existing drive is disconnected before we try mapping a new drive to that drive letter. After a brief pause we then create a new network path, this one using the IP address (strIPAddress) rather than the computer name:

strNewPath = "\\" & strIPAddress & "\" & strFolderName

And once we have the network path we can then call the MapNetworkDrive method and map the drive to the IP address:

objNetwork.MapNetworkDrive strDriveLetter, strNewPath

That’s going to take drive X:, which used to map to \\atl-fs-01\public, and remap it to the IP address (e.g., \\192.168.1.1\public). And then we loop around and repeat the process with the next mapped drive.

So now that the Scripting Car problems have been resolved (we hope) does that mean that the Scripting Guy who writes this column no longer needs you to send him $2,000? Heavens no; where would you get an idea like that? Granted the Scripting Car seems to be running OK, but for some reason the Scripting Guy who writes this column isn’t 100% confident that replacing a fuse that has absolutely nothing to do with the problem has somehow actually fixed the problem. And, even if it has, well, have we mentioned the fact that the Scripting Son will be headed off for college in another year? If we haven’t, don’t worry about it: no doubt you’ll have plenty of chances to hear all about that (and plenty of time to mail in your $2,000) before the big day comes.

Editor’s Note: We’re doing our best to keep the Scripting Guy’s begging to a minimum, but it’s a … challenge … to say the least. We would apologize to anyone who was feeling obligated to actually send this Scripting Guy money if we believed there was anyone like that out there.


How Can I Use a Logon Script to Remove All Mapped Drives?

Hey, Scripting Guy! How can I use a logon script to remove all mapped drives each time a user logs on?

-- VS

Hey, VS. You know, we’re glad you specifically mentioned the term “logon script” in your question. Why? Well, one of the drawbacks to mapping and unmapping network drives is the fact that there are no WMI classes capable of performing these tasks; that means we have to rely on Windows Script Host to remove mapped network drives. WSH does the job just fine, but with one caveat: WSH can map and unmap drives only on the local computer. That can be a bit inconvenient, to say the least (there’s no straightforward way to run a script that unmaps drives on a remote machine), but in this case it doesn’t matter; that’s because logon scripts always run locally anyway.

So how do you remove all the mapped network drives on a computer? Here’s one way:

On Error Resume Next

Set objNetwork = CreateObject("Wscript.Network")

Set colDrives = objNetwork.EnumNetworkDrives

For i = 0 to colDrives.Count-1 Step 2
    objNetwork.RemoveNetworkDrive colDrives.Item(i)
Next

Yes, this is odd-looking code, but we’ll explain why it looks that way. First, though, let’s point out that the script starts off by creating an instance of the Wscript.Network object; we then call the EnumNetworkDrives method to return a collection of all the mapped network drives on a computer.

This is where the quirkiness of WSH’s mapped network drives collection manifests itself. Suppose you have three mapped drives on your computer (drives X, Y, and Z). The collection returned by EnumNetworkDrives will look something like this:

X:
\\atl-fs-01\share1
Y:
\\atl-fs-01\share2
Z:
\\atl-fs-01\share3

As you can see, each mapped drive actually has two entries in the collection. Take drive X, for example. The first item in the collection (item 0) is the drive letter for drive X: X:. The second item in the collection (item 1) is the UNC path to the shared folder on drive X: \\atl-fs-01\share1. One drive, but two entries in the mapped network drive collection. Each of the other drives also has two items in the collection, meaning our three mapped drives will result in a six-item collection, with items alternating between drive letter and UNC path.

That’s why we can’t use a simple For Each loop to loop through the items in the collection. In order to remove a network drive, we need to call the RemoveNetworkDrive method, passing the method the drive letter for the drive to be removed. If we looped through all the items in the collection, the first time through the loop we’d pass RemoveNetworkDrive the value X:. So far so good; we’d remove drive X. However, the second time through the loop we’d pass RemoveNetworkDrive the value \\atl-fs-01-share1. That’s not so good: there aren’t going to be any drives with a drive letter \\atl-fs-01-share1. Which, of course, means that you’re script is going to fail.

Which also means we need to figure out a way around this issue. To that end, we use a For Next loop, looping from 0 (the first item in a collection is always item 0) to the number of drives in the collection minus 1. (Why “minus 1? Well, in a three-item collection item numbers would be 0, 1, and 2. Because the first item is item 0, the last item will always be the total number of items minus 1; in this case, 3-1, or 2. If you have 100 items in the collection the last item will have an index number of 99, or 100-1.)

On top of that, we also need to skip every other item; instead of hitting items 0, 1, 2, 3, 4, 5, we want to hit only items 0, 2, and 4 (the items that contain drive letters). That’s what the Step 2 does; it tells VBScript to only hit every other item. (Step 1, the default value for a For Next loop, hits every item.) Setting the Step value to 2 ensures that we’ll pass only drive letters to the RemoveNetworkDrive method.

Now that we have a way of grabbing just the drive letters (bypassing the UNC paths), the rest is easy: to remove a network drive we simply use this line of code, with colDrives.Item(i) representing the drive letter for the current item in the collection:

objNetwork.RemoveNetworkDrive colDrives.Item(i)

With any luck (although we’re talking computers here; who needs luck when working with computers?) any and all of the mapped drives found on this machine will be removed.


How Can I Change the Screensaver Timeout Value?

Hey, Scripting Guy! Can I use a script to change the timeout value for the screensaver on a computer?

-- JN

Hey, JN. For some reason, Microsoft’s scripting technologies come up a little short any time you’re talking about Windows settings and components, things like screensavers, wallpaper, the Taskbar, the Start menu, etc. You can use WMI (in particular, the Win32_Desktop class) to read many of these values, but you can’t use the Win32_Desktop class (or any equivalent class or object) to modify these values. Why? To be honest, we don’t really know; that’s just the way it is.

Fortunately, though, many of these settings are stored in the Windows registry, and if it’s stored in the registry, there’s a good chance you can write a script to manage those settings. The screensaver is no exception. Open up Regedit, and take a look in HKEY_CURRENT_USER; in the Control Panel\Desktop key, you’ll see several registry values related to the screensaver, including ScreenSaveTimeout. As you might have guessed, this is the registry value that determines how long the system must be idle before the screensaver kicks in. By default, this is set to 600 seconds, or 10 minutes.

So how do you change that value? By using a script just like this one, which changes the timeout value to 5 minutes (300 seconds):

HKEY_CURRENT_USER = &H80000001
strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")
strKeyPath = "Control Panel\Desktop"
objReg.CreateKey HKEY_CURRENT_USER, strKeyPath
ValueName = "ScreenSaveTimeout"
strValue = "300"
objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

To do this, we start by creating a constant named HKEY_CURRENT_USER and set the value to &H80000001; this tells WMI’s Registry provider that we’re working with the HKEY_CURRENT_USER portion of the registry. If we wanted to work with HKEY_LOCAL_MACHINE, we’d set a constant equal to &H80000001; &H80000003 would let us work with HKEY_USERS.

For this script we’re working on the local computer, so we set the variable strComputer to a dot (a WMI shortcut meaning the local computer). But the great thing about the Registry provider is that it works just as well on remote computers as it does the local computer. If you want to change the screensaver timeout value on a remote computer, just set the value of strComputer to the name of that computer.

From there we connect to the WMI service, and specify the registry path (Control Panel\Desktop). Next we call the CreateKey method. This is just to be on the safe side. If the registry key we’re looking for doesn’t exist, CreateKey will create it. If the registry key already exists, then CreateKey will just leave well enough alone.

After specifying the name of the registry value we want to change (ScreenSaveTimeout) and the new value (300), we then use the SetStringValue method to change the timeout value to 300 seconds.

Not too bad, huh? For more information about the Registry provider – probably one of the most under-appreciated on all WMI providers – take a look at the Registry chapter in the Microsoft Windows 2000 Scripting Guide. And check out the infamous Tweakomatic, the amazing utility that will actually write scripts that can read from and write to the registry. And to steal from an old commercial, with a name like Tweakomatic, it has to be good.

Incidentally, here’s another useful screensaver script for you; this one ensures that the screensaver is password-protected:

HKEY_CURRENT_USER = &H80000001
strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")
strKeyPath = "Control Panel\Desktop"
objReg.CreateKey HKEY_CURRENT_USER, strKeyPath
ValueName = "ScreenSaverIsSecure"
strValue = "1"
objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

See how much fun registry scripting can be?!? The Tweakomatic has additional scripts for managing the screensaver, as well as scripts for managing the Start menu, the Task bar, Windows Explorer folder settings, and a whole lot more. If you’re looking for ways to manage common desktop settings, that’s as good a place to start as any.


How Can I Change the Text for the Marquee Screen Saver?

Hey, Scripting Guy! How can I change the text for the Marquee screen saver?

-- SJ

Hey, SJ. Greetings from the frozen wasteland formerly known as Redmond, WA. On Monday night a freak November snowstorm hit the Seattle area. However, instead of leaving behind snow it left behind an inch or so of ice; ice which, in turn, has pretty much left the entire region paralyzed. (If you’re from the Midwest/Alaska/Norway/Greenland or anywhere else that doesn’t shut down completely just because you get a little ice and snow feel free to insert your favorite “People from Seattle are so wimpy” joke here.) Is it really that bad here? Well, let’s put it this way: people actually slept in their cars on the freeway last night: traffic wasn’t going anywhere and they were too far from home to walk. And so they spent the night sleeping in their cars on I-405.

Yuck.

Things aren’t much better as the Scripting Guy who writes this column writes this column Tuesday morning. Other than one person down the hall (who looks as though she might have spent the night here) the Scripting Guys’ building is pretty much deserted: obviously everyone either was legitimately prevented from coming in to work due to icy roads, or they phoned in and said they were being prevented from coming in to work due to icy roads. Either way, the Scripting Guy who writes this column largely has the place to himself.

You know those movies where a kid accidentally gets locked in a toy store or a candy store overnight? Well, having a Microsoft building all to yourself is almost as much fun.

Almost.

What’s that? Doesn’t the Scripting Guy who writes this column live on a side street that can be treacherous driving even when there isn’t ice or snow on the road? Couldn’t he have legitimately stayed home from work due to icy conditions? Wouldn’t that have made more sense than walking a mile and a half to the bus stop and then standing around waiting for the bus to finally come crawling in? And then, after enduring all that hardship, his reward is that he gets to spend the day working? Shouldn’t he have just stayed in bed this morning, like everyone else?

Oh, sure: now you tell him!

Oh, well. As long as we did make it in this morning we might as well show you a script that can change the text for the Marquee screen saver. As it turns out, each time the Marquee screen saver starts up it checks the registry to determine which text it should display. That means that all we have to do to modify that text is modify the appropriate registry value. How hard is that? Not very hard at all:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry
= GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Control Panel\Screen Saver.Marquee"
strValueName = "Text"
strStringValue = FormatDateTime(Date,
vbLongDate)

objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath,
strValueName, strStringValue

As you can see, we begin by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this constant later on to tell the Standard Registry provider which registry hive we want to work with. (That’s right: we’re going to work with the HKEY_CURRENT_USER hive. How did you know that?)

After defining the constant we then connect to the WMI service on the local computer (although this script can also be run against a remote computer; just assign the name of that computer to the variable strComputer). The big thing to watch out for here? We need to connect to the StdRegProv class, which happens to live in the root\default namespace. That’s different from the WMI classes used in the typical system administration script, classes that are almost always found in the root\cimv2 namespace. Just something to keep in mind.

After we make the connection to the WMI service we then need to assign values to three different variables:

· strKeyPath, the path to the registry key within the HKEY_CURRENT_USER hive. Because we have to connect to the Control Panel\Screen Saver.Marquee registry key it’s only fitting and proper that we assign strKeyPath the value Control Panel\Screen Saver.Marquee.

· strValueName, the name of the actual registry value we want to change. To modify the screen saver text we need to modify the string (REG_SZ) value named Text. Thus we assign the string Text to strValueName.

· strStringValue, the new text for the screen saver. We’ve gotten fancy here, making the current date the screen saver text. But not just the date: we’re using the VBScript function FormatDateTime to give us a date that will look like this: Wednesday November 29, 2006. To get fancy output like that we simple pass two parameters to FormatDateTime: the value to be formatted (the VBScript function Date) and the kind of formatting we want to apply (the VBScript constant vbLongDate). Are there other date-time formats besides the so-called “long” format that we could apply using this method? You bet there are; take a peek at the Microsoft Windows 2000 Scripting Guide for details.

After assigning values to our three variables we’re ready to go ahead and change the text in the registry. To do that we simply call the SetStringValue method, passing four parameters:

· The constant HKEY_CURRENT_USER

· The variable strKeyPath

· The variable strValueName

· The variable strStringValue

And that’s all we have to do. Best of all, there’s no need to log the user off and then have him or her log back on after making this change. Like we said, each time the Marquee screen saver starts up it grabs the latest settings – including the text to be displayed – from the registry. Consequently, this change takes effect the very next time the screen saver kicks off.

As for life here in the arctic, well, right now the sun is shining. It’s still cold, however, which means that the ice will probably melt during the day, the melted ice will refreeze over night, and then we’ll get to do this all over again. What a glorious time of year, huh?


How Can I Configure the Screensaver Used When No One is Logged On to a Computer?

Hey, Scripting Guy! I recently downloaded your Script Center screensaver, and it works great as long as someone is logged on to the computer. However, when no one is logged on the computer uses a different screensaver. How can I get the computer to use the Script Center screensaver when no one is logged on?

-- RF

Hey, RF. You know, it occurred to us that you might be trying to pull a fast one on the old Scripting Guys. For example, how do we know it’s really our screensaver that you want to run when no one is logged on to the computer? Maybe this is just a way to flatter the Scripting Guys into answering your question, and then, once we do so, you’ll go out and install some other scripting-related screensaver as the default. RF, RF: did you really believe that the Scripting Guys are so shallow that we’d succumb to cheap flattery like that?

Well you should have, because we are:

Const
HKEY_USERS = &H80000003

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = ".Default\Control Panel\Desktop"
ValueName = "SCRNSAVE.EXE"
strValue = "C:\WINDOWS\System32\Script Center.scr"

objReg.SetStringValue HKEY_USERS, strKeyPath, ValueName, strValue

As you can see, this is a WMI script for changing the registry. It’s very similar to the many other registry-modifying scripts we’ve shown you in the past, albeit with one unique twist. In our previous scripts we’ve always started out by defining a constant that tells that script that we want to work with either the HKEY_CURRENT_USER or the HKEY_LOCAL_MACHINE registry hive. And there’s good reason for that: we want whatever modification we’re making to apply to the current user or to the computer itself. Ah, but notice the first line of this script:

Const HKEY_USERS = &H80000003

That’s the secret right there: this time we want to work with the HKEY_USERS registry hive. Among other things, the HKEY_USERS hive provides you with a place to configure settings that, in lieu of any specific user, apply to anyone who logs on to the computer. In other words, if you configure a screensaver in HKEY_USERS, that screensaver is the default that will apply to any new user who logs on to the computer. (And that’s the screensaver that will continue to apply unless the user specifically configures a different screensaver.) Likewise, when no one is logged on to the computer that’s the screensaver that will be used.

Cool, huh? Incidentally, this is also where you can configure the wallpaper that appears at the logon screen; just set the registry value HKEY_USERS\.Default\Control Panel\Desktop\Wallpaper to the appropriate file path. Something like this:

Const
HKEY_USERS = &H80000003

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = ".Default\Control Panel\Desktop"
ValueName = "Wallpaper"
strValue = "C:\WINDOWS\System32\Script Center.bmp"

objReg.SetStringValue HKEY_USERS, strKeyPath, ValueName, strValue

After defining our constant we next connect to the WMI service on the local computer (although this script works equally well against remote computers). We then assign values to three different variables:

· strKeyPath – The path to the registry key within HKEY_USERS. In this case, that’s .Default\Control Panel\Desktop.

· ValueName – The name of the registry value we want to change (SCRNSAVE.EXE).

· strValue – The new value to be assigned to SCRNSAVE.EXE. In this script, that’s the full path to the screensaver.

All that’s left to do now is call the SetStringValue method (passing along the constant HKEY_USERS and our three variables as parameters) and we’re done:

objReg.SetStringValue HKEY_USERS, strKeyPath, ValueName, strValue

If you log off and wait a few minutes the Script Center screensaver should kick in. (Well, assuming that the registry value ScreenSaveActive is set to 1; if it’s set to 0, that means the screensaver has been disabled.)

See how easy that is? And no, we don’t mean the script (although that’s pretty easy, too); we mean how easy it is to get your question answered, provided you word that question in the right way, of course. Like they say, flattery will get you anywhere.


How Can I Tell if the Screen Saver is Active?

Hey, Scripting Guy! How can I tell if the screen saver is active and, if so, how long it has been running?

-- MS

Hey, MS. Interesting question; we don’t think we’ve ever been asked that before. And at first it had us stumped; after all, there aren’t any WMI classes or COM objects for managing the screen saver, and we didn’t know of any event that was fired each time a screen saver kicked in, at least not an event we could get to using a script. This had all the makings of a complex and complicated problem that would likely take us years to solve.

Or, as it turned out, it was something we could solve in just a couple minutes:

strComputer = "atl-dc-01"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process")

For Each objProcess in colProcesses
    If Right(objProcess.Name, 4) = ".scr" Then
        Wscript.Echo "The screen saver " & objProcess.Name & " is running."
        Wscript.Echo "Screen saver start time: " & objProcess.CreationDate
        Wscript.Quit
    End If
Next

Wscript.Echo "The screen saver is not running."

To be honest, we were originally making a mountain out of a molehill. Granted, there are no COM objects for managing the screen saver and there are no events fired when the screen saver kicks in. But that’s OK. When the screen saver runs, it typically runs a file with a .scr file extension. If we want to know whether or not the screen saver is running all we have to do is check and see if there are any processes running that have an executable name ending in .scr. If there are, that probably means the screen saver is running.

Note. We’re aware that this isn’t foolproof and there might be third-party screen savers that use a different file extension. But this approach should work for any of the screen savers that ship with the operating system.

Now that we understand the strategy let’s take a look at the script. For one thing, you might notice that the first line of code is a little different from most of our sample WMI scripts:

strComputer = "atl-dc-01"

Typically our first line of code sets the value of the variable strComputer to a dot (.); we do that because the dot represents the local computer, and setting strComputer to a dot gives us a script that will run on the local computer regardless of the name of that computer. We’re assuming, however, that you want to run this script against a remote machine; there are probably easier ways to tell whether or not the screen saver is running on your local machine without going to the trouble of writing a script. Therefore we set the value of strComputer to the name of a remote computer, in this case atl-dc-01.

Next we connect to the Win32_Process class and use the ExecQuery method to retrieve a collection of all the processes running on the computer atl-dc-01. We then set up a For Each loop to cycle through that collection. Inside the loop we check the value of the Name property for each process (this will be equivalent to the file name for the application). If the Name ends in .scr we assume that a screen saver is running; we then echo the executable file name for the screen saver as well as the time that the screen saver started running. (We can retrieve that value from the CreationDate property.) Having found the screen saver, we then call the Wscript.Quit method to exit the script.

How do we know if the executable file name ends in .scr? We simply use the VBScript Right function, and tell it to check the last four characters in the string:

If Right(objProcess.Name, 4) = ".scr" Then

But what if we don’t find a process whose Name ends in .scr? After checking each and every process, we exit the loop and then echo a message stating that the screen saver is not running.

When we run the script, we get back output similar to this:

The screen saver logon.scr is running.
Screen saver start time: 20050725153639.115344-420

Well, yes, that’s a good point: the start time comes back in UTC (Universal Time Coordinate) format, which isn’t the most intuitive date-time format ever created. Therefore, let’s modify our script to include a function that convert this UTC date-time into something a bit more readable:

strComputer = "atl-dc-01"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process")

For Each objProcess in colProcesses
    If Right(objProcess.Name, 4) = ".scr" Then
        Wscript.Echo "The screen saver " & objProcess.Name & " is running."
        dtmStartTime = objProcess.CreationDate
        dtmScreensaverStart = WMIDateStringToDate(dtmStartTime)
        Wscript.Echo "Screen saver start time: " & dtmScreensaverStart
        Wscript.Quit
    End If
Next

Wscript.Echo "The screen saver is not running."

Function WMIDateStringToDate(dtmBootup)
    WMIDateStringToDate = CDate(Mid(dtmBootup, 5, 2) & "/" & _
        Mid(dtmBootup, 7, 2) & "/" & Left(dtmBootup, 4) _
            & " " & Mid (dtmBootup, 9, 2) & ":" & _
                Mid(dtmBootup, 11, 2) & ":" & Mid(dtmBootup,13, 2))
End Function

When we run this script we get back output that’s a little easier to parse:

The screen saver logon.scr is running.
Screen saver start time: 7/25/2005 3:36:39 PM

Note. We won’t discuss the function for converting a UTC date to a regular date, but you can read about the process in this section of the Microsoft Windows 2000 Scripting Guide.

Like we said, determining whether or not the screen saver is running turned out to be fairly easy. Which makes us wonder whether people have been making this cold fusion thing much tougher than it should be….


How Can I Write a Script That Prevents the Screensaver from Starting?

Hey, Scripting Guy! How can I write a script that prevents the screensaver from coming on? Sometimes I get interrupted for a couple minutes (say I get a phone call or something) and the screensaver kicks in, which promptly locks the computer. Is there a way to keep that from happening?

-- SS

Hey, SS. We hope you’ll forgive us if we seem a little distracted today. As you probably heard, Bill Gates has decided to step down as Microsoft’s Chief Software Architect. Needless to say, someone will have to replace him and, well – all modesty aside – who would be a more logical choice than the Scripting Guys? We’re expecting the phone to ring any minute now.

Ray Ozzie? No, never heard of him. Why do you ask?

Of course, seeing as how this is Microsoft (although we are thinking about changing the company name after we take over) there’s a good chance that they’ll send us an email instead of calling us. That’s why we decided to see if we could answer your question; after all, we don’t want our screensaver kicking in the moment that email arrives.

Not that it’s arrived yet, mind you. But maybe the server is having problems.

To begin with, we should point out that we had no idea whether we could find a way to keep the computer active and thus prevent the screensaver from running. But we did. And if that doesn’t sound like the perfect qualification for Chief Software Architect, well, we don’t know what does.

Here’s the script we came up with:

If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  " & Chr(34) & strPath & chr(34)
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand)
    Wscript.Quit
End If

Do While True
    Set objIE = CreateObject("InternetExplorer.Application")
    Wscript.Sleep 1000
    objIE.Quit
    Wscript.Sleep 60000
Loop

We should note that there might be other ways to perform this task. We tried a couple things that didn’t work, then stumbled upon an approach that did work. As soon as we found something that worked, we called it good. After all, while we don’t have any idea what a Chief Software Architect actually does, we assume that someone in that position probably shouldn’t spend hours and hours looking for alternate ways to keep a screensaver from locking. That’s why we went with the first solution we came up with. If you have a better/easier way to do this, let us know.

So what exactly did we come up with? Let’s start by looking at this block of code:

If LCase(Right(Wscript.FullName, 11)) =
"wscript.exe" Then
strPath = Wscript.ScriptFullName
strCommand = "%comspec% /k cscript  " &
Chr(34) & strPath & chr(34)
Set objShell = CreateObject("Wscript.Shell")
objShell.Run(strCommand)
Wscript.Quit
End If

This is actually optional, but we needed a script that could run for an indefinite period of time; after all, there’s no telling how long that telephone conversation or trip to the coffee machine will take. Because we don’t know how long the script needs to run we can’t put in any code that auto-terminates the thing after say, five minutes. In turn, that means we need a quick and easy way to manually terminate the script.

And probably the quickest and easiest way to ensure that is to force the script to run in a command window under CScript.exe. After all, in that case we just need to close the command window and the script will end.

We’d like to see Ray Ozzie come up with an idea like that.

That’s why we start the script with the aforementioned block of code. That code is designed to see if the script is running under Wscript.exe (and thus not running in a command window). That’s something we check for that with this line of code:

If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then

In case you’re wondering, the FullName property refers to the script host that the script is running under. The value of FullName will always be something like C:\Windows\System32\Cscript.exe or C:\Windows\System32\Wscript.exe. If the last 11 characters in FullName are wscript.exe then we’re running under Wscript. And we need to do something about that.

So can we change the script host after the script starts running? No, not as far as we know. However, we can do the next best thing: we can restart the script under CScript. That’s what the rest of the code block is for. In order to restart the script, we first grab the value of the ScriptFullName property and store it in a variable named strPath; if you aren’t familiar with ScriptFullName, this property holds the path to the script itself (e.g., C:\Scripts\Test.vbs). That brings us to this line of code:

strCommand = "%comspec% /k cscript  " & Chr(34) & strPath & chr(34)

What we’re doing here is constructing a command-line command that will run the current script under CScript.exe. To do that we start by calling the %comspec% /k environment variable; that will cause a command window to open and remain open (the /k is short for “keep open”). We then specify the script host – cscript – followed by a blank space. After that we insert a pair of double quotes – Chr(34) – followed by the path to the script, followed by a closing pair of double quotes. When we’re done, the variable strCommand will be equal to something along these lines:

%comspec% /k "C:\Scripts\Test.vbs"

Good question: why do we enclose the path in double quotes? Well, in this case, we don’t need to; we only have to do that if the path to the script file contains a blank space (e.g., C:\My Scripts\Test.vbs). We added the double quotes just to be on the safe side.

After we’ve put together our command-line command we then use this line of code to start a second instance of the script:

objShell.Run(strCommand)

And then we call the Quit method to terminate the first instance of the script. That leaves us with one copy of the script, a copy running under CScript.exe.

Note. What if the script already is running under CScript.exe? In that case we simply skip the initial code block altogether.

That’s all fine and dandy, but what exactly is that script doing? Why, it’s doing this:

Do While True
    Set objIE = CreateObject("InternetExplorer.Application")
    Wscript.Sleep 1000
    objIE.Quit
    Wscript.Sleep 60000
Loop

What we’ve done here is set up an endless loop, ensuring that the script will run forever and ever. (Or at least until you close the command window.) Inside that loop we create an instance of the InternetExplorer.Application object, an instance we never actually show on screen. After that we call the Sleep method, pausing the script for 1 second (1000 milliseconds). We then immediately terminate our instance of Internet Explorer, pause the script for 60 seconds (60000 milliseconds) and then repeat the process all over again, ad infinitum.

Note. There’s nothing magical abut the 60-second pause; we used that because we had our screensaver timeout set for 2 minutes and we needed to make sure we had some activity before those 2 minutes expired. If you have your screensaver timeout set to a higher value then you can pause a correspondingly-longer time before creating the next instance of Internet Explorer.

Yes, it’s a crazy idea, but it works: the screensaver stays off and, as an added bonus, nothing else pops up on the screen, either. If you add a shortcut to your Quick Launch taskbar you can invoke this script with a single mouse click. And then, of course, to end the script all you have to do is close the command window. Pretty sweet.

So there you have it, SS. Now, we were wondering if you could do us a favor? Could you give us a call here at the office? We suspect that our phones must not be working. After all, that’s the only reasonable explanation we could come up with as to why no one has called us yet.


Can I Pin a File to the Start Menu by Using a Script?

Hey, Scripting Guy! Can I pin a file to the Start Menu by using a script?

-- ZD

Hey, ZD. For those of you who haven’t bought Windows XP yet (hey, what’s the matter with you; we Scripting Guys have families to feed!), by default XP’s Start Menu changes over time: the more you use an application, the more likely it is to appear on the Start Menu. The less you use an application – well, use it or lose, huh? (Incidentally, this refers solely to the application shortcuts that appear when you first click the Start button. Shortcuts that show you up when you click All Programs stay put, and don’t appear or disappear depending on use.)

Most of the time this works out pretty good; after all, the applications you use most often are probably the applications you use most often. However, there might be an application or two that you always want to appear on the Start menu even if you don’t use the program all that often. In that case you can “pin” the item to the Start menu; once pinned, the application will stay on the Start menu forever (or until you unpin it), even if you never use the application.

In Windows Explorer, you can pin an application to the Start Menu by right-clicking the application icon and then clicking Pin to Start Menu. Nothing wrong with that, of course, but if want to really be cool you can pin items to the Start Menu using a script. Here, for example, is a script that pins the Windows Calculator (calc.exe) to the Start Menu:

Set objShell =
CreateObject("Shell.Application")
Set objFolder = objShell.Namespace("C:\Windows\System32")
Set objFolderItem = objFolder.ParseName("calc.exe")

objFolderItem.InvokeVerb("P&in to Start Menu")

As you can see, we use the Shell Application object in order to perform this task. After creating an instance of the Shell object, we use the Namespace method to bind to the folder (C:\Windows\System32) where Calculator resides. We then use the ParseName method to bind to the actual executable file (calc.exe).

Why can’t we just bind directly to the file? Who knows; that’s just the way the Shell object is designed. But hey, it’s just one extra line of code, and the net result is the same: we get hooked up to C:\Windows\System32\Calc.exe.

After that we use the InvokeVerb method to pin the application to the Start Menu. Verbs are items that appear on the context menu when you right-click the application icon in Windows Explorer. And that’s not a misprint in P&in to Start Menu; the ampersand is part of the command name, and indicates the shortcut key for the item (in this case, the letter i). It looks weird, but it has to be there.

Ok, ok, how did we know the ampersand has to be there? Simple: we ran this script, which returns a list of verbs that can be applied to Calc.exe:

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace("C:\Documents and Settings\gstemp\Desktop\Stuff")
Set objFolderItem = objFolder.ParseName("bob.vbs")

Set colVerbs = objFolderItem.Verbs
For Each objVerb in colVerbs
    Wscript.Echo objVerb
Next

And, yes, these verbs are pretty cool, although they aren’t as cool as you might expect. That’s because when you invoke a verb, that verb functions exactly the same as if you had chosen the item from the context menu. For example, if you right-click a file and choose Delete, a dialog box pops up asking if you want to send the file to the Recycle Bin. The same thing happens if you call the Delete method programmatically; instead of automatically deleting the file, that same confirmation box appears. Give this script a try, and you’ll see what we mean:

Set objShell =
CreateObject("Shell.Application")
Set objFolder = objShell.Namespace("C:\Windows\System32")
Set objFolderItem = objFolder.ParseName("calc.exe")

objFolderItem.InvokeVerb("&Delete")

Still, something worth playing around with.

And before you ask, the answer is yes: you can also unpin an item from the Start Menu by using a script:

Set objShell =
CreateObject("Shell.Application")
Set objFolder = objShell.Namespace("C:\Windows\System32")
Set objFolderItem = objFolder.ParseName("calc.exe")

objFolderItem.InvokeVerb("Unp&in from Start Menu")

Could life get any better?


How Can I Add a Shortcut to the Quick Launch Tray?

Hey, Scripting Guy! How can I add a shortcut to the Quick Launch tray?

-- ZR

Hey, ZR. Before we answer this question we need to first answer a different question. And here’s the answer: Frank “Home Run” Baker, who led the major league in homers four times, even though he never hit more than a dozen home runs in a single season.

Oh, wait; wrong question. Here’s the answer: no, unfortunately there’s really no way to enable and disable the Quick Launch tray using a script. There are some workarounds floating around the Internet that use the SendKeys commands, but SendKeys is a bit … flaky … (to say the least) so we don’t really recommend it. But if you search for Quick Launch and SendKeys you should be able to find a sample without any problem.

Assuming that the Quick Launch tray is already enabled, however, it’s pretty easy to add a new shortcut. This script, for example, adds Notepad.exe to the Quick Launch tray:

Set
objShell = CreateObject("WScript.Shell")
Set colEnvironmentVariables = objShell.Environment("Volatile")

strFolder = colEnvironmentVariables.Item("APPDATA") & _
"\Microsoft\Internet Explorer\Quick Launch"

Set objShortCut = objShell.CreateShortcut(strFolder & _
"\Notepad.lnk")
objShortCut.TargetPath = "Notepad.exe"
objShortCut.Description = "Open Notepad"
objShortCut.HotKey = "Ctrl+Shift+N"
objShortCut.Save

This script actually accomplishes two tasks. First, it determines the location of the Quick Launch folder for the logged-in user. (We should point out that this script must be run on the local computer. That’s because it relies on Windows Script Host to actually create the shortcut; without yet another set of workarounds WSH can’t be used against remote computers). To get the path to the Quick Launch folder we create an instance of the WScript.Shell object and then use this line of code to retrieve a collection of environment variables:

Set colEnvironmentVariables = objShell.Environment("Volatile")

As it turns out, there’s an environment variable named AppData that pinpoints the location of the Application Data folder. (If you don’t believe us, type echo %appdata% at the command prompt and see what happens.) Application Data is the “host” folder for the Quick Launch folder; the path to the Quick Launch folder will always be the path to the Application Data folder plus \Microsoft\Internet Explorer\Quick Launch. In other words, suppose this is the path to the Application Data folder:

C:\Documents and Settings\kenmyer\Application Data

In that case, this will be the path to the Quick Launch folder:

C:\Documents and Settings\kenmyer\Application Data\Microsoft\Internet Explorer\Quick Launch

In turn, that means we can use this code (which combines the value of the AppData environment variable with the string \Microsoft\Internet Explorer\Quick Launch) to get the location of the Quick Launch folder and store that value in a variable named strFolder:

strFolder = colEnvironmentVariables.Item("APPDATA") & _
 "\Microsoft\Internet Explorer\Quick Launch"

So much for task number 1. Task number 2 involves creating the shortcut for Notepad.exe, something we do here:

Set objShortCut = objShell.CreateShortcut(strFolder & _
 "\Notepad.lnk")
objShortCut.TargetPath = "Notepad.exe"
objShortCut.Description = "Open Notepad"
objShortCut.Save

We start off by calling the CreateShortcut method, passing it the path to the Quick Launch folder plus \Notepad.lnk, the file name for our new shortcut. We then set the following properties of the new shortcut:

Property | Description
TargetPath | File name/path to the – in this case – executable file we want to run any time we click the new shortcut. Because Notepad.exe is in the Windows path all we need to do is specify the file name: Notepad.exe.
Description | “Tool tip” for the shortcut. (That is, the comment that appears when you hold the mouse over the shortcut.)

That's all we really need to do. There are other properties we can set for our shortcut; for a more complete list, take a look at the Windows Script Host Reference on MSDN. For our purposes, however, we'll simply set the TargetPath and Description, and then call the Save method to save the new shortcut to the Quick Launch bar:

objShortCut.Save

Child’s play.

By the way, we know of another interesting piece of trivia regarding Home Run Baker, but TechNet probably wouldn’t appreciate it if we related that information here. If you’re interested, though, just search for Home Run Baker and – uh, well, never mind. Let’s just remember Home Run Baker for leading the league in home runs four times and call it good.


How Can I Change the Icon for an Existing Shortcut?

Hey, Scripting Guy! How can I change the icon for an existing shortcut?

-- DF

Hey, DF. You know, the Scripting Guys (some of them, at least) are old enough to remember a time when you would never even think about changing an icon; after all, back in those days having any icon at all was considered cool. (And if you had flying toasters on top of that, well ….) But times have changed, and apparently people aren’t satisfied with their icons anymore: yours is the fourth or fifth email we’ve gotten in the past few weeks asking if we could demonstrate a way to change icons using a script. And you know our motto here at Scripting Guys Central: the customer is always right.

Hey, we said that was our motto, we didn’t say we actually followed it.

Here’s the script:

Const
DESKTOP = &H10&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.NameSpace(DESKTOP)

Set objFolderItem = objFolder.ParseName("Test Shortcut.lnk")
Set objShortcut = objFolderItem.GetLink

objShortcut.SetIconLocation "C:\Windows\System32\SHELL32.dll", 13
objShortcut.Save

Before getting into the details we should mention the fact that this particular script must be run locally; that’s because it uses the Shell object, and the Shell object cannot be created on a remote machine. Run this on your own machine or run it as a logon script; just don’t expect to run it against a remote computer. It won’t work.

As for the script itself, we begin by defining a constant named DESKTOP and setting the value to &H10&; this happens to be the value used by the Shell object to bind to the Desktop folder. We create an instance of the Shell.Application object, and then use the NameSpace method to bind to the user’s desktop folder.

Note. This is the user’s desktop folder, and not the All Users’ Desktop folder. For information on binding to All Users’ Desktop or other special folders, see the special folder scripts in the Script Center Script Repository.

After binding to the folder we then call the ParseName method to bind to an individual file within that folder; as you can see here, we’re binding to a file with the filename Test Shortcut.lnk:

Set objFolderItem = objFolder.ParseName("Test Shortcut.lnk")

Finally we use the GetLink property to return the ShellLinkObject object that represents the file Test Shortcut.lnk. Once we do that we’re ready to change the icon, something we can do using two lines of code:

objShortcut.SetIconLocation "C:\Windows\System32\SHELL32.dll", 13
objShortcut.Save

In the first line we call the SetIconLocation method and pass it two parameters: the path to the file containing the icon (in this case, C:\Windows\System32\Shell32.dll) and the index number of the icon within that file. How in the world do we know what index number to use? Here’s one way to find out:

· Find a shortcut somewhere, right-click that shortcut, and then click Properties.

· On the Shortcut tab, click Change Icon.

· In the Change Icon dialog box, click Browse and then locate the file containing your icon. If you chose C:\Windows\System32\Shell32.dll then your screen should look something like this:

Icons are numbered beginning with 0 in the upper left-hand corner and continuing with all the icons in column 1. Here’s a revised graphic in which we’ve numbered some of the icons for you:

After setting the icon location and specifying the icon index, you call the Save method and – just like that – your shortcut icon will be changed.

Now, if they had an icon of a flying toaster we could understand why you’d want to use that. Otherwise….


How Can I Change the Target of a Desktop Shortcut?

Hey, Scripting Guy! How can I change the target that a desktop shortcut points to? For example, if I move a file from one server to another, I’d like be able to use a script to change the shortcut that points to that file.

-- AK

Hey, AK. This is an example of a scripting problem that’s actually pretty easy to solve, provided you know where to look for the solution. If you’re like most scripters, your first thoughts would be to use WMI or WSH; when both of those technologies turn out to be dead-ends you might think that maybe you can’t change shortcut targets using a script after all. Ah, but just when all seemed lost, who should come swooping in to save the day but our oft-neglected friend, the Shell object.

The Shell object is a kind of ragtag collection of scripting objects, many of which are either of minimal use to system administrators or whose functions are performed better/faster/easier using another scripting technology. But every now and then the Shell object provides an answer where none of these other scripting technologies are of much help. Modifying shortcut properties turns out to be just such a case.

We’re going to assume that you already know the location of the desktop shortcut that needs to be changed; for this example we’ll use a hypothetical shortcut named Accounts Payable Database.lnk found in the All Users desktop folder. With that in mind, here’s a script that changes the target of that shortcut to \\atl-fs-01\accounting\payable.exe:

Const ALL_USERS_DESKTOP = &H19&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(ALL_USERS_DESKTOP)
Set objFolderItem = objFolder.ParseName("Accounts Payable
Database.lnk")
Set objShellLink = objFolderItem.GetLink

objShellLink.Path = "\\atl-fs-01\accounting\payable.exe"
objShellLink.Save()

We start out by creating a constant ALL_USERS_DESKTOP and setting the value to &H19&. We then create an instance of the Shell object, and use the Namespace method to bind to the desktop folder. We use the ParseName method to link to the file itself (note that we have the specify only the file name – Accounts Payable Database.lnk – and not the entire path), and then use the GetLink method to retrieve the shortcut information.

After that it’s easy. We set the value of the Path property to the new shortcut target, and then call the Save() method to write this value to the shortcut itself. Voila: we’ve managed to change the target that this shortcut points to.

One thing to keep in mind here is that the Shell object is designed to work only on the local computer; you can’t create an instance of the Shell object on a remote machine. If you need to modify a shortcut on a remote computer, you’ll either need to run this as part of a logon script, or use a process similar to the one described in the September 1, 2004 Hey, Scripting Guy! column to first copy the script to the remote computer and then start the copied script on that machine.


How Can I Create a Folder on the Start Menu?

Hey, Scripting Guy! How can I create a folder on the Start menu? I want the folder to be named the same as the %username% environment variable.

-- SB

Hey, SB. As we are wont to do, we’re going to break this question down into subtasks. We’ll explain how to do each of these subtasks, then at the end put the pieces together to create a single script that solves your problem for you.

To begin with, we need to figure out the name of the logged-on user (which happens to map to the %username% environment variable). That’s easy: we just create an instance of the WSH Network object and retrieve the value of the UserName property. That’s what we do here:

Set objNetwork =
CreateObject("Wscript.Network")
strUser = objNetwork.UserName
strPath = "C:\Documents and Settings\" & strUser

Note that we’ve also created a variable named strPath and assigned it the value C:\Documents and Settings\ plus the user name (e.g., kenmyer). That gives us a path to a user folder like C:\Documents and Settings\kenmyer. If you want to create a shortcut to a different folder (for example, \\atl-fs-01\public\kenmyer), you just need to change this line of code accordingly.

Next we need to determine the locations of the Start menu Programs folder. That’s also easy. This time we create an instance of the WSH Shell object and call the SpecialFolders method, telling SpecialFolders we’d like the location of the Programs folder. We store the folder path in the variable strPrograms:

Set objShell = CreateObject("Wscript.Shell")
strPrograms = objShell.SpecialFolders("Programs")

(By the way, if you’d like to know the other folder paths you can locate using the SpecialFolders method, take a look at the Working With Special Folders section of the Microsoft Windows 2000 Scripting Guide.)

Finally we need to create the Start menu shortcut. Here’s the code that does that:

Set objShell = CreateObject("Wscript.Shell")
Set objShellLink = objShell.CreateShortcut(strPrograms & "\"
& strUser & ".lnk")
objShellLink.TargetPath = strPath
objShellLink.Description = "Home folder for " & strUser
objShellLink.WorkingDirectory = strPath
objShellLink.Save

Here we again use the WSH Shell object, this time calling the CreateShortcut method. To create the Start menu shortcut we need to pass CreateShortcut the complete path to the shortcut file we want to create (for example, C:\Documents and Settings\kenmyer\kenmyer.lnk). To determine this path, we simply combine the path to the user folder (C:\Documents and Settings\kenmyer), a slash (\), the user name (kenmyer), and the file extension for a shortcut file (.lnk). That’s what we do with this line of code:

Set objShellLink = objShell.CreateShortcut(strPrograms & "\" & strUser & ".lnk")

Next we assign values to some of the shortcut file’s properties. The TargetPath represents the path to the folder; thus we assign it the path to the folder. The Description represents the tooltip that appears when you pause the mouse over the item in the Start menu; it gets a simple description indicating that it’s the home folder for the logged-on user. Finally, WorkingDirectory represents, well, the working directory for the shortcut; it gets assigned the path to the folder. After assigning these property values we call the Save method to save the shortcut file and add it to the Start menu.

Now, as promised, here’s a script that combines all three of our subtasks:

Set objNetwork =
CreateObject("Wscript.Network")
strUser = objNetwork.UserName
strPath = "C:\Documents and Settings\" & strUser

Set objShell = CreateObject("Wscript.Shell")
strPrograms = objShell.SpecialFolders("Programs")

Set objShellLink = objShell.CreateShortcut(strPrograms & "\"
& strUser & ".lnk")
objShellLink.TargetPath = strPath
objShellLink.Description = "Home folder for " & strUser
objShellLink.WorkingDirectory = strPath
objShellLink.Save


How Can I Create a Shortcut in My Network Places?

Hey, Scripting Guy! How can I create a shortcut in My Network Places?

-- KP

Hey, KP. You’ll have to excuse us if we sound a little out of breath; we’ve been running around like crazy looking for a complicated and highly-technical solution to this problem. It was only after we failed miserably in that quest that we began to think about what we were trying to do. Maybe we were trying to make this much harder than we needed to. After all, My Network Places is nothing more than a folder on the hard disk, and a “network place” is nothing more than a shortcut. Could creating a network place involve nothing more than creating a shortcut in a folder? You be the judge:

Const NETHOOD = &H13&

Set objWSHShell = CreateObject("Wscript.Shell")
Set objShell = CreateObject("Shell.Application")

Set objFolder = objShell.Namespace(NETHOOD)
Set objFolderItem = objFolder.Self
strNetHood = objFolderItem.Path

strShortcutName = "Finance Department Public Folder"
strShortcutPath = "\\atl-finance-01\public"

Set objShortcut = objWSHShell.CreateShortcut _
(strNetHood & "\" & strShortcutName &
".lnk")
objShortcut.TargetPath = strShortcutPath
objShortcut.Save

We begin by defining a constant named NETHOOD and assigning it the value &H13&; we’ll use this in a moment to locate the My Network Places folder. We then create instances of two difference objects: the Windows Script Host Shell object and the Windows Shell object. That’s what these two lines of code do:

Set objWSHShell = CreateObject("Wscript.Shell")
Set objShell = CreateObject("Shell.Application")

Next we use a Windows Shell object method and property – Namespace and Self, respectively – to determine the location of the My Network Places folder. The Namespace method actually locates the folder for us; however, it returns a Folder object. For technical reasons we don’t need to worry about, we can’t do much with this object; therefore we use the Self property to return a FolderItem object representing the My Network Places folder. We do that because a FolderItem object is something that our script can deal with.

As soon as we have a FolderItem we can then grab the Path property and assign it to the variable strNetHood; strNetHood will now be equal to something along the lines of C:\Documents and Settings\kenmyer\NetHood. And, yes, that is a long and convoluted explanation for just three lines of code:

Set objFolder = objShell.Namespace(NETHOOD)
Set objFolderItem = objFolder.Self
strNetHood = objFolderItem.Path

Next we simply assign the name of the shortcut and the path to our network place to a pair of variables. We don’t really need to do this; we could just hard-code the values into the script. But assigning the values to variables will make it a little easier for you to modify the script to fit your own needs.

Now we turn to the WSH Shell object to actually create our shortcut. We begin by calling the CreateShortcut method, passing it the path to the new shortcut name. That will be a combination of four items: the path to the My Network Places folder (strNetHood), a trailing \, the name of our new shortcut (strShortcutName), and the file extension .lnk. That gives us a shortcut path similar to this:

C:\Documents and Settings\kenmyer\NetHood\Finance Department Public Folder.lnk

We set the TargetPath property of the shortcut to the network place (\\atl-finance-01\public) and then call the Save method to write the new shortcut to the file system. Just like that, we have a new network place.

Now, if you’ll excuse us, we need to go lie down for a moment. Who’d have thought that scripting would be such hard work?


How Can I Determine the Executable File Corresponding to a Shortcut?

Hey, Scripting Guy! How can I determine the executable file name corresponding to a .lnk shortcut file?

-- AM

Hey, DE. We’re glad you asked this question; unlike some questions we get (like, “How do I use a script to disable File and Print Sharing on my Windows 2000 computers?”), this happens to be one we have an answer for.

As it turns out, there’s a WMI class – Win32_ShortcutFile – that can retrieve this information for you. All you have to do is query the Win32_ShortcutFile class and ask for the Target property; that’s the name of the executable file (or script or document or whatever) that gets called when someone double-clicks the shortcut. Here’s a simple little script that lists all the shortcut files on a computer, specifying the icon caption that you see in My Computer or Windows Explorer (FileName), the name of the corresponding executable file (Target), and the file path to the shortcut itself (Description):

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery("Select * From Win32_ShortcutFile")
For Each objFile in colFiles
    Wscript.Echo "Name: " & objFile.FileName
    Wscript.Echo "Shortcut target: " & objFile.Target
    Wscript.Echo "File name: " & objFile.Description
Next

Of course, you don’t have to wade through all the shortcuts on a computer. Suppose you just want to locate the executable file for a single shortcut. In that case, you could use a script similar to this:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From Win32_ShortcutFile WHERE FileName = 'Adobe Photoshop Elements 2.0'")
For Each objFile in colFiles
    Wscript.Echo "Name: " & objFile.FileName
    Wscript.Echo "Shortcut target: " & objFile.Target
    Wscript.Echo "File name: " & objFile.Description
Next

Here’s another example, which checks to see if there are any shortcuts on a computer that reference Microsoft Virtual PC. Note that – per WMI conventions – any time you use a file path in a query you must use two \\’s in path names rather than one:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From Win32_ShortcutFile WHERE Target = " & _
        "'C:\\Program Files\\Microsoft Virtual PC\\Virtual PC.exe'")
For Each objFile in colFiles
    Wscript.Echo "Name: " & objFile.FileName
    Wscript.Echo "Shortcut target: " & objFile.Target
    Wscript.Echo "File name: " & objFile.Description
Next

That should give you a headstart on retrieving information about shortcuts. For those of you who’ve asked about creating and deleting shortcuts, we’ll address those topics very soon.


How Can I Start Windows Explorer Opened to a Specific Folder?

Hey, Scripting Guy! How can I start Windows Explorer opened to a specific folder?

-- CD

Hey, CD. We’re going to let you in on one of the Scripting Guys’ most sordid secrets: sometimes the Scripting Guys don’t agree with each other. No, it’s true; really. For example, some of the Scripting Guys think that soccer is a real sport; even worse, at least one of the Scripting Guys thinks that stock car racing is a real sport! Oh, brother.

Note. Following a brief … get-together … the Scripting Guy who writes this column has agreed that he was wrong and that both soccer and stock car racing are real sports after all. Sort of.

Here’s another area in which the Scripting Guys disagree. At least one of the Scripting Guys believes that system administrators and IT professionals would never stoop to using the Windows graphical user interface (GUI). And he’s not the only Microsoft-type who feels that way; we know of at least one Program Manager who, whenever he wants to start Word, opens a command window and types winword.exe at the command prompt. Other Scripting Guys would argue that system administrators don’t care whether they use the command line or a GUI tool: they just want to get the job done as quickly and as easily as possible.

So is one side right and one side wrong? Of course! However, just as we … agreed … that soccer and stock car racing were sports, we’ve also agreed that the command line and the GUI are equally good ways of getting Windows Explorer to open up with the focus on a specific folder. Therefore, we’re going to give you two different scripts to choose from, one command-line oriented, one GUI-oriented.

Let’s start with the command-line script first. To get this script (which we’ll call my-script.vbs) to work all you need to do is call the script from the command prompt, passing the name of the folder you want opened as the lone argument. In other words, to open Windows Explorer focused on C:\Scripts you’d type the following from the command prompt:

my-script.vbs "c:\scripts"

Are the double quotes required around the folder path? In this case, no. However, if there are any blank spaces in the path then the double quotes are required. This command string won’t work:

my-script.vbs c:\documents and settings\kmyer

Any time you pass a script an argument that includes a blank space the entire argument must be enclosed in double quotes (or else). In other words:

my-script.vbs "c:\documents and settings\kmyer"

That’s just the way the command shell works.

So what script are we running here? Good question. Turns out it’s this one:

Set objShell =
CreateObject("Wscript.Shell")

strPath = Wscript.Arguments(0)

strPath = "explorer.exe /e," & strPath
objShell.Run strPath

As usual, there’s really not that much to this. We start out by creating an instance of the Wscript.Shell object; this is the Windows Script Host object we can use to run scripts or executable files from within another script. We then take the first argument supplied to the script (c:\scripts) and stash that in a variable named strPath:

strPath = Wscript.Arguments(0)

So far so good, huh? In a moment, we’re going to use the Run method to start Windows Explorer. Before we do that, however, we should note that the Run method in WSH is pretty much the same thing as the Run dialog box. If you wanted to use the Run dialog box to start Windows Explorer (with the focus on the C:\Scripts folder) you’d type in this:

explorer.exe /e,c:\scripts

As it turns out, that’s the same syntax we use to start Windows Explorer using the Run method. All we have to do is construct that command and then execute it:

strPath = "explorer.exe /e," & strPath
objShell.Run strPath

In line 1 we’re taking the command explorer.exe /e, and tacking on the folder path (which is stored in the variable strPath); strPath will then take on the value explorer.exe /e,c:\scripts. In line 2 we call the Run method, passing the variable strPath as the command we want to run. If all goes well (and in scripting things always go well, right?), Windows Explorer will open up with the focus on C:\Scripts:

Cool.

So what’s wrong with that? Nothing; it works just fine. The only disadvantage to this script is the fact that it requires you to type in the full path to the folder; that can be a problem when you’re trying to open the folder C:\Documents and Settings\Default User\Application Data\Microsoft\SystemCertificates\My\Certificates. But as system administrators wedded to the command line, what choice to we have?

Well, you could always try the GUI approach (don’t worry; we won’t tell anyone):

Const WINDOW_HANDLE = 0
Const NO_OPTIONS = 0

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.BrowseForFolder _
(WINDOW_HANDLE, "Select a folder:",
NO_OPTIONS)

Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

objShell.Explore strPath

This script doesn’t require you to do any typing whatsoever. Instead, you simply start the script and it shows you the Browse For Folder dialog box:

Select a folder, click OK, and you’re on your way.

So how does this script work? Well, we start out by defining a pair of constants: WINDOW_HANDLE and NO_OPTIONS. WINDOW_HANDLE is a constant required by the BrowseForFolder method; NO_OPTIONS simply informs the script that we want to display a standard Browse For Folder dialog box. After that we create an instance of the Shell.Application object and then use this line of code to display the dialog box:

Set objFolder = objShell.BrowseForFolder _
    (WINDOW_HANDLE, "Select a folder:", NO_OPTIONS)

After the dialog box is displayed it will simply wait until we select a folder and then click OK. When that happens we use these two lines of code to: 1) create an object reference to the selected folder; and, 2) store the folder Path in the variable strPath:

Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

All we have to do now is call the Explore method, which exists solely to open Windows Explorer. By passing the variable strPath as the lone parameter that will cause Windows Explorer to open up with the focus on C:\Scripts:

objShell.Explore strPath

The moral of the story? Now you can use the command prompt to open up Windows Explorer or you can use the GUI to open Windows Explorer; it’s entirely up to you. And if you want to watch soccer or stock car racing the next time you feel like watching a sporting event, well, so be it. (Of course, you won’t be watching a real sporting event, but ….)


How Can I Use a Script to Lock the Taskbar?

Hey, Scripting Guy! How can I use a script to lock the Taskbar?

-- RK

Hey, RK. One of the Scripting Guys was a computer support person at the University of Washington when Windows 95 was released. This Scripting Guy has two lasting memories of those days. For one, he remembers having to install both Windows 95 and Microsoft Office on several hundred machines, back when that meant using 9 or 10 floppy disks to install Windows and another 14 or 15 to install Office. (CD drives? Network connections? This was back in the dark ages of computing, especially for people in the academic world.)

As you might expect, it took a couple of months for this Scripting Guy to get Windows and Office installed on all the department computers. Which leads into his second lasting memory of the Windows 95 era: spending the next few months going around from office to office helping people realign, resize, and lock down their Taskbars. Back in those days the Taskbar was not locked down by default; as a result, the UW ended up with Taskbars everywhere: docked on the right side, docked on the left side, docked on top of the screen. People resized their Taskbars so large that the toolbar filled up half the screen, or so small that they didn’t even know they had a Taskbar any more. For two months (although it seemed like two years) this Scripting Guy’s primary job was to reconfigure and lock down Taskbars. Day after day after day.

And, yes, coincidentally enough that was about the same time he began to wonder if maybe there might something else he could do with his life than be a computer support person.

Our poor little Scripting Guy; if only he’d had a script like this one at his disposal:

HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
ValueName = "TaskBarSizeMove"
dwValue = 0

objReg.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

As it turns out, the Taskbar can be locked and unlocked by toggling a value in the registry; in particular, by toggling the registry value Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskBarSizeMove. Set this value to 0 and the Taskbar will be locked; set this value to 1 and the Taskbar will be unlocked. As you might expect, in order to lock the Taskbar our script does nothing more than simply set the value of TaskBarSizeMove to 0.

To accomplish this feat the script starts out by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this constant to tell the script which registry hive to work with. (Locking and unlocking the Taskbar is done on a per-user basis.) We then use these two lines of code to connect to the WMI service on the local computer:

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

But don’t worry; you’re not limited to running this script locally. Instead, the script can easily be modified to work against a remote computer. To do that, simply assign the name of the computer to the variable strComputer. For example, this code binds to the WMI service on a computer named atl-ws-01:

strComputer = "atl-ws-01"
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Is that easier than walking around and manually locking the Taskbar on 300-400 computers? We can’t say for sure, but it definitely sounds easier, doesn’t it?

After making the connection to the WMI service we assign the registry path (Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced) to a variable named strKeyPath. We then assign the actual registry value (TaskBarSizeMove) we want to change to a variable named ValueName. Finally, we assign the value 0 to a variable named dwValue. This variable represents the value we want assigned to TaskBarSizeMove.

Note. What if we wanted to unlock the Taskbar instead of locking it? No problem; we’d just assign dwValue the value 1.

All we have to do now is call the SetDWORDValue method, passing as parameters the constant HKEY_CURRENT_USER and the variables strKeyPath, ValueName, and dwValue:

objReg.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

That will do the trick, although you’ll probably have to log off and then log back on before the change actually takes effect. Because of that you might want to add this code to a logoff script; that way the change will automatically take place when the user logs off. The next time the user logs on the Taskbar will be locked.

And, yes, our Scripting Guy desperately wishes he would have written this script 10 years ago. Of course, he still would have had to copy the script onto a floppy disk and then cart that floppy around from office to office. In addition, neither Windows Script Host nor WMI existed back then, which might have made it a bit more difficult to get the script to run. But those are just details, and surely our Scripting Guy would have figured out a way to sidestep minor issues like that.

Good point: not this Scripting Guy. But maybe someone else would have been able to suggest something.


How Can I Create a Shortcut For an Executable File That Uses Parameters?

Hey, Scripting Guy! How can I create a shortcut where the target path is an executable file that uses parameters (for example, something.exe -t –s)?

-- MC

Hey, MC. A week or so ago we mentioned that we were going to make some changes to Hey, Scripting Guy!; in particular, we were going to stop wasting time and start getting straight to the point. Needless to say, we didn’t exactly get around to that. Now we’re considering making a different change. You might have noticed that the layout for each of our Hey, Scripting Guy! columns includes a big Q for question and a big A for answer. That makes sense: after all, this is a question-and-answer column, something commonly referred to as “Q and A.” Except that Q and A is the way old fogeys describe this column, and the Scripting Guys would never want to be thought of as old fogeys. Therefore, we’re thinking about updating this column to A and A rather than Q and A, and replacing the big Q for question with a big A.

What’s the big A for? Well, for you old fogeys out there, the big A is for ask. You might be thinking, “Wait a second: ask is a verb, not a noun.” Well, that just shows how out-of-touch you are. At Microsoft (the company that consistently tries to reinvent the English language) the latest rage is to use the word ask as a noun; in particular, to use the word ask instead of using the word question. Here are a couple of examples drawn from emails the Scripting Guy who writes this column received the other day:

“I have an ask for everyone on this list.”

“Never mind; I discovered the answer to my own ask.”

OK, if you promise not to tell anyone, that sounds really weird to us, too. However, if that’s what it takes to sound cool, well, then that’s what it takes to sound cool. Therefore, MC, here’s an answer to your ask:

Set objShell = CreateObject("Wscript.Shell")
strDesktop = objShell.SpecialFolders("Desktop")

Set objShortcut = objShell.CreateShortcut(strDesktop & "\Test.lnk")
objShortcut.TargetPath = "Notepad.exe"
objShortcut.Arguments = "C:\Scripts\Test.txt"

objShortcut.Description = "Starts Notepad with a file already loaded."
objShortcut.WorkingDirectory = strDesktop

objShortcut.Save

So how did we answer this ask? (You know, that still sounds kind of dorky to us.) To begin with, we created an instance of the Wscript.Shell object:

Set objShell = CreateObject("Wscript.Shell")

We then grabbed the path to the Desktop folder, something we do using the Shell object’s SpecialFolders property and the parameter Desktop:

strDesktop = objShell.SpecialFolders("Desktop")

Note. If you’re wondering why we grabbed the path to the Desktop folder, well, that’s easy: we have to put our shortcut somewhere, so we decided to put it on the Desktop.

From there we call the CreateShortcut method, passing the path to the Desktop folder and the value \Test.link as the method parameter:

Set objShortcut = objShell.CreateShortcut(strDesktop & "\Test.lnk")

We probably don’t need to tell you that Test.lnk is the file name for our new shortcut file. But apparently we went ahead and told you anyway, didn’t we? Oh well.

For our little sample script we’re creating a shortcut that starts Notepad and then opens the file C:\Scripts\Test.txt. To that end, we assign Notepad.exe to the shortcut’s TargetPath property:

objShortcut.TargetPath = "Notepad.exe"

What’s that? Shouldn’t we have assigned Notepad.exe C:\Scripts\Test.txt to the TargetPath property? Well, you’d think so, wouldn’t you? However, that typically doesn’t work; in general, the target path needs to be a pointer to a specific file (or folder). In this case, that file is Notepad.exe; therefore, we need to set the TargetPath to Notepad.exe without including the parameter C:\Scripts\Test.txt.

So then where the heck do we put our parameter? That’s easy; we assign that value to the Arguments property:

objShortcut.Arguments = "C:\Scripts\Test.txt"

That’s where your command-line parameters go.

After that we assign values to the Description and WorkingDirectory properties. (Which we don’t have to do; these are optional properties.) We then call the Save method and officially create our new Desktop shortcut:

objShortcut.Save

And what do you think will happen when we double-click that shortcut? You already know what’s going to happen: Notepad is going to start up and, when it does, it’s going to load the file C:\Scripts\Test.txt. Which is just the sort of thing we wanted it to do.

OK. Having taken care of today’s ask we now have a tell for you. (Which is yet another example of Microsoft-speak. And, to be honest, we always thought that tell was a verb, too.) The Scripting Guys are still in Orlando, but that doesn’t mean cool things aren’t happening in the Script Center. For example, you still have plenty of time to win a Scripting Guys Gift Pack as part of our TechEd Challenge. We’ve got several new articles aimed at people interested in learning Windows PowerShell, and today we’re featuring part 2 of our two-part series on burning CDs with Windows Vista. If that doesn’t qualify as a tell, well, we don’t know what does.

Although, come to think of it, we don’t know what qualifies as a tell. Gee, maybe we are old fogeys after all.

Nah ….


How Can I Delete a Desktop Shortcut If I Don’t Know the Shortcut’s File Path?

Hey, Scripting Guy! I need to delete some desktop shortcuts; however, I don’t know which folder those shortcuts might be in. For example, they might be in C:\Documents and Settings\All Users\Desktop or they might be in, say, C:\Documents and Settings\kenmyer\Desktop. How can I delete these shortcuts if I’m not sure where they’re located?

-- SB

Hey, SB. You know, this is kind of a bad time; the second half of the UNLV-Georgia Tech game is just about to start. It’s hard enough to try and watch a basketball game at work as it is; to then have to answer a scripting question at the same time is darn near impossible.

On the other hand, Virginia is currently destroying Albany (66-45 with less than 8 minutes remaining), so maybe we can take some time out to answer this question after all. And before you ask, yes, the Scripting Guy who writes this column did pick Albany to upset Virginia, a selection that caused the Scripting Son to take one look at his father’s choices and go, “Look, if you’re not even going to try then why bother making picks in the first place?”

Strong words from the Scripting Son, especially in light of the fact that, after day 1 of the NCAA men’s basketball tournament, the Scripting Dad had the lead over the Scripting Son and the Scripting Brother in their annual March Madness challenge. In fact, if Gonzaga could have made a few shots and if Albany could have – well, OK, made a lot of shots – the Scripting Guy who writes this column would have a perfect score at the moment.

But then again, that’s the story of his life: he’s always this close to being perfect.

Do you realize that, as this column is being written, North Texas only trails Memphis by 6? Unbelievable.

Anyway, on the off-chance that some of you aren’t obsessed with the ups and downs of college basketball, here’s a script that can delete a desktop shortcut regardless of the folder where that shortcut actually resides:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_ShortcutFile Where FileName = 'Digital Voice Editor 2'")

For Each objItem in colItems
    If Instr(objItem.Name, "desktop") Then
        strPath = objItem.Name
        strPath = Replace(strPath, "\", "\\")
        Set colFiles = objWMIService.ExecQuery _
            ("Select * From CIM_Datafile Where Name = '" & strpath & "'")
        For Each objFile in colFiles
            objFile.Delete
        Next
    End If
Next

Can we explain how this works? Let’s see … well, Virginia just called timeout, so the answer to that is yes, we can explain how this works. (Though we’ll have to do it before the 30-second timeout is over.) What we’ve got here is a desktop shortcut named Digital Voice Editor 2. We want to delete it, but we don’t know for sure which folder the shortcut resides in. Is that a problem? Sure sounds like one, doesn’t it?

As it turns out, though, this is no problem at all. To delete the shortcut we start out by connecting to the WMI service on the local computer. (Although you can easily modify this script to remove desktop shortcuts from a remote computer: all you have to do is assign the name of that computer to the variable strComputer). After making the connection we then use the ExecQuery method and the Win32_ShortcutFile class to return a collection of all the shortcut files that have a FileName of Digital Voice Editor 2:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_ShortcutFile Where FileName = 'Digital Voice Editor 2'")

Just a second, SB: Memphis is now up by 13. That’s more what we expected. On the other hand, Albany is now trailing 80-55. In retrospect, picking Albany to win might have been a mistake.

But hey, there’s still 55 seconds left.

Now, back to work. As it always does, the ExecQuery method returns a collection of items; in this case, that’s a collection of all the shortcuts that have the name Digital Voice Editor 2. That means that our next step is to set up a For Each loop that loops through that entire collection:

For Each objItem in colItems

The first thing we do inside that loop is actually an optional step. Because SB specifically mentioned desktop shortcuts we use the InStr function to determine whether the term desktop appears anywhere within the file Name (which, when it comes to the Win32_ShortcutFile class, is equivalent to the file path):

If Instr(objItem.Name, "desktop") Then

This enables us to remove desktop shortcuts while retaining, say, Start Menu shortcuts. If you’d prefer to delete all the shortcuts to a file (whether or not they happen to be on the desktop) then simply delete the If-Then statement.

And yes, we know: every time UNLV pulls ahead Georgia Tech comes roaring back. Of course, we can’t actually watch that game because CBS insists that either UNLV or Georgia Tech is in the same geographical area as Redmond, Washington. Apparently we don’t know as much about geography as we thought we did.

Geographic Update. The Notre Dame (Indiana) and Winthrop (North Carolina) game is blacked-out for the same geographical reason. OK ….

Now, where were we? Oh, right: inside the For Each loop. As you can see, inside the loop we assign the value of the file Name (which, again, is the same thing as the file path) to a variable named strPath. And then we encounter this line of code:

strPath = Replace(strPath, "\", "\\")

Why do we need this line of code? Well, our file path is going to be something like this: C:\Documents and Settings\All Users\Desktop\Digital Voice Editor 2.lnk. There’s nothing wrong with that; that’s what a file path is supposed to look like. However, to actually delete this shortcut we need to use that path in a WMI query. That’s a problem, because the \ is a reserved character and can’t be used in a query without first being “escaped” (which simply means that you need to preface each of those characters with another \).

And that’s what we use the Replace function for: we replace each \ with a \\. That gives us a path (stored in the variable strPath) that looks like this: C:\\Documents and Settings\\All Users\\Desktop\\Digital Voice Editor 2.lnk.

That also gives us a path we can use in a WQL query:

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_Datafile Where Name = '" & strpath & "'")

What’s this query going to do? It’s going to return a collection of all the files that have a Name (path) equal to the value of the variable strPath. And after it does so we’re going to use this block of code to loop through that collection and delete the corresponding shortcut file:

For Each objFile in colFiles
    objFile.Delete
Next

And then we swing around and repeat the process, just in case there are any other shortcuts named Digital Voice Editor 2 that we need to delete.

We hope that helps, SB. As for the Scripting Guy who writes this column, nothing will help; he’s having a miserable day. At lunch time they had a projector TV set up in the cafeteria. NCAA basketball, right? Wrong: instead, they were showing a cricket match. (Yes, a cricket match.) Not long after lunch the Scripting Guy who writes this column had to go to a meeting, meaning that he missed both Wisconsin’s frenzied comeback against Texas A&M-Corpus-Christi and Nevada’s overtime win over Creighton. Between cricket matches, meetings, and the need to do his job the Scripting Guy who writes this column has hardly had a chance to watch any basketball whatsoever. Apparently Microsoft has lost its sense of priorities.


How Can I Correlate Logical Drives and Physical Disks?

Hey, Scripting Guy! How can I correlate logical drives and physical disks?

-- LB

Hey, LB. We’ll make a deal with you: we’ll show you a script that can tell you which logical drives are found on which physical disks, and you promise not to say, “Wait, I don’t understand how this script works.” To tell you the truth, we’re not so sure that we understand how the script works, either. As you’ll see it’s a bit wacky, to say the least.

The problem is that there is no simple, straightforward way to retrieve the logical disks associated with a given physical disk. Instead, we have to use a couple of association classes (Win32_DiskDriveToDiskPartition and Win32_LogicalDiskToPartition) to figure out the relationship between logical drives and disks. There’s nothing wrong with doing this, except that association classes can be extremely confusing, and the code required to use them can be even more confusing. If you’re a relative newcomer to scripting, this might be a time when you just use the code without trying too terribly hard to understand it all.

Here’s the script:

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

Set colDiskDrives = objWMIService.ExecQuery("SELECT * FROM Win32_DiskDrive")

For Each objDrive In colDiskDrives
    Wscript.Echo "Physical Disk: " & objDrive.Caption & " -- " & objDrive.DeviceID
    strDeviceID = Replace(objDrive.DeviceID, "\", "\\")
    Set colPartitions = objWMIService.ExecQuery _
        ("ASSOCIATORS OF {Win32_DiskDrive.DeviceID=""" & _
            strDeviceID & """} WHERE AssocClass = " & _
                "Win32_DiskDriveToDiskPartition")

    For Each objPartition In colPartitions
        Wscript.Echo "Disk Partition: " & objPartition.DeviceID
        Set colLogicalDisks = objWMIService.ExecQuery _
            ("ASSOCIATORS OF {Win32_DiskPartition.DeviceID=""" & _
                objPartition.DeviceID & """} WHERE AssocClass = " & _
                    "Win32_LogicalDiskToPartition")

        For Each objLogicalDisk In colLogicalDisks
            Wscript.Echo "Logical Disk: " & objLogicalDisk.DeviceID
        Next
        Wscript.Echo
    Next
    Wscript.Echo
Next

We told you it was weird-looking, didn’t we? It actually starts off innocently enough: we simply connect to the WMI service and then use the ExecQuery method to retrieve a collection of all the physical disk drives installed on a computer. We create a For Each loop to loop through the collection of disk drives and echo the values of the Caption and DeviceID properties.

Now it starts to get a little wild. The DeviceID – which uniquely identifies each disk drive – will have a value like this: \\.\PHYSICALDRIVE0. We’re going to use this value in a query, so we have to “escape” each \ with a second \. (Why? Because a single \ is a reserved character in WMI.) That’s what this line of code does:

strDeviceID = Replace(objDrive.DeviceID, "\", "\\")

After we do that we can then use this query to return all the disk partitions associated with the first physical disk drive on the computer:

Set colPartitions = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_DiskDrive.DeviceID=""" & _
        strDeviceID & """} WHERE AssocClass = " & _
            "Win32_DiskDriveToDiskPartition")

Believe it or not we’re making progress now; after all, we now have a collection of all the disk partitions found on the first drive. With the partitions in hand we set up a second For Each loop to iterate through these partitions; for each partition in the collection we then use this query to retrieve all of the logical drives found on those partitions:

Set colLogicalDisks = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_DiskPartition.DeviceID=""" & _
        objPartition.DeviceID & """} WHERE AssocClass = " & _
            "Win32_LogicalDiskToPartition")

All that’s left now is to set up a third For Each loop, this one to echo back all the logical disks found on the first partition of the first physical disk:

For Each objLogicalDisk In colLogicalDisks
    Wscript.Echo "Logical Disk: " & objLogicalDisk.DeviceID
Next

We loop around and repeat the process for all the remaining partitions on disk 1, then loop back even further and repeat the process for any additional disk drives found on the computer. When you’re all done, you’ll get back a report similar to this:

Physical Disk: IC25N040ATMR04-0 -- \\.\PHYSICALDRIVE0
Disk Partition: Disk #0, Partition #0

Disk Partition: Disk #0, Partition #1
Logical Disk: C:

Disk Parititon: Disk #0, Partition #2
Logical Disk: D:

Physical Disk: Memory Stick Slot -- \\.\PHYSICALDRIVE1

As you can see, the first disk has three partitions and two logical drives: drives C and D. The second disk has no partitions or logical drives. (What, you think Microsoft would give the Scripting Guys computers that actually had two disk drives in them?)

Like we said, if you don’t follow all this, don’t worry about it. No one knows how hot dogs are made, either, and the truth is, you probably don’t want to know. Just slap some mustard and relish on the thing and enjoy. The same thing applies to this script (although possibly without the mustard and relish).


How Can I Determine the Percentage of Free Space on a Drive?

Hey, Scripting Guy! I know how to determine the amount of free space on a drive, but how can I determine the percentage of free disk space on a drive?

-- MS

Hey, JP. You’re right: determining the amount of free disk space on a drive is pretty easy. For example, this simple little script tells you the amount of free space (in bytes) for drive C on a computer:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_LogicalDisk Where DeviceID = 'C:'")
For Each objDisk in colDisks
    Wscript.Echo objDisk.FreeSpace
Next

And we have good news for you: determining the percentage of free disk space is almost as easy. That’s because the percentage of free disk space requires just a simple calculation: you take the available disk space and divide it by the total size of the drive. Suppose we had a really small drive, a 10-byte drive with 2 bytes of free disk space. To determine the percentage of free disk space, we’d divide 2 by 10 (free space by total size) and get a value of .20. In other words, 20% free space on our teeny-tiny drive.

It’s a simple equation, and we already have half of it: the FreeSpace property of the Win32_LogicalDisk class tells us the amount of free space. And although you might not have realized it, we also have the second half of the equation: the Size property of that same class tells us the size of the drive. All we need to do in our script is divide the FreeSpace by the Size, and we have our percentage:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_LogicalDisk Where DeviceID = 'C:'")
For Each objDisk in colDisks
    intFreeSpace = objDisk.FreeSpace
    intTotalSpace = objDisk.Size
    pctFreeSpace = intFreeSpace / intTotalSpace
    Wscript.Echo pctFreeSpace
Next

Nifty, huh? The only problem with this script is that it reports back a decimal number like 0.22457268101427. Technically accurate, yes, but an answer that doesn’t really look much like a percentage. So let’s use VBScript’s FormatPercent function to get back a value like 22.45%:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_LogicalDisk Where DeviceID = 'C:'")
For Each objDisk in colDisks
    intFreeSpace = objDisk.FreeSpace
    intTotalSpace = objDisk.Size
    pctFreeSpace = intFreeSpace / intTotalSpace
    Wscript.Echo FormatPercent(pctFreeSpace)
Next

That’s much better.

Just for the heck of it, here’s another variation. By default, the Win32_LogicalDisk class looks at all disk drives on your computer, including floppy drives, removable Zip drives, etc. If you’d like to get back free space information on just your hard drives, modify your WQL Query so that it selects all drives with a DriveType of 3 (which, in the world of WMI, represents a hard drive). In other words:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_LogicalDisk Where DriveType = 3")
For Each objDisk in colDisks
    intFreeSpace = objDisk.FreeSpace
    intTotalSpace = objDisk.Size
    pctFreeSpace = intFreeSpace / intTotalSpace
    Wscript.Echo objDisk.DeviceID, FormatPercent(pctFreeSpace)
Next


How Can I Determine Which Users are Using the Most Disk Space on a Drive?

Hey, Scripting Guy! How can I determine which users are using the most disk space on a drive?

-- PW

Hey, PW. Interesting question. In theory, you could probably do this by retrieving a collection of all the files on a drive, checking the owner, determining the file size, and then keeping a running tally of the amount of disk space charged to each user. Like we said, in theory that would work, but it would be a complicated little script, and, depending on the number of files on the drive, you might bring your computer to its knees while the script performs all its machinations.

Therefore, we decided to go with an approach that is much easier, much faster, and much less resource-intensive. The catch? (Oh, come on: you knew there had to be a catch here.) You’ll have to enable disk quotas on the drive in question in order for this to work.

We won’t spend much time talking about enabling and disabling disk quotas in today’s column. If you’re running Windows XP or Windows Server 2003, you can find a fairly extensive article on disk quotas in our Scripting for Windows Server 2003 center. If you’re running Windows 2000, the Microsoft Windows 2000 Scripting Guide includes information on using scripts to enable and disable disk quotas. Keep in mind that when we say you have to enable disk quotas on the drive, that doesn’t mean you have to enforce those quotas: you can enable disk quotas and allow the operating system to tally up the amount of disk space charged to each user without restricting access to the drive in any way. And, of course, you can always enable disk quotas, run your script, and then disable disk quotas again.

Note. If you decide to do the old enable/disable trick, keep in mind that it will likely take several minutes – at least – for the operating system to tally up disk space use after quotas have been enabled. In other words, don’t enable disk quotas and then immediately run the script to determine disk space usage; that’s probably not going to work.

Assuming you have enabled disk quotas on drive C of a computer, and assuming you’re running Windows XP or Windows Server 2003, here’s a script that will return the amount of disk space currently charged to each user:

strComputer = "."

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

strDrive = "Win32_LogicalDisk.DeviceID=" & chr(34) & "C:" & chr(34)

Set colQuotas = objWMIService.ExecQuery _
    ("Select * from Win32_DiskQuota Where QuotaVolume = '" & strDrive & "'")

For Each objQuota in colQuotas
    Wscript.Echo "User: "& objQuota.User
    Wscript.Echo "Disk Space Used: "& objQuota.DiskSpaceUsed
Next

As you can see, the script begins simply enough, setting a variable named strComputer to a dot (WMI notation for the local computer) and then connecting to the WMI service. (Although this example connects to the WMI service on the local computer, you can retrieve disk quota information for a remote computer simply by assigning the name of that computer to the variable strComputer.) We then have this interesting line of code:

strDrive = "Win32_LogicalDisk.DeviceID=" & chr(34) & "C:" & chr(34)

As it turns out, the Win32_DiskQuota class – the class we’ll be working with to retrieve information about disk space usage – is a bit different than most WMI classes. For one thing, the QuotaVolume property – which indicates the drive we’re working with – isn’t a typical WMI property; instead, it’s actually an embedded instance of the Win32_LogicalDisk class. That’s fine, but it does make it a bit awkward to include QuotaVolume in a Where clause; that’s because the value of QuotaVolume will look something like this:

Win32_LogicalDisk.DeviceID="C:"

Inside a Where clause, it’s tricky to deal with the double quote marks around the drive letter. Because of that, we use the preceding line of code to create a string value equivalent to Win32_LogicalDisk.DeviceID="C:", using Chr(34) as a way to add in the double quotes. It’s an extra line of code, but it’s much easier to put the variable strDrive into the Where clause than it is to put Win32_LogicalDisk.DeviceID="C:".

Trust us; it is.

Speaking of which, the next thing we do is call the ExecQuery method to return a collection of all the disk quota entries for drive C:

Set colQuotas = objWMIService.ExecQuery _
    ("Select * from Win32_DiskQuota Where QuotaVolume = '" & strDrive & "'")

From there we set up a For Each loop to walk through the collection, echoing back the user name and the amount of disk space (for drive C) currently charged to the user.

Now, that works great on Windows XP or Windows Server 2003, but it won’t do you much good on Windows 2000; that’s because Windows 2000 doesn’t support the Win32_DiskQuota class. So are you out of luck if you’re running Windows 2000? Of course not; you can just use the DiskQuota object instead:

Set colDiskQuotas = CreateObject("Microsoft.DiskQuota.1")
colDiskQuotas.Initialize "C:\", True

For Each objUser in colDiskQuotas
    Wscript.Echo "Logon name: " & objUser.LogonName
    Wscript.Echo "Quota used: " & objUser.QuotaUsed
Next

As you can see, here we create an instance of the Microsoft.DiskQuota.1 object, then use the Initialize method to return the collection of disk quota entries for C:\. (Don’t leave off the \; it’s required.) We then set up a For Each loop and, for each quota entry, echo the values of the LogonName and the QuotaUsed properties.

And you’re right: this script is a lot easier than the one we showed you for Windows XP and Windows Server 2003. So why didn’t we just show you this script and call it good? Well, the catch here – oh, come on: you knew there had to be a catch here, too – is that the DiskQuota object can’t be created remotely; that means the latter script can run only on the local computer. The WMI script might be a tad bit more complicated, but it has the advantage of working against remote computers as well.


How Can I Modify a Disk Quota Entry Under Windows 2000?

Hey, Scripting Guy! How can I modify a disk quota entry under Windows 2000?

-- CL

Hey, CL. We have some good news and some bad news for you. The bad news is that we’re doing one of those good news-bad news things that nobody ever likes. The good news is that you actually can modify a disk quota entry on a Windows 2000 computer. You can’t do this remotely: the script has to run on the same computer as the disk quota entry. But at least it will do what you ask it to.

For example, here’s a script that changes the quota threshold (the point at which a low disk space warning is issued) and the actual disk quota limit. This script changes the quota entry for user kenmyer on drive C of a computer; on Windows 2000 you have to manage quota entries user-by-user, and disk-by-disk. (In other words, no queries that grab all the disk quota entries for all the users on all the disks.)

Here’s the script:

Set colDiskQuotas = CreateObject("Microsoft.DiskQuota.1")
colDiskQuotas.Initialize "C:\", True
Set objUser = colDiskQuotas.FindUser("kenmyer")

objUser.QuotaThreshold = 90000000
objUser.QuotaLimit = 100000000

The script begins by creating an instance of the Microsoft.DiskQuota.1 object. This object is actually part of the so-called Shell Objects for Scripting, which is why the script must run locally: the Shell objects can’t be created on remote computers. Why? That’s like asking why is the grass green or why is the sky is blue: no one knows.

Well, OK, maybe they do know why the grass is green and the sky is blue. Nevertheless, don’t try to create the Microsoft.DiskQuota.1 object on a remote machine; it won’t work.

We then call the Initialize method to bind to the disk quota information for drive C. As you can see, the Initialize method takes two parameters: the drive we want to connect to (C:) and a second parameter that indicates whether we want to open the drive with read-write access or read-only access. We need read-write access in order to modify a quota entry, so we set this second parameter to True.

After we connect to the disk we need to bind to the disk quota entry for kenmyer; that’s what we do here:

Set objUser = colDiskQuotas.FindUser("kenmyer")

We then simply specify new values for the QuotaThreshold and QuotaLimit properties (in bytes) and we’re done. We don’t even have to call a Save method of any kind.

In case you’re wondering, there’s actually quite a bit you can do with disk quotas on Windows 2000; the one limitation is that everything you do must take place on the local machine. For more information, you might check out this portion of the Microsoft Windows 2000 Scripting Guide.


How Can I Report Disk Quota Information For the Logged-on User?

Hey, Scripting Guy! In a logon script, how can I report back disk quota information (like amount of space used and amount of space remaining) for the logged-on user?

-- HY

Hey, HY. You know, today is shaping up as another tough day for the Scripting Guy who writes this column. For example, even as we speak he’s busy sipping a cappuccino in a sidewalk café on the Piazza San Marco in Venice, perusing an Italian newspaper and waiting for the family to begin their visit to the Basilica di San Marco. From there, they’ll take a backstage tour of the Doge’s Palace. After that the Scripting Guy who writes this column will likely eat some gelato (Italian ice cream), take a leisurely cruise along the Grand Canal, then eat some more gelato. Definitely going to be a tough day. Oh, waiter: un altro cappuccino, per favore.

Note. As usual, we know what you’re thinking. You’re thinking, “But the Scripting Guys work really hard, don’t they? Therefore, the Scripting Guy who writes this column truly deserves a vacation like this, doesn’t he?” To answer your questions: no they don’t, and no he doesn’t. But he’s taking advantage of it anyway.

Needless to say, the Scripting Guy who writes this column is finding it hard to write this column. But that’s OK: after all, Venice is a tourist trap, there’s no doubt about that. And that’s good, because it means there are street vendors everywhere, street vendors happy to sell you just about anything, including a logon script that reports back disk quota information for the logged-on user:

strComputer = "."

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName
strDomain = objNetwork.UserDomain

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

Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_QuotaSetting Where State = 1")

For Each objDisk in colDisks
    strDrive = objDisk.VolumePath
    strDrive = Replace(strDrive, "\", "")

    Set objQuota = objWMIService.Get _
        ("Win32_DiskQuota.QuotaVolume='Win32_LogicalDisk.DeviceID=" & chr(34) & strDrive & chr(34) & "'," & _
            "User='Win32_Account.Domain=" & chr(34) & strDomain & chr(34) & _
                ",Name=" & chr(34) & strUser & chr(34) & "'")

    Wscript.Echo "Drive: " & objDisk.VolumePath
    Wscript.Echo "Disk Space Used: " & Int(objQuota.DiskSpaceUsed / 1048576) & " megabytes"
    Wscript.Echo "Quota Limit: " & Int(objQuota.Limit  / 1048576) & " megabytes"
    Wscript.Echo "Disk Space Remaining: " & Int((objQuota.Limit - objQuota.DiskSpaceUsed) / 1048576) & _
        " megabytes"
    Wscript.Echo
Next

Before we go any further, we should point out that the Scripting Guy who writes this column speaks Italian even worse than he speaks English; that’s making it hard for him to translate the explanation behind this script. (Mario’s – the street vendor we bought the script from – explanation is written in Italian. For some reason, none of the street corner script vendors in Venice seem to speak English.) Nevertheless, we’ll see what we can do.

Note. Mario has managed to communicate to us that this script works only on Windows XP and later versions of Windows. If you need to perform this task on Windows 2000, you can either take a look at the Microsoft Windows 200 Scripting Guide and try to figure out how to do this or let us know, and we’ll see if we can address this in a future column.

Mario also let us know that this script works on Windows Vista only with elevated privileges. That means you should run the script from the command prompt, but when you start the command prompt, right-click and select Run As Administrator.

To begin with, we (or, more correctly, Mario) creates an instance of the Wscript.Network object; he then uses these two lines of code the grab the values of the UserName and UserDomain properties and store them in the variables strUser and strDomain, respectively:

strUser = objNetwork.UserName
strDomain = objNetwork.UserDomain

As you probably know, the UserName returns the logon name of the user (e.g., kenmyer), while the UserDomain returns the domain name for that user (e.g., fabrikam). And that’s good, because when we set out to retrieve disk quota we’re going to need both the user’s name and his or her domain name.

After grabbing the user name and domain name Mario then connects to the WMI service on the local computer. Could we perform this feat against a remote computer? Beats us; we’ll ask Mario.

Mario says, “Certamente.” (Certainly) If you need information about the logged-on user’s disk quota space on a remote computer just assign the name of that remote computer to the variable strComputer:

strComputer = "atl-fs-01"

However, keep one thing in mind: to use WMI remotely requires you to be a local administrator on the remote computer. Because this is designed to be a logon script, that means that the logged-on user (e.g., Ken Myer) must be a local admin on any remote machine the script connects to. So maybe you can get information about disk quotas on a remote computer. Or maybe you can’t.

That brings us to this line of code:

Set colDisks = objWMIService.ExecQuery _
    ("Select * From Win32_QuotaSetting Where State = 1")

What we want to do is check disk quota information for all the disk drives on the local computer. Of course, disk quotas might not even be enabled on all the disk drives; if that’s the case, then our script might return misleading information. (For example, suppose disk quotas are not enabled on a drive. In that case the script will say that the user has 0 available megabytes of quota space. That’s because, technically, no disk quotas means that no one has any disk quota space.) That’s why we added the Where clause WhereState = 1; this filters our data so we get back information only about disk drives where quotas have actually been enabled.

Note. Need more information about working with disk quotas? In that case, Mario recommends that you take a look at this article in the Windows Server 2003 section of the Script Center.

Our next step is to set up a For Each loop that loops us through all the disk drives returned by our WQL query. (That is, all the disk drives where disk quotas have been enabled.) The first thing Mario does inside that For Each loop? Why, he executes these two lines of code, of course:

strDrive = objDisk.VolumePath
strDrive = Replace(strDrive, "\", "")

Why these two lines? Well, to begin with, we need the drive letter of the disk drive in order to retrieve quota information for the logged-on user. (Which makes sense: if you don’t know the drive letter how would the script know which drive to access?) The VolumePath property just happens to contain the drive letter, so we grab the value of that property and stash it in the variable strDrive.

Unfortunately, though, there’s a slight problem here. The VolumePath property stores drive letters like this: C:\. That’s fine, except that the Win32_DiskQuota class needs to work with drive letters in this format: C: (no trailing \). Therefore, in our second line of code we simply use the Replace function to replace any instances of the \ character with, well, nothing:

strDrive = Replace(strDrive, "\", "")

And then … well, and then we run into this:

Set objQuota = objWMIService.Get
_
("Win32_DiskQuota.QuotaVolume='Win32_LogicalDisk.DeviceID="
& chr(34) & strDrive
& chr(34) & "'," & _
"User='Win32_Account.Domain=" & chr(34)
& strDomain & chr(34)
& _
",Name=" & chr(34) & strUser & chr(34) &
"'")

You’re right: this could very well be the scariest-looking WQL query ever devised. (Mario, what were you thinking here!?!) We’re not going to explain this query in any detail today; for an explanation like that, see our article on disk quotas. For now, we’ll simply note that this query binds us to the first disk drive in the collection; more precisely, it binds us to an instance of the Win32_DiskQuota class where the QuotaVolume property happens to be an instance of the Win32_LogicalDisk class where the DeviceID property is equal to the value of the variable strDrive.

Whew. Just a second while we catch our breath.

OK; we’re ready now. And then, after connecting to the appropriate Win32_DiskQuota class, we retrieve quota information for the instance of the Win32_Account class where the Domain property is equal to the user domain and the Name property is equal to the user name.

Thanks, Mario.

We think.

To make a long story short, suppose the first disk drive in our collection of quota-enabled drives is drive C, and suppose the logged-on user is fabrikam\kenmyer. In that case, we’re going to get back disk quota information for user fabrikam\kenmyer for drive C of the local computer. And then we’re going to use these lines of code to echo back that information:

Wscript.Echo "Drive: " & objDisk.VolumePath
Wscript.Echo "Disk Space Used: " & Int(objQuota.DiskSpaceUsed / 1048576) & " megabytes"
Wscript.Echo "Quota Limit: " & Int(objQuota.Limit  / 1048576) & " megabytes"
Wscript.Echo "Disk Space Remaining: " & Int((objQuota.Limit - objQuota.DiskSpaceUsed) / 1048576) & _
    " megabytes"

Granted, some of these lines of code look a little scary, too. That’s because we’re doing a little bit of math and a little formatting before echoing back our property values. By default, disk quota information is returned as bytes; thus you’ll learn that Ken Myer has used 543271897 bytes of disk space. What the heck does that mean? We have no idea; that’s why we take the value of the DiskSpaceUsed property and divide it by 1048576, converting the byes to megabytes:

objQuota.DiskSpaceUsed / 1048576

That’s going to give us back a value similar to this:

518.10445499420166015625

Because we don’t really need to see all those decimal points we use the Int function to take the result of our calculation and return just the integer portion (518). We then do the same thing for the Limit property, which returns information about the user’s disk quota limit.

So then how do we know how much disk space is still available to Ken Myer? That’s easy: we simply take the quota space given to him (Limit) and subtract the amount of disk space he’s already used (DiskSpaceUsed). That’s what we do here:

Wscript.Echo "Disk Space Remaining: " & Int((objQuota.Limit - objQuota.DiskSpaceUsed) / 1048576) & _
    " megabytes"

From there we loop around and repeat the process with the next disk drive in the collection.

That should do it, HY. If you encounter any problems, just let us know.

Wait, check that: let Mario know. As for the Scripting Guy who writes this column, it’s just about time to learn about the “Secret Itineraries” of the Doge’s Palace. The tour sounds pretty interesting, using secret passages to move throughout the palace. Most likely, however, the Scripting Guy who writes this column will skip the Torture Room. Not because he’s squeamish, mind you. But, after all, he is on vacation, and walking into the Torture Room will remind him too much about walking into his manager’s office for their weekly one-on-one sessions.


How Can I Retrieve Disk Quota Information for a Single User?

Hey, Scripting Guy! How can I retrieve disk quota information for a single user?

-- AB

Hey, AB. There are a couple different ways to get disk quota information for a single user, depending on which version of Windows you have installed on the computer. If you’re running Windows XP or Windows 2003, you can retrieve disk quota information for a user via the Win32_DiskQuota class. For example, here’s a script that retrieves disk quota information for user kenmyer:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

Set colQuotas = objWMIService.ExecQuery _
    ("Select * From Win32_DiskQuota Where " & _
        "User='Win32_Account.Domain=""fabrikam"",Name=""kenmyer""'")

For Each objQuota in colQuotas
    Wscript.Echo "Disk Space Used: " & objQuota.DiskSpaceUsed
    Wscript.Echo "Limit: " & objQuota.Limit
    Wscript.Echo "Quota Volume: " & objQuota.QuotaVolume

    intStatus = objQuota.Status
    Select Case intStaus
        Case 0
            strStatus = "OK"
        Case 1
            strStatus = "Warning limit reached"
        Case 2
            strStatus = "Quota exceeded"
        Case Else
            strStatus = "Unknown"
     End Select

    Wscript.Echo "Status: " & strStatus
    Wscript.Echo "Warning Limit: " & objQuota.WarningLimit
Next

OK, granted, the WQL query looks a little crazy (all those double quote marks!), but that’s just the way the Win32_DiskQuota class is designed: the User property is actually a reference property, which is a pointer to another object and another class (in this case, the Win32_Account class). Fortunately, while our might not pretty, it’s not all that hard to deal with. What if you want to get quota information for a user named carolphilips, whose account is in the contoso domain? That’s easy; just replace kenmyer with carolphilips and fabrikam with contoso:

Set colQuotas = objWMIService.ExecQuery _
    ("Select * From Win32_DiskQuota Where " & _
        "User='Win32_Account.Domain=""contoso"",Name=""carolphilips""'")

And at least the rest of the script is pretty straightforward; we just echo the values of the various properties. The only “fancy” thing we’ve added is a Select Case statement that converts the numeric value returned by the Status property to a readable string. For example, if Status comes back as a 2, the script converts that to the string Quota exceeded. Other than that, about the only thing you might want to do to this script is divide all the quota numbers by 1024. That would convert these values (which come back as kilobytes) to megabytes.

Incidentally, this script returns quota information for kenmyer on all the drives on the computer. What if you only want quota information for a single drive? Ah, you just had to ask that, didn’t you? This can be done; it’s just that the WQL query looks even weirder due to the fact that the QuotaVolume property is yet another reference property:

Set objQuota = objWMIService.Get _
    ("Win32_DiskQuota.QuotaVolume='Win32_LogicalDisk.DeviceID=""C:""'," & _
        "User='Win32_Account.Domain=""tomservo"",Name=""Administrators""'")

If you need to get information for a different drive, then just copy the preceding query and replace C: with the appropriate drive letter.

Of course, while this script works great on Windows XP and Windows 2003, it doesn’t do you much good on Windows 2000; that’s because the Win32_DiskQuota class isn’t supported on Windows 2000. (Nor is it supported on Windows NT 4.0 or Windows 98, at least in part because those versions of Windows don’t support disk quotas, period.) So what do you do if you need to get disk quota information for a Windows 2000 computer?

Well, in that case, you use this script, which takes advantage of the Shell object to get disk quota information for kenmyer for drive C: on the local computer:

Set colDiskQuotas = CreateObject("Microsoft.DiskQuota.1")
colDiskQuotas.Initialize "C:\", True
Set objUser = colDiskQuotas.FindUser("fabrikam\kenmyer")

Wscript.Echo "Logon name: " & objUser.LogonName
Wscript.Echo "Quota limit: " & objUser.QuotaLimit
Wscript.Echo "Quota threshold: " & objUser.QuotaThreshold
Wscript.Echo "Quota used: " & objUser.QuotaUsed

As you can see, this is actually a shorter, more elegant-looking script, and with relatively few double quote marks. So why don’t we recommend that you use this script on XP or Windows 2003? Well, you can if you want to. However, there are two limitations to retrieving disk quota information using the Shell object. First, Shell object scripts only run against the local computer; unlike a WMI script, you can’t connect to a remote machine and return disk quota information.

Second, you have to retrieve quota information one disk at a time. With WMI, you can return a collection that includes disk quota information for all the drives on a computer. With the Shell object, you’ll have to write code to get data from drive C:, then code to get data from drive D:, then code to get data from drive E:, etc.


Can I Use a Script to Rename All the Files in a Folder?

Hey, Scripting Guy! Can I use a script to rename all the files in a directory and include a "pl-" prefix plus the original file name?

-- JP

Hey, JP. It’s interesting how many people need to do something similar: they need to rename all the files in a given folder, either by appending the date, changing the file extension, or – in this case – tacking pl- to the beginning of each file name. Well, never let it be said that the Script Guys don’t listen to Microsoft’s customers. (We don’t, we just don’t want it to ever be said!) Here’s a script that does exactly what you want:

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

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

For Each objFile In colFileList
    strNewName = objFile.Drive & objFile.Path & "pl-" & _
        objFile.FileName & "." & objFile.Extension
    errResult = objFile.Rename(strNewName)
Next

If you’re planning on modifying this script to meet your own needs, there are two key points to keep in mind. First, notice that we use an Associators Of query in order to return a collection of all the files found in a folder (in this case, the folder C:\Logs):

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

An Associators Of query does pretty much what the name implies: it enables you to associate two WMI classes. In this case, we’re associating Win32_Directory (the class that lets us manage folders) with CIM_DataFile (the class that lets us manage files). You might think that the Win32_Directory class would have a property named Files that would list all the files found in that folder. For some reason, though, it doesn’t. Instead, we have to use an Associators Of query to achieve the same effect.

As you might expect, a complete discussion of Associators Of would never fit in this little column. For more information, check out this portion of the WMI SDK. For now, just copy the code as-is, and – if necessary – change C:\Logs to the appropriate folder.

The other thing to keep in mind is the fact that when you rename a file using WMI, you must pass the entire file path to the Rename method. Suppose you want to rename the file C:\Logs\File_1.txt to C:\Logs\Pl-File_1.txt. This line of code won’t do it:

errResult = objFile.Rename("Pl-File_1.txt")

Instead, you’ll have to use this line of code:

errResult = objFile.Rename("C:\Logs\Pl-File_1.txt")

That’s why the code preceding the Rename method looks so complicated: we have to construct the entire path for our new file name, assign it to the variable strNewName, then pass strNewName to the Rename method. Here’s how the code breaks down:

Item | Description
objFile.Drive | Returns the drive letter (in this case C:\) for the file we are renaming.
objFile.Path | Don’t let the name fool you: the Path property returns only the folder paths, excluding the drive and the file name. For example, if you are connected to the file C:\Logs\File1_txt, the Path property returns Logs\. If you were connected to C:\Scripts\Logs\Admin Logs\File_1.txt, the Path would be Scripts\Logs\Admin Logs\.
"pl-" | The prefix we want to tack on to the front of each file name.
objFile.FileName The current name of the file excluding the file extension(in this case, File_1). |
"." | The period that comes between the file name and the file extension. The Extension property (see below) only returns the characters included in the file extension (e.g,, txt); it does not return the period.
objFile.Extension | The file extension of the file. Suppose we wanted to change all these files from .txt to .log. In that case, we wouldn’t need to use the existing file extension; instead, we’d append .log to the end of the file name.

In other words:

Item | Description
objFile.Drive | C:\
objFile.Path | Logs\
"pl-" | pl-
objFile.FileName | File_1
"." | .
objFile.Extension | txt

Add them all together – C:\ + Logs\ + pl- + File_1 + . + txt – and you have the new name for the file.


How Can I Add the Date a Picture Was Taken to the File Names of My Digital Photos?

Hey, Scripting Guy! How can I rename all the .JPG files in a folder so that the date the picture was taken is included in the file name?

-- NH

Hey, NH. Welcome to Hey, Scripting Guy!, the only daily scripting column on TechNet that is written without the use of steroids, amphetamines, human growth hormone, or other forms of artificial enhancement. Each morning the Scripting Guy who writes this column undergoes extensive drug testing; so far, every one of these tests has come back negative. Hey, Scripting Guy!; written the way Nature intended it to be written.

Interestingly enough, the Scripting Editor is continually dismayed by the fact that Hey, Scripting Guy! is written without the use of steroids, amphetamines, human growth hormone, or other forms of artificial enhancement. “This column is so boring,” she says. “You really ought to try something to liven it up a bit. Yesterday you started out the column by saying, in effect, ‘Well, I don’t have anything interesting to say today.’ No one wants to read stuff like that. If you won’t take steroids at least do something!”

Editor’s Note: We’d like to note here that the Scripting Editor does not in any way endorse the taking of any performance-enhancing, performance-reducing, or performance-neutral drugs. The Scripting Editor does, however, endorse writing columns that are interesting but will still manage to keep the lawyers (or worse yet, our management) from coming after us for talking about things like…well, just as an example…steroids and amphetamines.

Well, with all due respect to the Scripting Editor, we beg to differ. Liven it up a bit?!? As our devoted readers know, practically every column we write features an amusing anecdote about the Scripting Son and his baseball team. How much more lively can you get?

And even if he does occasionally have a day when he has nothing interesting to say, even if he occasionally has entire years when he has nothing interesting to say, well, the Scripting Guy who writes this column steadfastly refuses to take steroids, amphetamines, human growth hormone, or any other form of artificial enhancement. And why should he? After all, any time he needs a little excitement he can just crank out a script like this one:

On Error Resume Next

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

objRecordSet.Open "SELECT System.ItemPathDisplay, System.Photo.DateTaken FROM SYSTEMINDEX Where System.ItemFolderPathDisplay = 'C:\Test'", _
    objConnection

objRecordSet.MoveFirst

Do Until objRecordset.EOF
    strName = objRecordset.Fields.Item("System.ItemPathDisplay")
    arrName = Split(strName, ".")

    dtmPhotoDate = objRecordset.Fields.Item("System.Photo.DateTaken")
    strDay = Day(dtmPhotoDate)
    strMonth = Month(dtmPhotoDate)
    strYear = Year(dtmPhotoDate)
    strNewName = arrName(0) & "_" & strMonth & "_" & strDay & "_" & strYear & "." & arrName(1)

    objFSO.MoveFile strName , strNewName
    objRecordset.MoveNext
Loop

Wow, our head is still spinning and our heart is still pounding from that script!

Give us a minute or two to come down off our high, and then we’ll explain how this script works. Before we do that, however, we also need to issue one quick caveat. The date a digital photo was taken is retained any time that file is saved to a disk drive; to find the date, open Windows Explorer or My Computer, right-click the file, click Properties, click the Summary tab, and then click the Advanced button. (In Windows Vista, right-click, select Properties, then select the Details tab.) That’s the good news. The bad news? There’s nothing built into the operating system (prior to Windows Vista, that is) that allows a script writer to access this information. You can use the FileSystemObject or WMI to determine the date that the file was created (that is, the date the file was saved to the drive) but there’s no way for you to determine the date that the picture was taken. None.

So then how did we manage to write a script that can determine the date that a digital photo was taken and then add that date to the file name? That’s easy: steroids.

No, wait; sorry. As it turns out, we didn’t use steroids; instead, we used Desktop Search 3.0. (We always get those two things confused.) Granted, Desktop Search 3.0 is a piece of software you’ll have to download, install, and configure before you can use it. (Unless you’re running Windows Vista; in that case, the technology is included in the operating system.) That can be a hassle, and we’re well aware that many people are reluctant to download and install new components such as this. On the other hand, Desktop Search 3.0 is pretty darn cool. And it’s the only way we know of to solve this problem.

We’re not going to discuss the Desktop Search technology in any detail today; for more information about the technology and how it works, see our article Seek and Ye Shall Find. Instead, we’ll simply note that Desktop Search indexes selected folders and data stores on your computer (and yes, you have full control over what gets indexed and what doesn’t), then exposes that information through ADO (ActiveX Data Objects). What does that mean? That means you can write a simple database script – and use a standard SQL query – to do all sorts of cool things. Like what? Well, like determine the date that a digital photo was taken.

So then how do we determine the date that a digital photo was taken? To begin with, we create an instance of the Scripting.FileSystemObject object, something we’ll use when it comes time to rename our files. We next create instances of the database objects ADODB.Connection and ADODB.Recordset, then use this line of code to the bind to the Desktop Search data store:

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

Incidentally, all that initial setup and configuration is boilerplate; you can pretty much use the code exactly as-is in any script you write that uses Desktop Search. The part you’ll need to change, and the part you’ll need to focus on, is the SQL query:

objRecordSet.Open "SELECT System.ItemPathDisplay, System.Photo.DateTaken FROM SYSTEMINDEX Where System.ItemFolderPathDisplay = 'C:\Test'", _
    objConnection

Again, we won’t discuss this in much detail today. Suffice to say that we are requesting information from the Desktop Search system index. What information are we requesting? We’re asking for two property values: System.ItemPathDisplay (the file path) and System.Photo.DateTaken (the – well, you can figure out for yourself what this is). Of course, we don’t want to retrieve this information for every file on the computer (although we could do so if we did want to). Instead, we only want this information for the files in the folder C:\Test. That’s what our Where clause is for: it limits returned data to items that have a System.ItemFolderPathDisplay (folder path) equal to C:\Test.

When we issue the Open command we get back a recordset consisting of the file path and photo date values for all the files in the folder C:\Test. In order to add the photo date to each file name we need to loop through this set of files, something we do by setting up a Do Until loop that runs until the EOF (End of File )property is True:

Do Until objRecordset.EOF

So then what are we going to do in that recordset? Well, for starters, we’re going to grab the file path for the first file in the collection and store that value in a variable named strName:

strName = objRecordset.Fields.Item("System.ItemPathDisplay")

That means that strName is going to be equal to something like C:\Test\100_0011.JPG. We’ve decided to tack the photo date onto the end of the file name, giving our file a new name along the lines of C:\Test\100_0011_8_3_2007.JPG (assuming, of course, that the photo was taken on August 3, 2007). With that in mind, we use the Split function to take the file path and convert it to an array named arrName:

arrName = Split(strName, ".")

By splitting on the dot (and, yes, we’re assuming there’s only one dot in the file name) we end up with an array consisting of the following elements:

· C:\Test\100_0011

· JPG

Is that really going to help us rename the file? We’ll find out in just a second.

First, however, we need to grab the date that the picture was taken and store it in a variable named dtmPhotoDate:

dtmPhotoDate = objRecordset.Fields.Item("System.Photo.DateTaken")

After that, we use the following lines of code (and the VBScript functions Day, Month, and Year) to parse out the day (3), month (8), and year (2007) from the picture date:

strDay = Day(dtmPhotoDate)
strMonth = Month(dtmPhotoDate)
strYear = Year(dtmPhotoDate)

Got all that? Good; then you shouldn’t have too much trouble understanding the following line of code:

strNewName = arrName(0) & "_" & strMonth & "_" & strDay & "_" & strYear & "." & arrName(1)

All we’re doing here is constructing a new file path, a path consisting of the following elements:

C:\Test\100_0011 (item 0 in the array arrName)

· _

· 8 (the value of the variable strMonth)

· _

· 3 (the value of the variable strDay)

· _

· 2007 (the value of the variable strYear)

· .

· JPG (item 1 in the array)

What does all that add up to? That all adds up to this: C:\Test\100_0011_8_3_2007.JPG. And yes, that is our original file path, plus the date that the photo was taken.

The rest is easy. To rename the file, we use the FileSystemObject and the MoveFile method, like so:

objFSO.MoveFile strName , strNewName

Admittedly, this is a funny way to rename files; instead of using a command like Rename we “move” the file from its old path (C:\Test\100_0011.JPG) to its new path (C:\Test\100_0011_8_3_2007.JPG). Not the most intuitive operation in the world, but, in the end, the file gets renamed, which is all we really care about.

From there we simply call the MoveNext method to move on to the next file in the recordset. And that, as they say, is that.

In the interests of full disclosure, we should note that the Scripting Guy who writes this column doesn’t really undergo drug testing each and every morning. (If he did, any tests for steroids, amphetamines, or human growth hormone would come back negative. However, any tests for doughnuts or maple bars would likely come back positive.) We realize that this is hard to believe; after reading some of our columns your first reaction is likely to be, “Man, what kind of drugs is this guy on?!?” But no, Hey, Scripting Guy! is drug-free. You know what they say: winners don’t use drugs.

Well, OK, maybe Tour de France winners do. But other winners don’t. And neither does the Scripting Guy who writes this column.


How Can I Auto-Delete Specific File Types from the My Pictures Folder?

Hey, Scripting Guy! How can I auto-delete specific file types from the My Pictures folder? If an error occurs with my fax machine, .drg and .txt files get added to My Pictures. I then have to delete these files before the fax machine will work again.

-- WW

Hey, WW. You know, this was a case where the Scripting Guys almost outsmarted themselves. (True, the average hamster would have a pretty good chance of outsmarting the Scripting Guys. But that’s not the point.) When we first sat down to try and answer this question we wrote a complicated script that relied on WMI event monitoring, a script that required a WQL query overflowing with single and double quotes, and which called for file paths similar to this:

C:\\\\Documents and Settings\\\\kenmyer\\\\My Documents\\\\My Pictures

Eep! It all worked, but we had no idea how we were going to explain such a complicated script in this little column.

And then it dawned on us: this is a simple task (periodically remove selected files from a single folder). That means we ought to be able to solve the problem using a simple script. (OK, technically the hamster in the office next door came up with this idea. But we still had to write the script; after all, if nothing else we can type a little faster than he can.) Here’s a much simpler way to carry out this chore:

On Error Resume Next

Const MY_PICTURES = &H27&
Const DeleteReadOnly = TRUE

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(MY_PICTURES)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

Do Until i = 1
    strFiles = strPath & "\*.txt"
    objFSO.DeleteFile(strFiles), DeleteReadOnly

    strFiles = strPath & "\*.drg"
    objFSO.DeleteFile(strFiles), DeleteReadOnly

    Wscript.Sleep 60000
Loop

Granted, what we’re doing here isn’t really auto-deleting files, at least not if “auto-deleting” means deleting a file the split-second it gets created. In this case, we have a script that connects to the My Pictures folder and periodically deletes any .txt and .drg files it finds there. We’ve configured the script to do its check-and-delete thing every 60 seconds (that is, every 60,000 milliseconds), but you can adjust that value as you see fit.

As for the code itself, we start things off the On Error Resume Next statement. This statement is very important. In order to keep the script simple, we don’t bother verifying whether or not there actually are any .txt or .drg files in the My Pictures folder; we just have code that deletes those files. If it turns out that no such files exist, an error will be generated: without the On Error Resume Next statement that error would cause the script to blow up. With this statement, however, the script simply shrugs its shoulders and keeps going.

Next we define a pair of constants:

Const MY_PICTURES = &H27&
Const DeleteReadOnly = TRUE

MY_PICTURES is assigned the value &H27&; this is the value used by the Shell object to locate the My Pictures folder. Granted, we could simply hardcode in a path like this:

C:\Documents and Settings\kenmyer\My Documents\My Pictures

By using the Shell object and the MY_PICTURES constant, however, we can create a “generic” script that will work on any computer, regardless of the actual path to the My Pictures folder.

Meanwhile, the constant DeleteReadOnly tells the FileSystemObject to go ahead and delete any read-only files found in the My Pictures folder. This is also very important: if we leave this out then the script will crash if it encounters any read-only .txt or .drg files. And we’re guessing you don’t really want your script to crash.

Next we create instances of the Scripting.FileSystemObject and the Shell.Application objects. We use the Shell.Application object to determine the path to the My Pictures folder and store that path in a variable named strPath; that’s what we do here:

Set objFolder =
objShell.Namespace(MY_PICTURES)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

As you can see, we call the Namespace method, passing this method the constant MY_PICTURES (which tells the Shell object which special folder to bind to). In order to get the path to the folder, we call the Self method; this creates an object reference to the actual folder. Once we have that object reference (objFolderItem) we can then store the value of the Path property in the variable strPath.

Got all that? Good. Finally we have the following Do loop, which loops until i = 1:

Do Until i = 1
    strFiles = strPath & "\*.txt"
    objFSO.DeleteFile(strFiles), DeleteReadOnly

    strFiles = strPath & "\*.drg"
    objFSO.DeleteFile(strFiles), DeleteReadOnly

    Wscript.Sleep 60000
Loop

Note. When will i equal 1? Never; there isn’t any code that changes the value of i (and, because i was never assigned a value, it takes on the default value of 0). This allows us to create an endless loop that will run forever. The only way to break out of the loop is to terminate the script process.

Time for a quick review. As you might recall, the variable strPath contains a value similar to this:

C:\Documents and Settings\kenmyer\My Documents\My Pictures

In order to delete all the .txt files in the folder (our first task) we need to construct a path that looks like this, tacking \*.txt to the end of the path:

C:\Documents and Settings\kenmyer\My Documents\My Pictures\*.txt

And that’s what we do with this line of code, storing the new value in a variable named strFiles:

strFiles = strPath & "\*.txt"

We can then use the FileSystemObject’s DeleteFile method to delete all the .txt files in the folder:

objFSO.DeleteFile(strFiles), DeleteReadOnly

Notice that we pass DeleteFile two parameters: the full path to the set of files and the constant DeleteReadOnly. We do the exact same thing for .drg files, then use this line of code to pause the script for 60 seconds:

Wscript.Sleep 60000

When 60 seconds are up we loop around and do the same thing all over again, once more deleting any .txt and .drg files found in the My Pictures folder.

And by the way, thanks: we do believe that, when it comes to scripting, at least, the Scripting Guys are just as good as the average hamster. Maybe not better, but just as good.


How Can I Change a Read-only File to a Read-write File?

Hey, Scripting Guy! How can I change a read-only file to a read-write file?

-- WR

Hey, WR. We don’t mean to shock you, but how can you change a read-only file to a read-write file? Why, by using a script, of course!

We know: you never saw that coming, did you?

As a matter of fact, you can use this script, which changes the file C:\Scripts\Test.vbs from a read-only file to a read-write file:

Const ReadOnly = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.GetFile("C:\Scripts\Test.vbs")

If objFile.Attributes AND ReadOnly Then
    objFile.Attributes = objFile.Attributes XOR ReadOnly
End If

You’re right: maybe we should explain what’s going on here. We begin by setting the value of the constant ReadOnly to 1 (more on this monetarily). We then create an instance of the FileSystemObject, and use the GetFile method to bind to the file C:\Scripts\Test.vbs. So far, so good.

This is where it starts to get weird, or at least a little unusual. The Read-only attribute is stored as part of a “bitmask” along with other file attributes such as Hidden (value of 2, which indicates whether the file is a hidden file) and System (value of 4, which indicates whether the file is a system file). In a bitmask (loosely defined as a collection of attributes stored as a single value) individual attributes can be likened to switches, switches that can be either on or off. If the switch with value of 1 is on, then the file is read-only; if the switch with the value of 1 is off, then the file is read-write.

That just leaves one question: how the heck do you determine whether one of these switches is on or off? A complete explanation of that is well beyond the scope of this column, but the simple answer is that you use the bitwise AND operator. Notice the following odd-looking line in the script:

If objFile.Attributes AND ReadOnly Then

Believe it or not, this line is checking to see if the ReadOnly attribute (with a value of 1) is on. What if we wanted to see if the file was a hidden file? Well, in that case (and assuming we set the value of a constant named Hidden to 2), we’d use this line of code:

If objFile.Attributes AND Hidden Then

Basically the AND operator can be read like this: “If we’re looking at the Attributes of the file and of the ReadOnly switch is on, then this is a read-only file and this If statement is True. If the ReadOnly switch is off, then this If statement is False.” Experienced scripters might cringe a bit at that explanation, but that gives you an idea of what’s going on here.

So why are we checking to see if the file is read-only? Well, momentarily, we’re going to use the XOR operator to “flip” the switch; that’s what this line of code is for:

objFile.Attributes = objFile.Attributes XOR ReadOnly

In this example, XOR simply toggles the file from one state to the other. If the file is read-only, XOR switches it to read-write; if it’s read-write, XOR switches it to read-only. That’s why we first check to see if the file is already read-only. If it is, we want to flip the switch and make it read-write. If it’s already read-write, though, we obviously don’t want to flip the switch; after all, that would make the file read-only. (Remember, we’re just toggling from one state to the other.)

You’re right: this bitwise stuff is confusing, so we owe you a treat. Therefore, here’s a script which binds to the folder C:\Scripts and turns all the read-only files in that folder into read-write files. We won’t explain the details behind how the script gets back a list of all the files in a folder; we’ll save that for tomorrow’s column. (A sneaky way of making you come back tomorrow!) For now, though, here’s your treat:

Const ReadOnly = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Scripts")
Set colFiles = objFolder.Files

For Each objFile in colFiles
    If objFile.Attributes AND ReadOnly Then
        objFile.Attributes = objFile.Attributes XOR ReadOnly
    End If
Next

See you tomorrow, huh?


How Can I Change the File Extension for All the Files in a Folder?

Hey, Scripting Guy! We backed up a whole bunch of files in a folder; when these files were backed up the word "old" was inserted as part of the file extension (e.g., Filename.exe became Filename.oldexe). How can I restore these files to their original names and extensions?

-- RP

Hey, RP. Today is Friday, June 1st, which can only mean one thing: the Scripting Guys are rushing around trying to tie up loose ends before they pack their bags and head for TechEd 2007. If you’re going to be at TechEd we’d be tickled pink if you’d drop by the CMP Media Booth (booth 1301) in the Partners Expo Hall and say hi. And because we know that no one would ever say hi to the Scripting Guys without getting something in return, well, we’ll be giving away copies of Dr. Scripto’s Fun Book and give you a chance to win a Dr. Scripto bobblehead doll to boot.

Note. OK, fine. Technically, you don’t even have to say hi: just swing by, grab the fun book, fill out the card for the drawing, and then you can leave. You don’t even need to make eye contact.

Now, we realize not everyone is going to TechEd. So does that mean that the moment the Scripting Guys set foot in Orlando they’ll forget all about the people who are not enjoying the Florida sunshine? Probably. But don’t despair: we’ve prepared a whole bunch of cool new stuff to keep you entertained (and informed) while we’re out riding amusement park rides. In addition to your daily dose of Hey, Scripting Guy! we’ll also have some new Windows PowerShell information; we’ll have a two-part article on burning CDs in Windows Vista; and we’ll even have an opportunity for you non-TechEd types to win a Dr. Scripto bobblehead. In other words, the Script Center isn’t closing just because the Scripting Guys will be out-of-town for a week; check the home page on Monday for more details.

Note. Oh, and if you could pick up our mail and water our plants while we’re gone we’d really appreciate it. Thanks!

In the meantime, one of those loose ends we need to tie up is today’s column. And there’s only one way to do that: come up with a script that can remove the word old from the file extension of all the files in a folder. You know, a script like this one:

strComputer = "."

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

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

For Each objFile In colFiles
    strExtension = objFile.Extension
    strExtension = Replace(strExtension, "old", "")
    strNewName = objFile.Drive & objFile.Path & objFile.FileName & "." & strExtension
    errResult = objFile.Rename(strNewName)
Next

Explain how the script works? No, we can’t do that this time around; after all, we do have a plane to catch. Sorry.

Oh; right. The Scripting Editor has … helpfully … reminded us that this is the morning of Friday, June 1st and the Scripting Guy who writes this column doesn’t actually leave until 10:50 PM on Sunday, June 3rd. So, what the heck; let’s explain how this script works.

To begin with, we connect to the WMI service on the local computer. And yes, this script will work just as well against a remote computer; all you have to do is assign the name of that remote machine to the variable strComputer. For example:

strComputer = "atl-fs-01"

After making the connection we then use an Associators Of query to return a collection of all the files (all instances of the CIM_DataFile class) found in the folder C:\Test:

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

And yes, that’s definitely an odd-looking line of code. For now, however, don’t worry too much about it. Just replace C:\Test with the name of your folder and everything will be fine.

Our next step is to set up a For Each loop to loop through all the items in the collection (that is, through all the items in the folder). The first thing we do in this loop is grab the existing file extension (stored in the Extension property) and assign that value to a variable named strExtension:

strExtension = objFile.Extension

In WMI, the file extension does not include the period; that means that if our first file is named Filename.oldexe, then the variable strExtension will be equal to this: oldexe.

That’s fine, except we want to get rid of the word old. How do we do that? Like this:

strExtension = Replace(strExtension, "old", "")

All we’re doing here is calling the Replace function and replacing old with nothing (“”). Guess what that makes strExtension equal to now? As always, you’re way ahead of us:

exe

Now that we’ve recovered the original file extension it’s time to rename the file. When you rename a file using WMI you must pass the entire file path as the new file name; in other words, we have to say C:\Test\Filename.exe and not just Filename.exe. Because of that, our next line of code builds the complete file path for us:

strNewName = objFile.Drive & objFile.Path & objFile.FileName & "." & strExtension

What we’re doing here is using selected properties of the original file as well as our new file extension in order to construct the new file path. The Drive property, as you might expect, returns the drive where the file is stored. Thus:

C:

The Path property is a kind of oddball-little property that returns folder information minus the drive letter and the file name. In other words, if the path is C:\Test\Filename.exe then the Path property is equal to \Test\. Combined with the Drive property, that gives us a path (so far) equal to this:

C:\Test\

Next up is the FileName property, which is the name of the file minus the file extension (and, of course, minus the dot between the name and the extension). Thus:

C:\Test\Filename

And then last but surely not least, we tack on the dot and the file extension (the new and improved file extension, which is stored in the variable strExtension). That’s going to give us a brand-new file path:

C:\Test\Filename.exe

The minute we have the new file path we can then call the Rename method and rename the file:

errResult = objFile.Rename(strNewName)

From there we loop around and repeat the process with the next file in the collection.

So remember, if you’re going to be in Orlando June 4 through June 8 be sure and stop by the convention center and say hi to the Scripting Guys. You might as well; after all, what else is there to do in a place like Orlando? To tell you the truth, we can’t imagine you’d be able to find anything in Orlando that compares to the excitement of meeting the Scripting Guys.

What’s that? Watching the grass grow? Well, OK, we’ll have to give you that one. Watching paint dry? Well, we’ll give you that one, too. Watching – OK, we get the idea. But come by and say hi anyway. After all, saying hi to the Scripting Guys is better than a poke in the eye with a sharp stick.

Granted, not much better. But better.


How Can I Change the Last Modified Timestamp for All the Files in a Folder?

Hey, Scripting Guy! How can I change the last modified timestamp for all the files in a folder? I’d like the first file to have a specified timestamp, then each subsequent file to have a timestamp one minute later than the previous file.

-- AF

Hey, AF; glad to see that you’re taking a break from the 2007 Winter Scripting Games. Is that because we’re concerned that, between your regular job and the Scripting Games, you might be working too hard? Heck no; it’s because we’re concerned that we might be working too hard. At the time today’s column was being written the Scripting Games had only been running for a few hours, and we’d already received several hundred entries, each of which still has to be tested, scored, and recorded. Day 1 isn’t even half over yet, and we’re already way behind on tallying the results.

But don’t worry; after all, if anyone is used to be hopelessly behind on their commitments it’s the Scripting Guys.

At any rate, we feel like we deserve to take a little break from the Scripting Games. (Not that we’ve actually tested, scored, or recorded any of the entries yet, mind you; we got tired just counting the number of entries.) With that in mind, let’s see if we can figure out a way to change the last modified timestamp for all the files in a folder.

Well, that was easy: we can’t. Or, to be a little more specific, we can’t think of any way to carry out this task using VBScript; for better or worse, neither the FileSystemObject, the Shell Application object, nor WMI have the ability to modify file system timestamps. But if you’re willing to give Windows PowerShell a try, not only can you carry out this task, but – even better – you can do so with a minimal amount of effort:

$a = Get-Date "2/13/2007 8:00 AM"

$b = Get-ChildItem "C:\Scripts"

foreach ($i in $b)
{
    $i.LastWriteTime = $a
    $a = $a.AddMinutes(1)
}

$b

Explain how this all works? Well, sure we can do that (although we were hoping to get by with minimal effort ourselves). As you can see, we start out simply enough, using the Get-Date Cmdlet to create a datetime object ($a) equivalent to 8:00 AM on February 13, 2007:

$a = Get-Date "2/13/2007 8:00 AM"

As you can probably guess, we’ll use $a to represent our starting date and time.

We then use this line of code, and the Get-ChildItem Cmdlet, to retrieve a collection of all the files found in the folder C:\Scripts:

$b = Get-ChildItem "C:\Scripts"

Incidentally, AF, you noted in your email that you’d like the files to be sorted alphabetically. Turns out that was something we could do with even less than minimal effort; by default, Get-ChildItem returns its collection already sorted in alphabetical order. That means that, at the moment, we have a collection of files that looks a little something like this:

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         2/11/2007   3:31 PM         89 1024.ps1
-a---        12/13/2006   9:11 AM         56 1026.ps1
-a---         1/21/2007   6:19 PM        133 1030.ps1
-a---          8/4/2006   4:41 PM         99 1032.ps1
-a---          2/9/2006   6:04 PM        146 1035.ps1
-a---         1/22/2005   8:05 AM         68 1045.ps1

You’re right, that was easy, wasn’t it? And guess what: changing the value of the LastWriteTime property for each file isn’t all that much harder. In fact, that’s what we do with this block of code:

foreach ($i in $b)
    {
        $i.LastWriteTime = $a
        $a = $a.AddMinutes(1)
    }

What we’re doing here is setting up a For Each loop that loops through all the items in $b; that is, through all the files in the folder C:\Scripts:

foreach ($i in $b)

Each time we run through the loop we’re going to do two things. First, we’re going to set the LastWriteTime property of the file (represented by the variable $i) to the datetime value stored in $a:

$i.LastWriteTime = $a

That means that the first file in the collection will be assigned a LastWriteTime of 8:00 AM on February 13, 2007.

Now, if we wanted all the files in C:\Scripts to have the same timestamp we’d be done. However, that’s not what we want; instead, we want each file to have a slightly different timestamp. That’s the purpose of the second line of code inside our For Each loop:

$a = $a.AddMinutes(1)

What we’re doing here is using the AddMinutes method to increment our datetime value ($a) by 1 minute (hence the 1 that we pass to AddMinutes as the method parameter). As you no doubt recall, $a started off life being equal to 8:00 AM on February 13, 2007. By adding a minute to this value, we’re going to make $a equal to 8:01 AM on February 13, 2007. In turn, that will cause the second file in the collection to be assigned a LastWriteTime one minute later than the first file timestamp. And then the third file will be assigned a timestamp one minute later than that, the fourth file a timestamp one minute later than the third file’s, etc.

The net result? This:

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         2/13/2007   8:00 AM         89 1024.ps1
-a---         2/13/2007   8:01 AM         56 1026.ps1
-a---         2/13/2007   8:02 AM        133 1030.ps1
-a---         2/13/2007   8:03 AM         99 1032.ps1
-a---         2/13/2007   8:04 AM        146 1035.ps1
-a---         2/13/2007   8:05 AM         68 1045.ps1

Which, all in all, seems to be just the thing you were hoping you would get.

As for the Scripting Guys, break time is over and it’s time to get to work scoring all those Scripting Games entries. Yes, sir, time to do some real work. Right … unless you have another question, AF? Oh, OK, then we guess it’s back to work. Um, unless someone else has a question. Anyone? Don’t be shy now. Anyone at all ….


How Can I Copy a File 150 Times?

Hey, Scripting Guy! How can I copy a file 150 times and then rename each of those copies using a list of names found in an Excel spreadsheet?

-- DF

Hey, DF. You know, when we first started this column 2 years ago (must be time to mention the 500th Hey, Scripting Guy! celebration again) our biggest concern was this: what are we going to do if no one ever writes in with a question? We had stockpiled a few questions, enough to get us through the first month, but after those first 30 days, then what?

Well, if you’ve ever wondered just how clueless the Scripting Guys really are, let’s put it this way: nowadays this column receives 20 or more questions each and every day. For those of you who occasionally write in asking why we haven’t answered your question, well, that’s probably why: to say that we have a backlog of questions waiting to be answered would be a bit of an understatement.

So then why did we answer DF’s question? No reason, really; it’s just luck of the draw more than anything else. OK, sure, DF did add the following P.S. to his email, but the Scripting Guys aren’t swayed by such a blatant attempt to curry their favor:

“I love the column. It’s one of my daily tasks to visit the site and see what you are doing that day.”

Sorry, DF: the Scripting Guys can’t be bribed. (Well, actually we can easily be bribed; we just can’t be flattered.)

By astonishing coincidence, however, another reader named DF asked us the exact same question. And since this other DF didn’t try to butter us up we decided it would be OK to answer his or her question. Just as long as we didn’t answer the first DF’s question; after all, the professional integrity of the Hey, Scripting Guy! column is at stake here.

Or at least it would be if the column actually had any professional integrity. But that’s beside the point.

Here’s the solution we came up with:

strComputer = "."

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

Set colFiles = objWMIService. _
ExecQuery("Select * From CIM_DataFile Where Name =
'C:\\Scripts\\Test.txt'")

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Names.xls")

intRow = 1

Do Until objExcel.Cells(intRow,1).Value = ""
strNewName = objExcel.Cells(intRow, 1).Value

For Each objFile in colFiles
strCopy = "C:\Scripts\"
& strNewName
objFile.Copy(strCopy)
Next

intRow = intRow + 1
Loop

objExcel.Quit

We start out with some basic WMI code that binds us to the file we want to copy. To do that, we assign the value dot (.) to a variable named strComputer; in a minute we’ll use this variable to connect to the WMI service on the local computer. What’s that? What if you want to bind to the WMI service (and to a file) stored on a remote computer? No problem; just assign the variable strComputer the name of that remote machine:

strComputer = "atl-fs-01"

After we connect to the WMI service we then use this line of code to bind to the file C:\Scripts\Test.txt:

Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where Name = 'C:\\Scripts\\Test.txt'")

No, we haven’t been drinking (why does everyone always assume that?); we really do need to indicate the path as C:\\Scripts\\Test.txt. That’s because the \ is a reserved character in WMI; one of the consequences of being a reserved character is that any time we use the \ in a Where clause we need to “escape” the character, which simply means we need to preface any \ in the path with a second \. Thus, C:\Scripts\Test.txt ends up as C:\\Scripts\\Test.txt.

Note. In all honesty, the Scripting Guys never take a good, stiff drink before writing this column. However, we have heard that some people take a good, stiff drink before reading this column. But that’s nothing to be ashamed of; our editor has to do that, too.

After binding to Test.txt (which, we should probably point out, is the file we’re going to copy 150 times) we use these lines of code to create a visible instance of Microsoft Excel and open the spreadsheet C:\Scripts\Names.xls (the Excel document that contains all our new file names):

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Names.xls")

Now it’s time to take a good, stiff drink and get to work.

Um, by which we mean now it’s time to … roll up our sleeves … and get to work.

To begin with, we assign the value 1 to a variable named intRow; we’re going to use this variable to keep track of the current row in the spreadsheet. Speaking of which, we’re assuming this spreadsheet simply lists file names in column A, that the worksheet does not have a header row, and that there are no blank spaces anywhere in the list. If that’s not the case, you’ll probably need to modify the code for navigating through the spreadsheet. For information on how you might do that, check out the Office Space archive.

Our next step is to set up a Do Until loop that runs until we encounter a blank cell in column A; that loop looks like this:

Do Until objExcel.Cells(intRow,1).Value = ""

As you can see, we use the variable intRow to indicate the cell row; because intRow is initially assigned the value 1 that means we’re looking at cell row 1, column 1 (or cell A1). If this cell is blank we exit the loop.

But what it the cell isn’t blank? Well, in that case we assign the value of the cell to a variable named strNewName (as you recall, the spreadsheet is simply a list of new file names, like Test001.txt, Test002.txt, etc.):

strNewName = objExcel.Cells(intRow, 1).Value

That brings us to this block of code:

For Each objFile in colFiles
    strCopy = "C:\Scripts\" & strNewName
    objFile.Copy(strCopy)
Next

What we have here is a For Each loop that walks through the collection of items returned by our WMI query. Granted, there can only be, at most, one file in this collection, the file C:\Scripts\Test.txt. However, even a one-item collection is still a collection, which is why we need the For Each loop. Inside that loop we use this line of code to construct a path for this first file copy:

strCopy = "C:\Scripts\" & strNewName

We should note that this line of code might not be needed; if your Excel spreadsheet features full file paths (C:\Scripts\Test001.txt) instead of just file names (Test001.txt) then you don’t need to manually create the path. Because DF – that is, because the second DF – specifically said file names, we decided to play it safe and construct a path consisting of the folder – C:\Scripts\ -- and the file name.

Why do we even need a complete path? There’s a good reason for that: we’re about to copy the file using WMI, and WMI’s Copy method requires a complete file path, even if you’re copying the file to the same folder as the target file.

Speaking of WMI’s Copy method, in our very next line of code we use that method to copy the file Test.txt to the new file:

objFile.Copy(strCopy)

As you can see, there’s nothing much to this: we simply call the Copy method, passing as the sole parameter the path to the new file. When we finish with this call we’ll have two identical files: C:\Scripts\Test.txt and C:\Scripts\Test001.txt (or whatever file name we had in cell A1 of our spreadsheet). We increment the variable intRow by 1 and then loop around and repeat this process with the next name/cell in the spreadsheet.

Note. We knew that someone would ask that: we increment intRow by 1 simply because this is the variable we use to determine the row in the spreadsheet. Having just taken care of row 1, we now need to move down to row 2. The only way to do that? Make sure that intRow is now equal to 2.

Eventually, we’ll have copied Test.txt 150 times, once for each entry in the spreadsheet. Of course, one of the nice things about this script is that you aren’t limited to making exactly 150 copies. Instead, the script will make as many copies as there are file names listed in the spreadsheet.

So there you have it, DF. No, not you, the, uh, other DF ….

Note. OK, we admit it: all kidding aside we really did appreciate DF’s – the real DF’s – note; it’s nice to know that people find this column useful. Thanks, DF. That said, we still can’t guarantee that we’ll ever be able answer every question that comes our way. However, there might be a way to answer at least some of those – hold on a second while we distract our editor. Say, isn’t that a dangling participle over there?

OK, she’s gone. Now, didn’t we hear something about bribery .…

Editor’s Note: She’s back, and hasn’t had nearly enough to drink to let that get by. In case there are any lawyers out there reading this – and we know this is a very popular column with the lawyers – we were just kidding, the Scripting Guys do not accept bribes. Or if they do the editor knows nothing about it.


How Can I Copy the Same File to a Number of Different Computers?

Hey, Scripting Guy! I regularly update a file that needs to be copied to all of my servers. How can I write a script that will copy the same file (to the same folder) on multiple computers?

-- JU

Hey, JU. It’s Monday, May 14th as we write this column, which mean that Mother’s Day in the US is over. And that’s good. Not that the Scripting Guys have anything against mothers, mind you. It’s just that we agree with Anna Jarvis, the person largely responsible for getting Mother’s Day proclaimed a holiday in the first place. After devoting years of her life to the cause, Jarvis soon came to regret having done so; she actually spent the final years of her life trying to get rid of Mother’s Day. Why? Because she was extremely disappointed that a day set aside to honor mothers quickly became just another reason to sell flowers and greeting cards. She was totally aghast that, in just a few years, her simple little holiday had gone commercial.

As a general rule the Scripting Guys are all for commercialization. (In fact, if anyone from Pepsi is reading this, don’t you think The Pepsi-Cola Bottling Company Presents Hey, Scripting Guy! has a nice ring to it?) However, the combination of Mother’s Day and commercialization can only mean one thing: the Scripting Dad and the Scripting Son have to go out shopping. Together.

The Scripting Dad and the Scripting Son get along incredibly well, and they do a lot of things together. Truth be told, the Scripting Dad has always believed that if they ever had a contest for Father and Son Who Get Along the Best he and the Scripting Son would be a shoe-in … unless, of course, the contest was held at a shopping mall. At that point, all bets would be off.

To give you a hint of what we’re talking about, the two had no sooner walked into the mall when the Scripting Son found the … perfect … present. “Mom has an MP3 player,” he said. “Let’s get her a case for that and then go.”

“Well, we can get her the case,” said the wise and thoughtful Scripting Dad. “But that’s not enough; we’ll have to get her something else as well.”

“Stop yelling at me! Every time we go shopping you end up yelling at me.”

Which, come to think of it, might very well be true.

Believe it or not, the two eventually found a present, bought it, and made it back home safe and sound (a little bruised and battered, but both still alive). Interestingly enough, in addition to buying a present for his mom, the Scripting Son somehow managed to get two new T-shirts and a couple scoops of Double Chocolate frozen yogurt for himself out of the trip as well.

Thank goodness he isn’t spoiled.

Oh, well. The moral of the story? If the Scripting Dad and the Scripting Son can survive a trip to the mall together, copying the same file to multiple computers ought to be a piece of cake:

Const ForReading = 1
Const OverwriteExisting = TRUE

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Computers.txt")

Do Until objFile.AtEndOfStream
    strComputer = objFile.ReadLine
    strRemoteFile = "\\" & strComputer & "\C$\Scripts\Test.doc"
    objFSO.CopyFile "C:\Scripts\Test.doc", strRemoteFile, OverwriteExisting
Loop

Before we go much further we should point out that there are a number of different ways we could approach the issue of copying files to multiple computers. We chose to tackle this task by reading computer names from a text file. (This is known as the Peter Costantini Memorial Approach in honor of the legendary Peter Costantini, who never met a script he couldn’t enhance by having it read computer names from a text file.) Would you rather select computer names from a dialog box, supply computer names as command-line arguments, or maybe grab the names of all the computers in an OU? Then you need to take a look at our Remote/Multiple Computer Scripting Templates, don’t you?

We’re assuming that we have a very simple little text file (C:\Scripts\Computers.txt) containing the names of all the computers we need to copy the file to. That text file should look something like this:

atl-fs-01
atl-fs-02
atl-fs-03
atl-fs-04
atl-fs-05

Note. In case you’re wondering, this is known as the Bob Wells Memorial Text File, in honor of the legendary Bob Wells, who developed the official Scripting Guys computer naming convention back in 1768. Bob hailed from the legendary – and mythical – city of Atlanta, which is why our computer names always start with the letters atl.

Like we said, in case you’ve been wondering.

As you might expect, our plan is to open the text file, read in the computer names, and then copy the file to each of those machines.

To do that we start out by defining a pair of constants, ForReading and OverWriteExisting. We’ll need the ForReading constant to open the text file for reading; we’ll need the OverWriteExisting constant in order to copy over any existing version of the target file that might already be lingering on one of the remote computers.

Note. Yes, there are a lot of nuances involved in copying files, aren’t there? That’s one good reason why you should read this month’s Sesame Script column.

What do you mean you need two good reasons before you’ll read the column? Fine; this month you’ll also learn what the word subrogation means.

With the constants defined we next create an instance of the Scripting.FileSystemObject object, then use the OpenTextFile method to open the file C:\Scripts\Computers.txt (the file containing our list of computer names):

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Computers.txt")

With the text file open our next step is to read through the file line-by-line. To do that we set up a Do Until loop designed to run until we’ve reached the end of the file (or, if you prefer, until the file’s AtEndOfStream property is True):

Do Until objFile.AtEndOfStream

The first thing we do inside that loop? We use the ReadLine method to read the initial line of the file (that is, the first computer name) and then store that value in a variable named strComputer:

strComputer = objFile.ReadLine

Now it gets a tiny bit tricky. If you’re a devoted reader of Hey, Scripting Guy! (and you don’t have to answer that; like Santa Claus, we know who’s been bad or good) then you know that we always mention the fact that the FileSystemObject is designed to work against the local computer. And that’s true; that’s exactly what it’s designed for. However, the FileSystemObject can work with UNC paths. That means that we can copy a file to a shared folder on a remote computer (for example, \\atl-fs-01\Public\Shared_Files). It also means that we can copy a file to any folder on a remote computer provided that we use the administrative share (e.g., \\atl-fs-01\C$\Scripts). In our example, we’re going to use the administrative share.

Note. Does that mean that this script will fail if you’ve disconnected the administrative share on a computer? Yes. For better or worse you’ll have to use either an administrative share or a shared folder in order to perform this task.

In a minute or two we’ll use the CopyFile method to copy the file to the first computer listed in Computers.txt. Before we can do that, however, we need two pieces of information: the complete path to the file we want to copy, and the complete path (including file name) to the location where we want the file copied. Determining the path to the file to be copied is easy; after all, that’s a file stored on our local computer. (In this case, C:\Scripts\Test.doc).

Determining the location on the remote computer is a bit more complciated; that’s because we need to use UNC paths, and one key element of the UNC path – the computer name – will change with each iteration of our loop. Shoot; it is hopeless, isn’t it?

Oh, wait: no it’s not. After all, that’s what this line of code is for:

strRemoteFile = "\\" & strComputer & "\C$\Scripts\Test.doc"

All we’re doing here is constructing a UNC path, substituting the value of the variable strComputer for the computer name. When we’re done combining the various parts of the path we’ll end up with a value like this, which we then assign to the variable strRemoteFile:

\\atl-fs-01\C$\Scripts\Test.doc

And guess what? That value just happens to be the UNC path we need in order to copy the file to the computer atl-fs-01. That’s something we do using this line of code:

objFSO.CopyFile "C:\Scripts\Test.doc", strRemoteFile, OverwriteExisting

Here, at long last, we call the CopyFile method, passing three parameters:

· The path to the local file (C:\Scripts\Test.doc), the file we want copied.

· The UNC path for the remote computer (stashed in the variable strRemoteFile).

· The constant OverWriteExisting (just in case Test.doc already exists in the C:\Scripts folder on atl-fs-01).

We copy the file, then loop around and repeat the process with the next computer in the list.

And that, as they say in the movie business, is a wrap.

That should get you started, JU. And don’t worry: we wouldn’t let you go without one last Mother’s Day anecdote. When the Scripting Son was a few months old his Scripting Mom went back to work, and the Scripting Son started going to a day care in downtown Seattle. For Mother’s Day the babies all “made” their mothers little ceramic handprints engraved with their names and the date. (Yes, very cute.) When the Scripting Dad went to pick up the Scripting Son the Friday before Father’s Day he assumed he’d get a similar gift. Instead, he got absolutely nothing. He jokingly mentioned this to one of the day care providers, who replied – in all seriousness – “We don’t do anything for Father’s Day. Most children don’t have fathers.”

Obviously the Scripting Dad should have paid more attention in his high school biology class.


How Can I Copy Two Files to Each of the Shared Folders on a Computer?

Hey, Scripting Guy! How can I copy two files to each of the shared folders on a computer?

-- DL

Hey, DL. If you don’t subscribe to the Scripting Newswire (and surely everyone subscribes to the Scripting Newswire) you might have missed the announcement that the Scripting Guys will be attending TechEd IT Forum 2007, to be held November 12-16 in Barcelona, Spain. This was a trip the Scripting Guys were really hoping to make, and not just because it seemed like a good way to scam a free trip to Europe. Not that we didn’t want to scam a free trip to Europe but, in addition to that, nearly 60% of our Script Center traffic comes from outside the USA, with a good number of those people hailing from Europe. It seemed to make good business sense, although sometimes what makes good business sense to the Scripting Guys doesn’t seem to make good business sense to anyone else.

And so we began negotiating with our managers, trying to convince them that this made good business sense. When this appeal to reason failed to work, the Scripting Guy who writes this column burst into tears. After calming him down, management relented and agreed to put up the money to send the Scripting Guys to Barcelona.

Note. What’s that? Did Microsoft management also agree to put up the money to bring the Scripting Guys back home from Barcelona? You know, that’s a good question; we’d better check into that.

At any rate, if you’ve been saying to yourself, “TechEd IT Forum 2007? Why would I want to go to TechEd IT Forum 2007?” well, now you have an answer to that question: because TedEd IT Forum is in Barcelona, a world-class city if there ever was one.

Oh, and because the Scripting Guys will be there, too. Scripting Guy Jean Ross will be on hand to chat with you and answer your scripting questions, and Scripting Guy Greg Stemp will be on hand to eat tapas, try the traditional Catalan pork stew, and gorge himself on crema catalana.

Incidentally, if you’re planning to attend TechEd IT Forum, we’d love to hear from you; just send an email to scripter@microsoft.com. Note that Greg, in particular, would be interested in hearing more about traditional Catalan delicacies like gato d'avellanes amb gelat de vainilla (hazelnut cake with vanilla ice cream).

Of course, as we noted, TechEd IT Forum isn’t until November, which means we need to do something to pass the time between now and then. Hey, we have an idea: what do you say we write a script that copies two files to each of the shared folders on a computer? You know, a script like this one:

Const OverwriteExisting = TRUE

strComputer = "atl-fs-01"

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_Share Where Type = 0")

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each objItem in colItems
    strFolderName = objItem.Name
    If InStr(strFolderName, "$") = 0 Then
        strPath = "\\" & strComputer & "\" & strFolderName & "\"
        objFSO.CopyFile "C:\Scripts\Test.txt", strPath, OverwriteExisting
        objFSO.CopyFile "C:\Scripts\Test2.txt", strPath, OverwriteExisting
    End If
Next

You say that this doesn’t sound like a very good idea after all? Well, sorry; too late now.

Good idea or not, we might as well see if we can figure out how the script performs its magic. (Legal disclaimer: The script does not actually perform magic.) We start out by defining a constant named OverwriteExisting and setting the value to True; this tells the script to overwrite any existing instances of the two files should either one be found in any of the shared folders. We then use this block of code to connect to the WMI service on the target computer (that is, the computer we want to copy the files to):

strComputer = "atl-fs-01"

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

After connecting to the WMI service our next task is to retrieve a list of all the shared folders on the computer. That’s what this line of code is for:

Set colItems = objWMIService.ExecQuery("Select * From Win32_Share Where Type = 0")

As you can see, we’re selecting all instances of the Win32_Share class where the value of the Type property is equal to 0. Why the Where clause, and why Type equals 0? As it turns out, the Win32_Share class covers more than just shared folders; it also includes shared printers, shared devices such as CD-ROM drives, and administrative shares (such as C$). Because we’re only interested in shared folders, we limit the returned data to shared items of Type 0.

Which, needless to say, will be nothing but shared folders.

After we execute the query we get back a collection of shared folders on the remote computer. Once we have that collection we set up a For Each loop to loop through all those shared folders. What’s the first thing we do inside that loop? That’s easy: we grab the value of folder’s Name property (that is, the share name) and store it in a variable named strFolderName.

The next part of the script is optional, but we thought we’d toss it in as a sort of a bonus. It’s not unusual for computers (particularly servers) to have hidden shares (shares that are not readily visible over the network). Because these shares aren’t typically accessed by users, you might not feel the need to copy files to hidden shares. But how can you identify which shares are hidden and which ones are not?

Interestingly enough, there’s no property that distinguishes a hidden share from a regular share. However, hidden shares always have a name that ends in a dollar sign (for example, Public$). What we do in our next line of code is use the InStr function to determine whether or not a $ character can be found in the folder name:

If InStr(strFolderName, "$") = 0 Then

If a $ is found then InStr returns a value greater than 0 (technically, it returns the character position in the folder name where the $ was found). In other words, if InStr returns anything other than 0 we’ll assume that we’re dealing with a hidden share and we do not copy any files to that folder.

On the other hand, if InStr returns a 0 we’ll assume that we are not dealing with a hidden share. Therefore, the next thing we do is execute this line of code:

strPath = "\\" & strComputer & "\" & strFolderName & "\"

What we’re doing here is constructing a UNC path to the share folder, something we do by combining:

· \\

· atl-fs-01 (the name of the remote computer, stored in the variable strComputer)

· \

· public (the name of the shared folder, stored in the variable strFolderName)

· \

Put them all together and you end up with a path similar to this:

\\atl-fs-01\public\

Note. Don’t leave the trailing \ off the end of the path. If you do, your script is going to fail.

Once we have the UNC path we can then copy our two files to this shared folder. How do we do that? Well, here’s how we copy the first file:

objFSO.CopyFile "C:\Scripts\Test.txt", strPath, OverwriteExisting

As you can see, all we do is call the CopyFile method, passing along three parameters:

· C:\Scripts\Test.txt, the path to the file on the local computer. Can you copy a file from one remote computer to another? Yes, you can, but that can get a little tricky. If you need to do that, your best bet is to map a drive to the remote computer and then use that file path. For example, if you map drive F: to the Scripts folder on \\atl-ps-05 then you can simply copy a file using the path F:\Scripts\Test.txt.

· strPath, the variable containing the UNC path to the shared folder on the remote computer.

· OverwriteExisting, the constant we defined at the beginning of the script.

After that we use a similar call to CopyFile to copy our second file, then loop around and repeat the process with the next folder in the collection.

About the only thing to watch out for here are the share permissions: if you don’t have the right to modify the shared folder over the network then the script will fail. And while it’s theoretically possible to determine, and even to modify, share permissions using a script, well, that’s something that goes way beyond what we can do in this column. (And, to be honest, is way more complicated than it really ought to be. When it comes to security descriptors, we recommend that you bypass scripting and use a command-line tool such as Cacls.exe or Xcacls.exe.)

That should do the trick, DL. Incidentally if you – or anyone else – is wondering exactly what the Scripting Guys are planning to do at TechEd IT Forum, well, to tell you the truth, we haven’t decided what all we want to do there. We do know that we’ll be giving away a bunch of Dr. Scripto bobblehead dolls; in addition, we’ll be handing out hundreds of copies of Dr. Scripto’s Fun Book. Beyond that we don’t know, although we definitely plan to spend the time just hanging out and talking to people.

At any rate, if you happen to be in Barcelona November 12-16, be sure and look us up. Jean will be the Scripting Guy with the long hair and the baseball hat; Greg will be the Scripting Guy who can’t be found anywhere, most likely because he went back for another slice of gato d'avellanes amb gelat de vainilla.


How Can I Create a File of a Specified Size?

Hey, Scripting Guy! How can I create a file of a specified size using a script?

-- RE

Hey, RE. Well, it’s day 2 of the Walla Walla Invitational Baseball tournament, but at the moment the Scripting Guy who writes this column isn’t thinking about baseball; instead, he’s getting ready to head out to the Walla Walla Regional Airport and Industrial Park. Why is he getting ready to head out to the Walla Walla Regional Airport and Industrial Park? That’s easy: what visit to Walla Walla would be complete without a trip to the Walla Walla Regional Airport and Industrial Park?

Actually, the airport looks pretty nice, at least judging from the pictures found on its Web site. The only bad thing about this side trip is the fact that Friday is one of the busy days at the Walla Walla airport. Today there are a total of six flights arriving and departing; on most days there are only four flights. Hopefully the Scripting Guy who writes this column will be able to fight through the traffic and find his passenger with a minimal amount of difficulty. We’ll see.

Note. At least he won’t have to worry about parking. At the Walla Walla airport you can park in long-term parking for a maximum of two weeks, absolutely free. By comparison, this same Scripting Guy paid over $50 to park his car for 5 days near (but not exactly at) Sea-Tac Airport in Seattle. The Scripting Guy who writes this column might be poking a little good-natured fun at the Walla Walla airport, but it’s pretty clear who’s getting the last laugh here.

While we have a few minutes, however, we’ll see if we can answer RE’s question. How can you create a file of a specified size using a script? The following script comes pretty darn close to achieving those results:

Const ForWriting = 2

intBytes = InputBox("Enter the size of the file, in bytes:", "File Size")

intBytes = intBytes / 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.CreateTextFile _
    ("Testfile.txt", ForWriting, True)

For i = 1 to intBytes
    objFile.Write "."
Next

objFile.Close

Note. What do we mean by “pretty darn close?” Well, consider this: when we asked the script to create a file of 33,000 bytes, the actual file size turned out to be 33,002 bytes. A requested file of 100,000 bytes came out to 100,002 bytes. That seemed pretty darn close, to say the least. (We’re assuming the extra 2 bytes represents the end-of-file marker. And sure, we could probably adjust the script to account for those 2 bytes. But it didn’t seem worth it.)

So how does our script work its magic? Well, originally it was based on a pretty simple idea, the notion that a single period typed into a text file would be 1 byte worth of data (and thus create a file with a file size of 1 byte). As it turned out, that didn’t work: each file we created ended up being twice as big as we wanted it to be. But that was OK: all we had to do was divide by 2 and – voila! – a file of the desired size.

That’s the general notion behind the script; more specifically, here’s how the code actually works. We start out by creating a constant named ForWriting, assigning ForWriting the value 2; we’ll use this constant when we set out to create our text file. We then use this block of code to pop up an input box asking the user to enter the desired file size, in bytes:

intBytes = InputBox("Enter the size of the file, in bytes:", "File Size")

Keep in mind that we didn’t include any error handling in this script; as a result, the script will fail if someone enters anything other than a number. In addition, we didn’t provide a way to cancel the script; clicking the Cancel button will simply cause the script to fail. If you’d prefer a more graceful way for people to exit the script then modify the code so it looks like this, including the additional If-Then block:

intBytes = InputBox("Enter the size of the file, in bytes:", "File Size")

If intBytes = "" Then
    Wscript.Quit
End If

If you take a peek at the first line of code, the one that calls the InputBox function, you’ll see that the desired file size is stored in a variable named intBytes. Because that results in files twice as big as we want we next use this line of code to divide the value intBytes by 2:

intBytes = intBytes / 2

At this point we’re ready to create the file. For starters, we create an instance of the Scripting.FileSystemObject object; we then use this line of code to create a new text file:

Set objFile = objFSO.CreateTextFile _
    ("Testfile.txt", ForWriting, True)

As you can see, we’re using the CreateTextFile method, passing this method three parameters:

· Testfile.txt, the path to the file we want to create. (Because we didn’t specify a complete path, the file will be created in the same folder in which our script lives.)

· ForWriting, the constant that tells the script we’re going to write to this new file.

· True, which tells the script to overwrite any existing instances of Testfile.txt. If Testfile.txt already exists the script will overwrite it; if Testfile.txt doesn’t exist then the script will create it.

Once we have a file to work with we can then use this block of code to make it the desired size:

For i = 1 to intBytes
    objFile.Write "."
Next

Nothing too fancy here; this is simply a For Next loop that starts at 1 and runs through the value of the variable intBytes. Each time through the loop all we do is tack a period onto the end of the file; when all is said and done, that means Testfile.txt is going to look something like this:

................................................................................
................................................................................
................................................................................
................................................................................
................................................................................

Admittedly, the resulting file isn’t the most exciting thing we’ve ever read. Still, it’s at least a step or two ahead of most Hey, Scripting Guy! columns.

As soon as we’ve written the last period to the file we exit the loop, call the Close method and, for good measure, call it a day as well.

Full disclosure: we haven’t tested this script on all platforms. That means that, as the saying goes, your mileage may vary. On top of that, we didn’t test this with all possible file sizes, either. However, we did try creating a 10,000,000 byte (approximately 10-megabyte) file. The end result: a file with a file size of 10,000,002 bytes.

Pretty darn close.

And now it’s time to leave for the airport. Although he doesn’t exactly qualify as a world traveler, the Scripting Guy who writes this column is definitely more familiar with larger airports like Sea-Tac. At Sea-Tac, for example, it’s suggested that you arrive at least two hours before departure time, with three hours considered a more reasonable recommendation. At the Walla Walla airport they recommend that you arrive 45 minutes before departure time.

Well, except during the holiday rush. During those times they recommend you arrive an hour early.

Like we said, it’s all-too-obvious who’s getting the last laugh here.


How Can I Delete a Backup File Created on the Previous Day?

Hey, Scripting Guy! Each day we have a program that creates a file with a name similar to this: backup_20050607.bak. How can I delete the previous day’s file?

-- JC

Hey, JC. Hmmm, a script that goes out and automatically deletes whatever happened the day before. Boy, could we have used a script like that a few weeks ago!

Yes, we know. But that’s a story we probably shouldn’t tell.

Besides, we have a column to write here. You want a script that can delete a file that has a name like backup_20050607.bak, with the 20050607 representing the previous day (in this case, June 7, 2005)? All you had to do was ask:

dtmYesterday = Date - 1

strYear = Year(dtmYesterday)

strMonth = Month(dtmYesterday)
If Len(strMonth) = 1 Then
strMonth = "0" & strMonth
End If

strDay = Day(dtmYesterday)
If Len(strDay) = 1 Then
strDay = "0" & strDay
End If

strYesterday = strYear & strMonth & strDay

strFileName = "C:\Backups\backup_" & strYesterday &
".bak"

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.DeleteFile(strFileName)

As you probably guessed, the tricky part here is constructing the file name; once we have that, deleting the file is a piece of cake. So then how do we construct that file name?

Well, the one part of the file name that varies from day-to-day is the part that represents the date the backup file was generated. That means all we have to do is construct that portion of the file name; the rest can then be hard-coded in. Therefore, we start off by determining the date for the previous day; we do that by subtracting one day from the current date and then storing that value in a variable named dtmYesterday:

dtmYesterday = Date - 1

Next we use the Year function to grab the four-digit year value (e.g., 2005) from dtmYesterday; that value gets stored in the variable strYear. Following that, we use the Month function to grab the month value from dtmYesterday.

Note, however, that we have to do a little extra coding when it comes to the month (and to the day, too). Why? Well, suppose we’re dealing with June. In that case, the Month function returns the value 6. That’s fine, except in our file name two digits are allocated for the month; the month needs to be listed as 06. Therefore, we have to determine whether we have a one-digit month or a two-digit month. If it’s one-digit month, we then have to place a leading zero in front of the number. That’s what we do here:

If Len(strMonth) = 1 Then
    strMonth = "0" & strMonth
End If

This is actually pretty straightforward code. The Len (length) function tells us how many digits are in the variable strMonth. If it’s just 1, then we add a leading 0:

strMonth = "0" & strMonth

If the length is equal to anything other than 1, then we leave well enough alone.

After grabbing the month value we repeat this exact same process with the Day function, enabling us to retrieve the day portion of the date. That gets stashed in a variable cleverly-named strDay.

And that gives us all the pieces we need to construct the file name. To do so, we first jam together the year, month, and day (giving us a string like 20050607) using this line of code:

strYesterday = strYear & strMonth & strDay

Then we simply tack on the rest of the path information (in this example, we’re assuming that the file is stored in the folder C:\Backups):

strFileName = "C:\Backups\backup_" & strYesterday & ".bak"

As you can see, we just combine the strings C:\Backups\backup_, the date we constructed (20050607), and .bak. Put them all together and you’ll end up with something like this:

C:\Backups\backup_20050607.bak

Cool.

We then use these two lines of code to create an instance of the FileSystemObject and delete the file:

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.DeleteFile(strFileName)

Because we used the FileSystemObject, this particular script will work only on the local machine. But what if the backup file is located on a remote computer? No problem; in that case we can use WMI to locate and remove the file. So then why didn’t we use WMI in the first place? Well, the FileSystemObject can locate and delete the file in less than a second; WMI will take a little longer. We decided to go with speed and efficiency.

But, like we said, that speed and efficiency won’t do you much good if the file is on a remote computer. Fortunately, in a situation like that you can use a WMI script to delete the file. This script (which we won’t discuss today) deletes the backup file from a remote computer named atl-fs-01:

dtmYesterday = Date - 1

strYear = Year(dtmYesterday)

strMonth = Month(dtmYesterday)
If Len(strMonth) = 1 Then
strMonth = "0" & strMonth
End If

strDay = Day(dtmYesterday)
If Len(strDay) = 1 Then
strDay = "0" & strDay
End If

strYesterday = strYear & strMonth & strDay

strFileName = "C:\\Backups\\backup_" & strYesterday &
".bak"

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

Set colFiles = objWMIService.ExecQuery _
("Select * from CIM_DataFile where Name = '" &
strFileName & "'")

For Each objFile in colFiles
objFile.Delete
Next

Just like that, yesterday’s gone, and we can all pretend that it never happened. Right, boss?

Note. Couldn’t we have saved ourselves all this trouble and simply deleted all the files with a creation/modification date equivalent to the previous day’s date? Yes, provided that there would never be any other files stored in that folder that would have the same date and might inadvertently be deleted. All things considered, this seemed like the safest approach.


How Can I Delete All Files Older Than a Specified Date?

Hey, Scripting Guy! I’d like to have a script that can search my computer for all files older than a certain date, and then automatically delete those files. Can I do that?

-- GM

Hey, GM. Can you write a script that will search for and delete all the old files on your computer? You bet. Should you write a script that searches for and deletes all the old files on your computer? That’s a separate question that we’ll deal with in a minute.

Let’s start with the code itself. Here’s a script that searches for all the files on your computer that were created before November 2, 2003 and then echoes back the name of each file:

strDate = "20031102000000.000000+000"

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where CreationDate < '" & strDate & "'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

We know what you’re thinking: how does the script know that our target date is November 2, 2003? After all, November 2, 2003 doesn’t appear anywhere in the script.

Believe it or not, it does; it might not look like a date, but 20031102000000.000000+000 – the value we assign to the variable strDate – represents November 2, 2003. The date looks weird because WMI uses the UTC (Universal Time Coordinate) format. With the UTC format, the first four digits (2003) represent the year; the next two (11) represent the month; and the next two (02) represent the day. In other words, 20031102 is the same as 11/02/2003.

Incidentally, the next six digits represent the hour, minute, and seconds, in 24-hour format; we’ve left these at 0 because we aren’t worried about a specific time (that is, we’re not looking for files created before 2:37 PM on November 2, 2003). The six digits after the period represent milliseconds; those should always be left as zeroes. Finally, the +000 represents the offset from Greenwich Mean Time. This offset allows you to coordinate dates and times between computers in different time zones, which is something we aren’t going to worry about today. Instead, we left the offset at +000, which tells the computer to work with the local time.

So do we have to pass the date and time in this UTC format? Yes. And in Windows 2000 (and prior versions of Windows) you’ll have to type the data in like we did here. In Windows XP and Windows 2003, however, there’s a new WMI object – SWbemDateTime – that allows you to type in a regular old date (like 11/2/2003), and then automatically converts that date to UTC format.

Note. Want to know more about SWbemDateTime? Watch the webcast “Thing the Scripting Guys Never Told You,” from Scripting Week 2.

After assigning the date to strDate what we have left is just a plain old WMI script, one that searches for all the files with a CreationDate earlier than 11/2/2003. When we find one, we echo the file name.

Of course, you asked for a script that deletes old files. And if that’s what you want to do, then just replace the line Wscript.Echo objFile.Name with this line of code:

objFile.Delete

Why didn’t we put this line of code in the script for you? Well, we wanted you to think about this script before actually running it. Suppose you have one disk drive (C) on your computer. Do you really want to run a script that deletes all the files created earlier than November 2, 2003? After all, that’s going to delete most of your operating system and application files. Generally speaking, not the sort of thing you want to do.

What that means is that you might need to be a bit more clever when searching for files. For example, maybe your computer has two drives: drive C has your operating system and all your applications, and drive D has all your documents. In that case, you could write a WQL query that searches only drive D for files created before November 2, 2003:

strDate = "20031102000000.000000+000"

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where CreationDate < '" & strDate & "'" & _
        " AND Drive = 'D:'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

Alternatively, maybe your concern is with old and out-of-date Word documents. In that case, search only for files with a doc file extension that were created prior to November 2, 2003:

strDate = "20031102000000.000000+000"

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where CreationDate < '" & strDate & "'" & _
        " AND Extension = 'doc'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

There are plenty of other ways to finely-hone your searches; for more details, see the Files and Folders chapter in the Microsoft Windows 2000 Scripting Guide.

Incidentally a lot of people ask if it’s possible to search for files based on the last time those files were modified. Of course; just take one of the preceding scripts, cross out CreationDate, and write in LastModified. In other words:

strDate = "20031102000000.000000+000"

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where LastModified < '" & strDate & "'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next


How Can I Delete All the .BAK Files in a Folder That Are More Than 7 Days Old?

Hey, Scripting Guy! How can I delete all the .BAK files in a folder that are more than 7 days old?

-- KT

Hey, KT. You know, on his way to work this morning the Scripting Guy who writes this column heard an advertisement for a local seafood restaurant. Now, this Scripting Guy doesn’t like seafood, so he normally wouldn’t pay any attention to an ad for a restaurant like this. However, his interest was piqued when the ad pointed out that this was the only restaurant in the Seattle area that served crab ice cream. As you might expect, the Scripting Guy who writes this column was shocked: Seattle considers itself a world-class city, yet it only has one restaurant that serves crab ice cream?!? That’s an outrage, pure and simple.

And we agree with you: someone should be fired over that. Those of you who live in other cities have no idea how good you have it. After all, any time you get a craving for crab ice cream you can probably just walk outside your front door and see half-a-dozen places where you can satisfy that craving. Sadly, that’s not true for those of us stuck in the Seattle area.

Note. If you’ve ever eaten crab ice cream, drop us a line and let us know; we’d love to hear about it. We’d also be interested in knowing how big the guy was who forced you to eat the stuff.

Admittedly we’re still a little shaken by all this, but we’ll try to answer your question anyway, KT. Not that this doesn’t represent something of an outrage itself: after all, deleting a specific type of file based on the file creation date is harder than it really should be. OK, we should qualify that a bit: doing this on the local computer isn’t too bad. But in this day and age, who wants to do everything on the local computer? If you want to perform this kind of task remotely – and we know that many of you do – things can get a little tricky. But that’s all right. Here’s a reasonably straightforward way to delete all the .BAK files in a folder that are more than 7 days old, and on either the local machine or a remote machine to boot:

dtmDate = Date - 7

strDay = Day(dtmDate)

If Len(strDay) < 2 Then
    strDay = "0" & strDay
End If

strMonth = Month(dtmDate)

If Len(strMonth) < 2 Then
    strMonth = "0" & strMonth
End If

strYear = Year(dtmDate)

strTargetDate = strYear & strMonth & strDay

strComputer = "."

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

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

For Each objFile In FileList
    strDate = Left(objFile.CreationDate, 8)
    If strDate < strTargetDate Then
        If objFile.Extension = "bak" Then
            objFile.Delete
        End If
    End If
Next

And don’t worry; as soon as we finish this dish of crab ice cream we’ll explain how this all works.

OK. The first thing we need to do is calculate our target date: in this case, that’s the current date minus 7 days. That’s what we do in this first line of code:

dtmDate = Date - 7

Because we’re going to use WMI we next need to convert dtmDate to a UTC-like date time value. We won’t discuss the UTC date-time format today; for more information see the Microsoft Windows 2000 Scripting Guide. For now all we need to know is that we must convert a date like 11/16/2006 to this, with the first four digits representing the year, the next two digits representing the month, and the last two digits representing the day:

20061116

It’s pretty easy to extract the individual pieces of a date-time value; we can do that using the Day, Month, and Year functions. The one tricky part occurs when we have a date like 6/5/2006. In that case both the month (6) and the day (5) have only one digit. Why is that a problem? That’s a problem because, in the UTC format, the month and the day must be two digits longs. To deal with that problem we’ve included code like this for both the day and the month:

strDay = Day(dtmDate)

If Len(strDay) < 2 Then
    strDay = "0" & strDay
End If

What are we doing here? Well, we’re first using the Day function to grab the day portion of the target date and store it in a variable named strDay. We then use the Len function to check the number of characters in strDay. If strDay has less than 2 characters we use this line of code to tack a leading zero onto the value:

strDay = "0" & strDay

In other words, if strDay equals 5, the preceding block of code will add a leading zero, making strDay equal to this:

05

Sometimes we Scripting Guys are so clever it’s scary, isn’t it? (Editor’s Note: Never as scary as crab ice cream though.)

After assuring ourselves that both the day and month have two digits, we then use this line of code to combine the year, month, and day into a UTC-like value:

strTargetDate = strYear & strMonth & strDay

Our next step is to connect to the WMI service on the local computer (although, again, we could just as easily run this script against a remote machine). We then use this crazy-looking query to return a collection of all the files found in the folder C:\Scripts:

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

Good point: we aren’t interested in all the files in C:\Scripts, are we? Instead, we’re interested only in .BAK files older than our target date. Don’t worry; we haven’t forgotten that. However, while it’s theoretically possible to write a WMI query that returns just those files, writing a query like that is probably more trouble that it’s worth. (Just like going all the way to the ice cream shop and finding out that all they have is crab ice cream.) We found it easier to instead return all the files, then simply going through the collection one-by-one , deleting those files that meet our criteria.

So how do we do that? Well, to begin with, we set up a For Each loop to loop through all the files in the collection. Inside that loop we use the Left function to grab the first 8 characters of the CreationDate property (a value that will be in UTC format) and store those characters in a variable named strDate:

strDate = Left(objFile.CreationDate, 8)

Why do we do that? Well, let’s say that this particular file was created on 10/30/2006. If that’s the case, that makes strDate equal to 20061030, a UTC-like value if we ever saw one. That also means that we can compare strDate to our target date by using code like this:

If strDate < strTargetDate Then

What if strDate is less than our target date? That means that this file is potentially a candidate for deletion. Why “potentially?” Well, remember, we want to delete only files that have a .BAK file extension. Therefore, we need to make a second check, one that looks at the file extension to see if it is equal to bak (note that, in WMI, you don’t include the period in the file extension):

If objFile.Extension = "bak" Then

If we do have a .BAK file older than the target date we then use this line of code to delete the file:

objFile.Delete

Otherwise we simply loop around and repeat the process with the next file in the collection. When we’re all done each and every .BAK file in the C:\Scripts folder (or at least those .BAK files older than our target date) will be gone.

Interestingly enough, in the course of researching this column we not only figured out how to delete old .BAK files, but we also discovered a way to enjoy crab ice cream. Here’s the secret: get a big glob of chocolate syrup. (Hot fudge is best, but any chocolate syrup will do.) Next, get a can of whipped cream and some chopped nuts (we’d recommend walnuts). Pour the syrup into a bowl, then top with the nuts and whipped cream. Finally, throw the crab ice cream away and just eat the syrup, nuts, and whipped cream. Delicious!


How Can I Delete All the .CSG Files on Drive C of a Computer?

Hey, Scripting Guy! How can I delete all the .csg files on drive C of a computer?

-- DL

Hey, DL. You know, it’s funny how quickly things can change. Just a few hours ago the Scripting Guy who writes this column was in such a bad mood that he had no intention of explaining to you (or anyone else for that matter) how they could delete all the .csg files on drive C of a computer; in fact, the Scripting Guy who writes this column was already making plans to go live on a deserted island and never speak to anyone ever again.

Note. What’s that noise you hear in the background? Nothing, really; just the other Scripting Guys cheering. Sorry to disappoint you, guys; turns out he’s not going anywhere after all.

Oh, come on, Scripting Editor: there’s no need to start crying.

So what could possibly have caused the jovial and fun-loving Scripting Guy to turn so grouchy? Well, his evening actually started out quite nicely: there’s nothing better than hopping in the car with the Scripting Son and heading for a Husky basketball game. As they drove along, however, and as they dealt with the Seattle traffic, the Scripting Son made some clever remark about there being so many people on the road that they should just stay home and watch the game on TV. At that moment, the Scripting Guy who writes this column had a sudden – and horrifying – thought: the tickets to the game were still sitting on the counter at home.

You’re right: that was a problem. Fortunately, thanks to a U-turn and some questionable driving tactics, the pair managed to return home, get the tickets, and still reach their favorite Seattle sandwich shop in time to grab some quick dinner before the game. (After all, red lights and stop signs are really more of a suggestion than anything else.)

Or at least they thought they were about to grab a quick dinner: instead, they discovered that their favorite sandwich shop was closed, soon to be replaced by a sports bar. A sports bar?!? Come on: if you want to drink beer and watch sports on TV you can do that at home. But can you get a huge sandwich, with deli-style ham piled high on fresh-baked bread, at home? Not at the Scripting House, that’s for sure.

And then, to add insult to injury, the Huskies raced to an 11-point halftime lead against the Oregon Ducks (of all teams), then promptly lost that lead just a few minutes into the second half. “If they lose this game, on top of everything else that’s happened tonight, then I’m moving to a deserted island,” muttered the Scripting Guy who writes this column.

Note. You know, we didn’t really pay any attention at the time, but now that you mention it the Scripting Son didn’t try to talk his dad out of moving to a deserted island. Hmmm ….

Fortunately, though, the Huskies pulled themselves together and clobbered the Ducks 89-77. In turn, the Scripting Guy who writes this column cashed in his one-way ticket to the deserted island, then sat down and hammered out a script that can delete all the .csg files on drive C of a computer:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Drive = 'C:' and Extension = 'csg'")

For Each objFile in colFiles
    objFile.Delete
Next

As you can see, this is a pretty simple little script. We begin by connecting to the WMI service on the local computer. Could we use this same script to delete .csg files from a remote computer? You bet; just assign the name of the remote computer to the variable strComputer, like so:

strComputer = "atl-fs-01"

After we make the connection to WMI we then issue this query to return a collection of all the .csg files found on drive C:

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Drive = 'C:' and Extension = 'csg'")

Like most WMI queries, the secret here lies in the Where clause. Obviously we don’t want to return (and thus delete) all the files on the computer; instead, we only want to return and delete files where two things are true:

· The file is on drive C.

· The file has a .csg file extension.

And thus, our Where clause: Where Drive = 'C:' and Extension = 'csg'. Note two things here. First, the colon is part of the drive designation: it’s drive C: and not drive C. Second, in WMI the period is not considered part of the file extension. Thus we’re looking for the extension csg and not the extension .csg.

So what do we do with the collection of files returned by our query? That’s easy: we simply set up a For Each loop to loop through that collection; inside the loop, we use the Delete method to delete the files one-by-one. That’s what this little block of code does:

For Each objFile in colFiles
    objFile.Delete
Next

Believe it or not, that’s all we have to do. Run this little script and any .csg files on drive C (and only on drive C) will be deleted.

You know, that’s another good question. Considering the fact that the Huskies have been struggling with both injuries and inexperience this year, would it really have been that big of a deal had they lost to Oregon? After all, the Ducks did come into the game ranked as the seventh-best team in the entire country.

Let’s put it this way. Suppose that the Martians landed on Earth and challenged the planet to a basketball game. If the Earth team wins the Martians will go home and we’ll all be saved. If the Earth team loses, however, the Martians will destroy our planet and everything on it.

Got that? OK, now suppose that the Oregon Ducks have been chosen to represent the planet Earth. With the fate of all humanity on the line, who do you think the Scripting Guy who writes this column will be rooting for?

Hey, come on: the destruction of the Earth is a small price to pay in exchange for the joy of watching Oregon lose.


How Can I Delete Specific Files in a Specific Folder?

Hey, Scripting Guy! How can I access the files in the folder T:\Act and delete any files that have the string “current” somewhere in the file name?

-- SC

Hey, SC. We have to admit, the notion of deleting anything current has a certain appeal, doesn’t it? Well, not for the Scripting Guys, of course; after all, our lives are so perfect there isn’t anything we’d want to get rid of. OK, maybe car payments. And mortgages. And that dog two houses down who still barks like crazy any time he sees us. (It’s been 10 years, dog; surely you must be used to us by now.)

But that’s about it. Maybe – no, definitely – the old guy around the corner who’s always outside without his shirt on. Oh, and – well, maybe we better focus on deleting all the files that have the string current somewhere in the file name:

strComputer = "."

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

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='T:\Act'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In colFileList
    If InStr(objFile.FileName, "current") Then
        objFile.Delete
    End If
Next

This script starts out – as many of our WMI scripts do – by binding to the WMI service on the local computer. (Of course, like most WMI scripts, this one can also be run against a remote computer.) We then use this query to return a collection of all the files in the folder T:\Act:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='T:\Act'} Where " _
        & "ResultClass = CIM_DataFile")

Next we set up a For Each loop in order to walk through the collection of files. Inside that loop we use the InStr function to determine whether or not the string current can be found in the file name:

If InStr(objFile.FileName, "current") Then

If InStr is True that means that the string can, indeed, be found within the file name. In that case, we use a single line of code to delete the file:

objFile.Delete

Yep: it’s that easy. We then loop around and repeat the process with the next file in the collection. By the time we’re done any file in T:\Act that had the string current in its file name will have gone to that great hard disk in the sky.

Or, to put it in terms that actually make sense, those files will have been deleted.

Now if we could just do the same thing with that dumb orange car – it doesn’t even have a hood! – that the neighbors have parked outside the house. And those blackberry bushes that keep trying to take over the yard. And ….


How Can I Delete Specified Characters From the Beginning and End of My File Names?

Hey, Scripting Guy! How can I truncate file names by removing a common beginning and ending, leaving only the unique middle portion of the name?

-- RE

Hey, RE. Before we answer your question we’d like to point out that we’ve finally posted the winners of the Scripting Guys TechEd Challenge. (If you are a winner we’ve got good news: our shipping materials have arrived, which means that, at long last, your prizes are out the door and on the way.) We’ve also posted one possible solution to the puzzle, a solution the Scripting Guys came up with and – we add proudly – a solution that would have earned us a perfect score of 140 points. Wasn’t it really, really hard to come up with a perfect score? No, of course not. Not for the Scripting Guys anyway.

Note. OK, so that might be due, in part, to the fact that we cheated: we started with a list of words and then derived the puzzle using that list. But hey, that’s about the only way the Scripting Guys could even hope to come up with a perfect score.

And even that took a few tries.

When we first posted the challenge we were concerned that people would find it too difficult. “This is hard,” we thought. “We need to offer a really big incentive in order to get people to even try solving the puzzle.” With that in mind, we made a ridiculous offer that we knew we’d never have to follow up on: if anyone turned in a perfect score, we’d rename the Script Center after them (well, for one day, that is).

Much to our surprise, nine different people submitted perfect scores, and some of those people submitted more than one perfect entry. (At some point you’d think we’d learn that just because the Scripting Guys find something to be really, really hard that doesn’t mean that anyone else will find that same task to be even the slightest bit challenging.) To tell you the truth, we weren’t prepared for all those perfect entries. Nevertheless, a deal is a deal, so sometime in the very near future we’ll begin renaming the Script Center in honor of our grand prize winners.

Note. So how does TechNet feel about us renaming the Script Center in honor of our grand prize winners? Surprisingly enough, TechNet is 100% in favor of this; in fact, the good people at TechNet think that this is by far the best idea the Scripting Guys have ever come up with.

Or at least that’s what they would think, provided they actually knew about all this.

Or at least that’s what we think they would think.

Now back to RE’s question. RE has a folder that has a series of files with names similar to this:

8901 George.txt
File 1234 George.txt
File 2345 George.txt
File 3456 George.txt
File 4567 George.txt
File 5678 George.txt
File 6789 George.txt
File 7890.txt

As you can see, most of the file names start with File and then end with George; a few files start with File but don’t end with George, and a few others end with George but don’t start with File. All the files, however, have a unique “middle name;” for example, the file File 1234 George.txt has a “middle name” of 1234. What RE would like to do is take that file name and remove the File and/or the George, resulting in file names like this:

8901.txt
1234.txt
2345.txt
3456.txt
4567.txt
5678.txt
6789.txt
7890.txt

How can he do that? Here’s one way:

strComputer = "."

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

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

For Each objFile In colFiles
    strPath = objFile.Drive & objFile.Path
    strExtension = objFile.Extension
    strFileName = objFile.FileName

    If Left(strFileName, 5) = "File " Then
        intLength = Len(strFileName)
        strFileName = Right(strFileName, intLength - 5)
    End If

    If Right(strFileName, 7) = " George" Then
        intLength = Len(strFileName)
        strFileName = Left(strFileName, intLength - 7)
    End If

    strNewName = strPath & strFileName & "." & strExtension
    errResult = objFile.Rename(strNewName)
Next

We actually get quite a few questions similar to this, questions about removing specific bits of text that have been tacked onto the beginning and/or the end of file names. Because of that, and because that’s what we’re supposed to do in this column anyway, let’s take a few minutes to walk through the code and see if we can figure out what’s going on here.

The script starts out by connecting to the WMI service on the local computer. Could we modify this script to perform the same task against a remote computer? You bet we can; to do that, simply assign the name of the remote computer to the variable strComputer:

strComputer = "atl-fs-01"

After connecting to the WMI service we then use the following bit of code to return a collection of all the files (instances of the CIM_DataFile class) found in the folder C:\Test:

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

When the collection comes back we set up a For Each loop to loop through all the files in that collection. And then we get down to business.

Admittedly, there are a couple different ways we could go about this task; we’ve opted to use an approach that requires a few extra lines of code but is a little safer to use. What makes it “safer?” Here’s one example. We could just use the Replace function to replace any instances of target terms File and George in the file path. So why don’t we? Well, suppose we had a file named File George Washington File George.txt. Doing a blank search-and-replace operation would result in this:

Washington.txt

In other words, we would have removed more than just the beginning and the ending. In turn, that means that we could run into name collisions if the folder also included a file named, say, File Washington George.txt. Likewise, suppose the folder we’re working with is named Documents From George. Removing all instances of George would delete the term from the folder path as well, something which would lead to all sorts of trouble. And the Scripting Guys don’t want trouble.

Which is why we didn’t mention that rename-the-Script-Center thing to anyone from TechNet.

At any rate, the first thing we do inside the loop is execute the following three lines of code:

strPath = objFile.Drive & objFile.Path
strExtension = objFile.Extension
strFileName = objFile.FileName

All we’re doing here is grabbing the individual pieces of the file path. If we have a file named C:\Test\File 1234 George.txt, that means that our variables will end up being assigned the following values:

Variable | Value
strPath | C:\Test\
strExtension | txt
strFileName | File 1234 George

For more information about properties like Drive and Path, see the Microsoft Windows 2000 Scripting Guide.

Our next step is to determine whether or not the file name starts with the characters File and, if it does, delete those characters. That’s what this block of code is for:

If Left(strFileName,
5) = "File " Then
intLength = Len(strFileName)
strFileName = Right(strFileName, intLength - 5)
End If

As you can see, we’re using the Left function to determine whether the first five characters in our file name are equal to File (note the blank space at the end). Let’s assume that they are. In that case, we then use the Len function to determine the number of characters in the string (i.e., the length of the string), storing that value in a variable named intLength:

intLength = Len(strFileName)

That brings us to this line of code:

strFileName = Right(strFileName, intLength - 5)

This is where we strip the word File from the beginning of the string. What we’re doing here is using the Right function to grab characters from the end of the string, working our way back towards the beginning. How many characters are we going to grab? We’re going to grab the total number of characters in the string, minus 5. And why “minus 5?” That’s easy: this will scoop all the characters in the string except for the first 5. In other words, if we have a file named File 1234 George strFileName will be assigned the following value:

1234 George

Which means that we’re now halfway towards our goal of truncating the first file name.

We then use a similar process to determine whether the term George (again, note the blank space, this time in front of the term) appears at the end of the file name; if so, we go ahead and delete that term as well. That’s what this block of code is for:

If Right(strFileName,
7) = " George" Then
intLength = Len(strFileName)
strFileName = Left(strFileName, intLength - 7)
End If

What’s the net result of all this? The net result is that strFileName is now equal to the file’s “middle name:”

1234

That’s great; this is just what we wanted. However, we still have one problem: when using WMI you can’t rename a file unless you pass the Rename method the complete file path. (Which means that simply having a file name of 1234 doesn’t do us much good.) With that in mind, we use the following line of code to construct a new file path, storing that value in the variable strNewName:

strNewName = strPath & strFileName & "." & strExtension

Nothing too fancy here; we’re simply combining the following values:

· C:\Test\

· 1234

· .

· txt

Put them all together and strNewName ends up with this value:

C:\Test\1234.txt

And you know what? You’re right: that does look an awful lot like a complete file path, doesn’t it? We’re glad you pointed that out, because that means we can go ahead and call the Rename method and actually rename the file:

errResult = objFile.Rename(strNewName)

And then it’s back to the top of the loop, where we repeat this process with the next file in the collection. When all is said and done, the folder C:\Test will be the proud owner of the following set of files:

8901.txt
1234.txt
2345.txt
3456.txt
4567.txt
5678.txt
6789.txt
7890.txt

Not bad, not bad at all.

Incidentally, if you liked the TechEd Challenge we will likely have some similar puzzles for you in the near future: we’re currently negotiating with TechNet Magazine to create a puzzle for each and every issue. Nothing is set in stone just yet, but we’ll likely end up creating these puzzles for them. Nonetheless, we do expect that the negotiations will be a bit contentious; after all, they pay us absolutely nothing to write the Hey, Scripting Guy! column for the magazine, and we need an iron-clad guarantee that we’ll get the exact same deal before we agree to start doing puzzles for them as well.

Don’t worry: no one takes advantage of the Scripting Guys.


How Can I Determine if a File Exists and, if It Does, Exit the Script?

Hey, Scripting Guy! How can I check to see if a particular file exists and, if it does, exit the script?

-- TO

Hey, TO. There are at least two ways to check for the existence of a file (that’s the real key here; exiting the script is easy). One of these approaches uses the FileSystemObject and the other uses WMI, and we’re going to show you both methods today. Why? Well, that way if anyone comes by and asks us to help them with something we can say, “Wow, we’d like to, but we have to finish this column, and because we’re showing people two scripts, well, that could take awhile.”

OK, so that’s only partly true: there are actually a couple valid reasons why you might choose one approach over the other. People generally find the FileSystemObject a bit easier to script; the only problem is that the FileSystemObject is designed to work solely on the local computer. WMI scripts tend to be a tad bit more complicated (although, as you’ll see, they’re still pretty easy), but WMI scripts can work with remote computers just as easily as they can the local computer. What does that mean? It means that, if you’re working on the local computer, you might want to use the FileSystemObject. If you’re working against a remote computer you need to use WMI.

In the grand tradition of the Scripting Guys let’s do the easy one first. Let’s take a look at a FileSystemObject script that checks for the existence of the file C:\Scripts\Test.txt. If the file is found the script exits; if the file is not found then the script displays a message to that effect.

Here’s the script:

Set objFSO = CreateObject("Scripting.FileSystemObject")

If objFSO.FileExists("C:\Scripts\Test.txt") Then
    Wscript.Quit
Else
    Wscript.Echo "The file does not exist."
End If

We told you it was easy. We begin by creating an instance of the Scripting.FileSystemObject and then call the FileExists method, passing the method the complete path to the file in question. If FileExists returns True that means the file was found; at that point we then use Wscript.Quit to exit the script. If FileExists is not found we then use this line of code to echo back our failure to find the file:

Wscript.Echo "The file does not exist."

That’s pretty much all we have to do.

Incidentally, it’s a good idea to use the FileExists method any time you’re working with the FileSystemObject. The FileSystemObject is very powerful, but also very sensitive: it just falls to pieces when things go wrong. Worst of all, even using On Error Resume Next won’t help; if the FileSystemObject tries to work with a non-existent file or folder your scripts are likely to crash even if you use On Error Resume Next. The way to work around that problem? Just use FileExists (or its companion method, FolderExists) before trying to bind to a file or folder.

OK, well, that’s all for today – oh, that’s right, we said we’d show you two scripts, didn’t we? (What were we thinking?) As we noted, you can also check for the existence of a file using WMI. The nice thing about using WMI is that the same script can run against remote computers as well as local computers; all you have to do is change the name of the computer and have at it.

Here’s a WMI script that checks for the existence of C:\Scripts\Test.txt and, if the file is found, exits:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_Datafile Where Name = 'C:\\Scripts\\Test.txt'")

If colFiles.Count > 0 Then
    Wscript.Quit
Else
    Wscript.Echo "The file does not exist."
End If

Like we said, a tad bit more complicated, but nothing that you shouldn’t be able to handle. We begin by setting the value of the variable strComputer to a dot; in the wonderful world of WMI the dot represents the local computer. But didn’t we say that this same script could be used against remote computers? Yes, we did, and that’s a simple enough task: just set the value of strComputer to the name of that remote computer. For example, this line of code will cause the script to run against the computer atl-fs-01:

strComputer = "atl-fs-01"

Next we connect to the WMI service, then use the ExecQuery method to select all instances of the CIM_Datafile class where the Name property is equal to C:\\Scripts\\Test.txt. And don’t panic, you haven’t suddenly been afflicted with double vision. In WMI the \ is a reserved character; that means any time a \ appears in a query the character must be “escaped,” which simply means prefacing the \ with a second \. Thus a file path like C:\Scripts\Test.txt gets written out as C:\\Scripts\\Test.txt. Hey, as long as it works, right?

Note. By the way, this could be worse. For example, suppose we had a path like \\atl-fs-01\public\scripts. That would get written out like so: \\\\atl-fs-01\\public\\scripts, with every single \ doubled up.

Our query returns a collection consisting of all the files on the computer named C:\Scripts\Test.txt. (Needless to say, there can either be one such file or no such files.) To determine whether or not the file was found we check the Count property of the returned collection. If Count is greater than 0 that means the file was found; in turn we call Wscript.Quit and exit the script. If the Count is not greater than 0 than no such file could be found and we echo a message to that effect.

So there you have it: not one but two ways to determine whether a file exists and, if it does, exit the script. Wow, no doubt someday people will be saying, “Where were you on the day the Scripting Guys showed us two ways to determine whether a file exists?” Until then, just use whichever script makes the most sense for you.


How Can I Determine if My Users Have Certain Files on Their Computers?

Hey, Scripting Guy! Is it possible to search a computer for .MP3 files or other files my users aren’t supposed to have?

-- AK

Hey, AK. Once again it’s WMI to the rescue. Using the CIM_DataFile class it’s easy to search a computer for specific file types. Want to know if your users have any .MP3 files lying around? Then just use a script similar to this:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Extension = 'mp3'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

As you can see, this is a pretty standard WMI script: we’re just looking for all instances of the CIM_DataFile class that have an Extension of MP3 (note that you don’t include the period; it’s MP3 and not .MP3). A very simple little script, and it runs reasonably fast: on a 2.39 GHz laptop, with 512 MB of RAM and 30 gigabytes of hard disk space, we got back a list of all the .MP3 files in less than 30 seconds.

It’s even possible to search for more than one file type with a single query. For example, this script searches for .WMA files as well as .MP3 files:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Extension = 'mp3' OR Extension = 'wma'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

But that’s not the cool thing. You said these are files your users are not allowed to have; we’re guessing that means you’d like them removed from their computers. The preceding script won’t do that; it just reports back the names of all the .MP3 and .WMA found on a computer. It’s still up to you to contact each user and ask him or her to delete the offending files. That’s a lot of work on your part, and you then have to rely on the users deleting the files (without accidentally deleting something else in the process). So why not let the script save you all that trouble? This version not only tracks down all the .MP3 and .WMA files on a computer, it also deletes the files as they are found:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Extension = 'mp3' OR Extension = 'wma'")
For Each objFile in colFiles
   objFile.Delete
Next

And remember, this works on remote computers just as quickly and just as easily as it does on the local computer. Just change the value of the variable strComputer to the name of the remote computer and have at it.


How Can I Determine the 20 Largest Files on a Computer?

Hey, Scripting Guy! How can I determine the 20 largest files on a computer?

-- PW

Hey, PW. Having recently returned from a vacation in Europe, the Scripting Guy who writes this column now understands that there are different ways to do things. Some of these would be difficult to adjust to on a regular basis: for example, ordering a soft drink in a restaurant and getting a teeny-tiny little glass with no ice and without free refills still seems like a crime against humanity. Other things – like getting 6-8 weeks of paid vacation each year – would be a bit easier to accept. The point is not that any of these are good (crepes smothered in dark chocolate sauce) or bad (pastries filled with – and we are not making this up – snails). The point is that it’s OK to do and to try different things.

Which is just a way of rationalizing the fact that, unlike our usual approach, this time we aren’t going to provide you with a solution that’s built into the operating system. Determining the 20 largest files on a computer is theoretically possible using nothing more than WMI; however, the script would be a bit cumbersome to write, and might take hundreds of thousands of years to run. (Which, coincidentally enough, is the same amount of time that elapses between the moment you finish your meal and the moment your Parisian waiter brings you the check.) If WMI represented the only way to find the 20 largest files we’d bite the old bullet and do that. But there’s a much better and much easier way to do this: download, install, and use Log Parser 2.2.

If you aren’t familiar with Log Parser 2.2, you should take a look at this Tales from the Script column. Log Parser is a nifty little utility that, as the name implies, makes it quick and easy to parse plain-text log files. However, Log Parser also makes it quick and easy to parse event logs, the file system, the registry, even Active Directory. After you’ve installed Log Parser, retrieving (and sorting) the 20 largest files on a computer requires just a couple minutes of processing time, and involves a script no more complicated than this:

Set objLogParser =
CreateObject("MSUtil.LogQuery")
Set objInputFormat =
CreateObject("MSUtil.LogQuery.FileSystemInputFormat")
objInputFormat.Recurse = -1

Set objOutputFormat = CreateObject("MSUtil.LogQuery.NativeOutputFormat")
objOutputFormat.rtp = -1

strQuery = "SELECT Top 20 Path, Size FROM 'C:\*.*, D:\*.*' ORDER BY Size
DESC"
objLogParser.ExecuteBatch strQuery, objInputFormat, objOutputFormat

We won’t explain this script in great detail today; for that level of information you should take a look at the Tales from the Script column. We will note, however, that the script begins by creating an instance of the MSUtil.LogQuery object, then uses this line of code to indicate that we want to work with items in the file system:

Set objInputFormat = CreateObject("MSUtil.LogQuery.FileSystemInputFormat")

We then set the value of the Recurse property to -1; that tells Log Parser that we want to recursively search through all the folders in the specified path. (And, no, we haven’t specified the path yet; we’ll do that in a minute.)

Those initial lines of code set up the input parameters; we then use the next two lines of code to set up the output parameters:

Set objOutputFormat = CreateObject("MSUtil.LogQuery.NativeOutputFormat")
objOutputFormat.rtp = -1

Again, without going into any great detail, the first line tells Log Parser to output data to the command window; setting the rtp property to -1 tells Log Parser to write all the data at once, without pausing after each screen and waiting for the user to press a key to continue. With Log Parser you aren’t limited to outputting data to the command window, but – for now – this seemed like the easiest and most intuitive approach.

With the input and output parameters taken care of we next create our Log Parser query:

strQuery = "SELECT Top 20 Path, Size FROM 'C:\*.*, D:\*.*' ORDER BY Size DESC"

If you have some experience at writing SQL queries this particular line of code should be pretty straightforward. All we’re doing here is asking for the 20 largest files; to do that we request the “Top 20” files, sorted by size in descending order. When we make that request Log Parser will grab all the files and sort them in descending order; because we requested only the first (top) 20, however, the only data that will be displayed onscreen will be the 20 largest files. What if we wanted the 50 largest files? Then we’d ask for the Top 50 files:

strQuery = "SELECT Top 50 Path, Size FROM 'C:\*.*, D:\*.*' ORDER BY Size DESC"

What if we wanted the 20 smallest files? In that case, we’d sort the files in ascending order, which would put the smallest files at the top of the list:

strQuery = "SELECT Top 20 Path, Size FROM 'C:\*.*, D:\*.*' ORDER BY Size ASC"

See? We told you it was pretty straightforward.

As you can see, we’re asking only for the file path and the file size; needless to say, we could get other information about the files as well. (See the Log Parser documentation for more information.) Also, note that we need to indicate each of the drives that we want to search, separating the drives by commas:

strQuery = "SELECT Top 20 Path, Size FROM 'C:\*.*, D:\*.*' ORDER BY Size DESC"

By the way, *.* is a standard file system wildcard meaning “all the files, regardless of file name or extension.” If we were interested in only the 20 largest Microsoft Word documents we would modify our query like so:

strQuery = "SELECT Top 20 Path, Size FROM 'C:\*.doc, D:\*.doc' ORDER BY Size DESC"

Save the script as a .vbs file and have at it; you’ll be surprised at how quickly you’ll get back information regarding the 20 largest files on the computer.

Although probably not as surprised as we were to discover that a traditional English breakfast includes both baked beans and a grilled tomato. But hey, food is food, right?

Unless, of course, there happens to be snails in it.


How Can I Determine the Owner of a File?

Hey, Scripting Guy! Is there a way to determine the owner of a file by using a script?

-- BD

Hey, BD. As a matter of fact there is a way to use a script to determine the owner of the file, though it’s understandable why you might not be able to find that information on your own. After all, there are two main scripting interfaces used for managing files: the Script Runtime’s FileSystemObject and the WMI class CIM_DataFile. Seeing as how neither of these interfaces includes a property or method for determining file ownership, the logical conclusion would be, “Oh, I guess you can’t do this after all.”

But you know how it is with scripting: things can often be done, though not the way you might logically expect to do them. In this case, you need to use WMI’s Win32_LogicalFileSecuritySetting class in conjunction with the Win32_LogicalFileOwner association class to determine file ownership. As you might expect, the LogicalFileSecuritySetting class grabs security information from a file. What it can’t do, however, is tell you the name of the file owner. That’s where the Win32_LogicalFileOwner class comes into play: it takes the owner’s SID (security identifier) and relays it to the Win32_SID class. The Win32_SID class can then lookup and report the owner name and domain.

Confused? We don’t blame you; association classes are not the most intuitive thing ever created. Fortunately, however, you don’t need to understand how association classes work; just rest assured that they do work. For example, here’s a script that reports the owner of the file C:\Scripts\My_script.vbs:

On Error Resume Next

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
      & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

strFile = "C:\Scripts\My_script.vbs"

Set colItems = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_LogicalFileSecuritySetting='" & strFile & "'}" _
        & " WHERE AssocClass=Win32_LogicalFileOwner ResultRole=Owner")

For Each objItem in colItems
    Wscript.Echo objItem.ReferencedDomainName
    Wscript.Echo objItem.AccountName
Next

Looks crazy, but it will do the job. And what if you need to get the owner of a different file? No problem: just set the value of the variable strFile to the complete path of that file.

And don’t let the names mislead you: these two classes can also be used to determine the owner of a folder. For example, this script reports back the owner of the folder C:\Scripts:

On Error Resume Next

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
      & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

strFile = "C:\Scripts"

Set colItems = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_LogicalFileSecuritySetting='" & strFile & "'}" _
        & " WHERE AssocClass=Win32_LogicalFileOwner ResultRole=Owner")

For Each objItem in colItems
    Wscript.Echo objItem.ReferencedDomainName
    Wscript.Echo objItem.AccountName
Next


How Can I Determine the Version Number of a File?

Hey, Scripting Guy! How can I extract version information for a DLL or other type of file?

-- DM

Hey, DM. There at least two ways we know of to retrieve the version information for a file: one designed for working on the local computer and one that can work either locally or remotely. Being the kind of guys that we are, we’ll show you both solutions.

Ok, being the kind of guys that we really are, we’d charge you for both solutions. But Microsoft won’t let us do that, so you get both scripts for free.

Of course, if you want to send us a check anyway….

Let’s start with the version designed for the local computer. This script uses the FileSystemObject and requires a grand total of 2 lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Wscript.Echo objFSO.GetFileVersion("c:\windows\system32\scrrun.dll")

As you can see, there isn’t much work involved here (one reason why it’s a Scripting Guys favorite). We simply create an instance of the FileSystemObject and then echo the value returned by the GetFileVersion method. And there isn’t much work involved in calling GetFileVersion, either; in fact, all we have to do is specify the file in question – in this case, C:\Windows\System32\Scrrun.dll – and the FileSystemObject will do the rest.

So if that’s easy why don’t we just recommend this script and be done with it? Well, for two reasons. First, the FileSystemObject is designed for use on the local computer; it’s not really designed to work against remote machines. (You can often use it against remote machines as long as you specify an administrative share: \\atl-ws-01\C$\Windows\System32\Scrrun.dll. However, even this doesn’t always work.)

Second, GetFileVersion does only one thing: it returns the version number of a file. (And by file we generally mean a .dll or .exe file; those are typically the only files that actually have version numbers.) What if you wanted some additional information, like the file size or the last modification date? GetFileVersion won’t help you there; you’d have to combine our two-line script with some additional code in order to get a more complete set of file properties.

Because of that you might want to use WMI to determine the version of a file. WMI works just as well remotely as it does locally; in addition, the CIM_Datafile class used to determine the file version can also return all sorts of other information about a file. The only drawbacks? A WMI script is a tad bit more complicated (but just a tad) and runs a tiny bit slower than a FileSystemObject script. Other than that, however, the two scripts are identical: they both return the version of a specified file.

Here’s a WMI script that reports back the version of the file C:\Windows\System32\Scrrun.dll:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_Datafile Where Name = 'c:\\windows\\system32\\scrrun.dll'")

For Each objFile in colFiles
    Wscript.Echo objFile.Version
Next

Like we said, not too bad. The only thing to be aware of is the way we format path names in our query:

"Select * from CIM_Datafile Where Name = 'c:\\windows\\system32\\scrrun.dll'"

Note that we had to use \\ throughout the path. That’s because the \ is a reserved character in WMI, and any time we want to use a \ in a Where clause we have to preface it with another \. It’s not a big deal; just something to keep in mind.

Other than that the script is pretty straightforward. This sample script returns information about the Scrrun.dll file found on the local computer. If you’d like to get that same information off a remote machine then change the value of the variable strComputer. For example, this line of code assigns the value atl-ws-01 to the variable strComputer:

strComputer = "atl-ws-01"

In turn, that will cause the script to run against the computer atl-ws-01.


How Can I Determine the Version Number of an Executable File?

Hey, Scripting Guy! How can I determine the version number of an executable file?

-- TW

Hey, TW. Well, that depends. If the file is located on the local computer, the easiest way is to use the FileSystemObject. Have a burning desire to know which version of Regedit.exe is installed on your computer? All it takes is two lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Wscript.Echo objFSO.GetFileVersion("c:\windows\regedit.exe")

As you can see, this is about as simple a script as you’ll ever write. In line 1, we create an instance of the FileSystemObject; in line 2, we use the GetFileVersion method to return the version number of C:\Windows\Regedit.exe. That’s all there is to it.

OK, we know: some of you are thinking, “Sure, that works fine if the file in question is located on the local computer. But what if that file is located on a remote computer? After all, the FileSystemObject is designed to work locally, not remotely. What do we do then?”

Relax; if the file is located remotely, you can use WMI and the CIM_DataFile class to get back this same information. Here’s a script that reports the version of Regedit,exe installed on the remote computer atl-ws-01:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("SELECT * FROM CIM_DataFile WHERE Name = 'c:\\windows\\regedit.exe'")
For Each objFile in colFiles
     Wscript.Echo objFile.Version
Next

Again, pretty straightforward, with one exception: notice that you have to use two slashes (\\) any time you include a path name in a WQL query. As a result, we end up looking for C:\\Windows\\Regedit.exe rather than C:\Windows\Regedit.exe. Other than that it’s a regular old WMI script, the kind you’ve written many times before


How Can I Determine Whether a File Exists on a Remote Computer?

Hey, Scripting Guy! How can I check to see if a file exists on a remote computer even if that file isn’t in a shared folder?

-- SP

Hey, SP. You know, the Scripting Guy who writes this column had minor periodontal surgery this morning. (Well, maybe his dentist thought it was minor. The Scripting Guy, who’s a bit of a baby when it comes to these sorts of things, begs to differ.) And, yes, our hero came to work anyway, meaning he’s either a real trouper or, well, an idiot. (Editor’s Note: I think we all know the answer to that one.) At any rate, even though he’s in no pain or discomfort (aside from the fact that he was only allowed to eat yogurt at lunch) he was definitely hoping to find an easy question to answer for today’s column. SP, this is just what the doctor ordered!

OK, technically the doctor ordered Vicodin and penicillin. But you know what we mean.

As you noted, you want to verify the existence of a file on a remote computer; however, that file might not be stored in a shared folder. Is that a problem? Well, it could be, if you were using the FileSystemObject. But if you’re using WMI, well, then it’s no problem at all: WMI can locate files in a regular old folder just as easily as it can locate files in a shared folder. Verify the existence of a file on a remote computer? It’s as easy as pulling teeth.

So to speak.

For example, here’s a script that will check to see if the file C:\Scripts\Test.vbs can be found on the remote computer atl-fs-01:

strComputer = "atl-fs-01"

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

Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where Name = 'C:\\Scripts\\Test.vbs'")

If colFiles.Count = 0 Then
    Wscript.Echo "The file does not exist on the remote computer."
Else
    Wscript.Echo "The file exists on the remote computer."
End If

We told you this Scripting Guy was looking for an easy question to answer; as you can see, there’s not much to this script at all. We begin by assigning the name of the remote computer to a variable named strComputer:

strComputer = "atl-fs-01"

In an unusual plot twist (at least for this column), that brings up an interesting question: could we run this same script on the local computer? You bet; all we have to do is assign a dot (.) to the variable strComputer:

strComputer = "."

Note. Why a dot? That’s easy: when constructing a WMI path the dot provides a way to indicate the local computer, even if you don’t know the name of the local computer.

After assigning the computer name to strComputer we connect to the WMI service on atl-fs-01. That brings us to this line of code:

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where Name = 'C:\\Scripts\\Test.vbs'")

Here we’re retrieving a collection of all the files on atl-fs-01 that have a name equal to C:\Scripts\Test.vbs. (In WMI, the Name property is equivalent to the file path.) And no, you’re not seeing double: we really did write out the path as C:\\Scripts\\Test.vbs. That’s not due to us taking one-too-many painkillers (we’re too tough to take any painkillers!). Instead, that’s due to the fact that the \ is a reserved character in WMI; because of that any time you use the \ in a query you must “escape” the character by prefacing it with another \. It’s a bit goofy-looking, but it works.

Now, where were we? (On painkillers, tough guy.) Oh, right: as we noted, our query returns a collection of all the files with the Name C:\Scripts\Test.vbs. To determine whether or not the file exists all we have to do is check the Count of the collection; as the name implies, the Count tells you the number of items in the collection. Suppose Count is equal to 0. That means there are no items in the collection; in turn, that means that there are no files named C:\Scripts\test.vbs located on atl-fs-01. Therefore, we echo back the fact that the file does not exist. If Count is not equal to 0 (and, because file paths must be unique, the only other possibility is a Count equal to 1) we echo back the fact that the file does exist on atl-fs-01.

Pretty simple, right? All that takes place inside this block of code:

strComputer = "atl-fs-01"

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

Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where Name = 'C:\\Scripts\\Test.vbs'")

If colFiles.Count = 0 Then
    Wscript.Echo "The file does not exist on the remote computer."
Else
    Wscript.Echo "The file exists on the remote computer."
End If

And there you have it. Because WMI works by connecting to the remote machine itself, it doesn’t matter whether the file is in a shared folder or not; WMI will track it down for you.

Ah, good question: suppose you’re looking for the file Test.vbs but you aren’t sure what folder that file might be stored in. Can WMI located a file even when you don’t know the folder name? You bet it can. We won’t discuss this script in any detail today, but it searches atl-fs-01 for any file named Test.vbs, regardless of the folder that file might be stored in:

strComputer = "atl-fs-01"

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

Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where FileName = 'Test' and Extension = 'vbs'")

If colFiles.Count = 0 Then
    Wscript.Echo "The file does not exist on the remote computer."
Else
    Wscript.Echo "The file exists on the remote computer."
End If

Note. For more information on using scripts to manage files and folders – including an explanation of what properties such as FileName and Extension represent – see the Microsoft Windows 2000 Scripting Guide.

We hope that helps, SP. We’d be happy to stay and chat about this, except it’s time to go home and eat dinner. And, trust us, after being limited to yogurt for lunch we are starved. Fortunately, for dinner the dentist has said we can have … um, yogurt …. Oh: and maybe a banana. How … nice ….

Come to think of it, SP, if you’d like to chat more about this script, well, maybe we can stay a bit longer after all ….


How Can I Display the Vertical and Horizontal Sizes of a

How Can I Display the Vertical and Horizontal Sizes of a .JPG File?

Hey, Scripting Guy! How can I display the vertical and horizontal resolutions of a .JPG file?

-- ST

Hey, ST. This is a very commonly-asked question, and for the longest time we had no answer for it: nothing built into the operating system seemed capable of returning this information, and even Dsofile, a COM object released by the Microsoft Office team specifically to return the values of extended properties like this, can’t get us picture sizes. The whole thing seemed hopeless.

But just because things looked bleak does that mean we gave up? Well, to tell you the truth, yes. But then we pieced together a Fun Zone article on using scripts to manage music files and playlists, and purely by accident we stumbled upon a workaround: as long as you import all your .JPG files into Windows Media Player you can use scripts to return information about the file, such as a picture’s vertical and horizontal size.

Now keep in mind that, by default, Windows Media Player doesn’t automatically keep track of photo items. To add .JPG files to your media collection from inside Windows Media Player you’ll need to click File and then select one of the options under Add to Library. Before you can do that, however, you must first click Tools, click Options, and then, on the Player tab, select Enable picture support for devices. If you don’t enable picture support, Windows Media Player will tell you that .JPG is an unsupported file extension. Enable picture support, then start adding pictures to the media collection.

Note. This doesn’t move your files or convert your .JPGs to some sort of proprietary Windows Media Player format; in fact, it doesn’t do anything to your files other than make them known to the Windows Media Player library.

After your .JPG files are known to Windows Media Player than you can use a script like this to retrieve the vertical and horizontal sizes of each picture:

Set objPlayer = CreateObject("WMPlayer.OCX" )
Set colMediaCollection = objPlayer.mediaCollection

Set objPhotos = colMediaCollection.getByAttribute("MediaType", "photo")

For i = 0 to objPhotos.Count - 1
    Set objPhoto = objPhotos.item(i)
    Wscript.Echo "Name: " & objPhoto.Name
    Wscript.Echo "Height: " & objPhoto.getItemInfo("WM/VideoHeight")
    Wscript.Echo "Width: " & objPhoto.getItemInfo("WM/VideoWidth")
Next

The script begins by creating an instance of the WMPlayer.OCX object, and then binds to the Windows Media Player media collection. The script then uses the getByAttribute method to return a collection of all the photo items in the media collection (in the world of Windows Media Player 10, photo items are simply .JPG files).

Our photo items come back in the form of an array; to iterate each item in that array we create a For Next loop that runs from 0 (the first item in an array is always numbered 0) to the number of items in the array minus 1 (objPhotos.Count – 1). Inside that For Next loop we bind to each .JPG file using this line of code:

Set objPhoto = objPhotos.item(i)

We then echo the values of the Name, WM/VideoHeight, and WM/VideoWidth attributes. As you probably already guessed, the latter two attributes give us the height and width of each picture.

Incidentally, you can do more with .JPG files than simply retrieve their height and width. For a complete list of photo item attributes check out the Windows Media Player SDK on MSDN.


How Can I Extract Just the File Name from the Full Path to the File?

Hey, Scripting Guy! How can I extract just the file name from the full path to the file?

-- SD

Hey, SD. As you might know, Ted Williams was the last major league baseball player to bat .400 for an entire season. Coming into the final day of the year, Williams was actually hitting something like .3997 which, when rounded up, would give him an official batting average of .400. The manager of the Red Sox offered to let Williams sit out the final doubleheader, enabling him to ensure his place in baseball history. But Ted Williams refused to take the easy way out: not only did he want to play, but he wanted to reach .400 legitimately, not due to some mathematical hocus-pocus like rounding up.

So what happened? Williams went 6-for-8 in the final doubleheader, hit .406 for the season, and punched his ticket to the Baseball Hall of Fame. And all because he refused to take the easy way out. No doubt there's a lesson in there for all of us.

As it turns out, the Scripting Guys faced a similar dilemma when it came time to answer your question. This is actually a fairly easy question to answer; in fact, the solution requires just three lines of code. That's fine, but it doesn't really sound like enough information to build an entire column out of. So, will the Scripting Guys be like Ted Williams and refuse to take the easy way out? Will they go above and beyond the call of duty and write a long and thoughtful column on a more complicated subject? Will they do the right thing?

You've got to be kidding, right? Here's what we came up with instead:

Set
objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.GetFile("C:\Scripts\Test.txt")
Wscript.Echo "File name: " & objFSO.GetFileName(objFile)

So why is this question is so easy to answer? Because the FileSystemObject has several methods for extracting the individual pieces of a file path, including one method – GetFileName – that can grab just the file name for you. To do that we simply create an instance of the Scripting.FileSystemObject, then use the GetFile method to bind to the file in question (in this case, C:\Scripts\Test.txt). After that we call the GetFileName method, passing the object reference to C:\Scripts\Test.txt as the sole parameter to that method:

Wscript.Echo "File name: " & objFSO.GetFileName(objFile)

And what do we get back? We get back the name of the file, just like you were hoping to:

test.txt

The end.

Actually, that's a good point: there's no doubt that Ted Williams would have done more than simply type three lines of code. OK, how about this? Here's a revised script that shows some of the other items that the FileSystemObject can extract from a file path:

Set
objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.GetFile("C:\Scripts\Test.txt")

Wscript.Echo "Absolute path: " &
objFSO.GetAbsolutePathName(objFile)
Wscript.Echo "Parent folder: " &
objFSO.GetParentFolderName(objFile)
Wscript.Echo "File name: " & objFSO.GetFileName(objFile)
Wscript.Echo "Base name: " & objFSO.GetBaseName(objFile)
Wscript.Echo "Extension name: " &
objFSO.GetExtensionName(objFile)

And here's what we get back:

Absolute path: C:\scripts\test.txt
Parent folder: C:\scripts
File name: test.txt
Base name: test
Extension name: txt

Better? Admittedly it's not quite the same thing as going 6-for-8 in a doubleheader. But we like to think of it as being the scripting equivalent.

Incidentally, this approach works with UNC paths as well as with local file paths. Suppose we have a file with the UNC path \\atl-fs-01\Public\Test.txt. If we run GetFileName against that file we'll get back the following:

Absolute path: \\atl-fs-01\public\test.txt
Parent folder: \\atl-fs-01\public
File name: test.txt
Base name: test
Extension name: txt

Nice.

The only catch to all this is that the file must exist; otherwise you'll get a "File not found" error. But suppose you have a series of file paths that don't exist. What do you do then? Give up?

Come on: would Ted Williams give up? Of course not. Instead, he'd write a script like this:

strName = "C:\Scripts\Test.txt"
arrNames = Split(strName, "\")
intIndex = Ubound(arrNames)
Wscript.Echo "File name: " & arrNames(intIndex)

What we're doing here is taking a file path (C:\Scripts\Test.txt) and stashing that path in a variable named strName. We then use the VBScript Split function to chop that file into an array, splitting the path on the \ character:

arrNames = Split(strName, "\")

When we do that we end up with an array consisting of the following elements:

· C:

· Scripts

· Test.txt

Does that help us? You bet it does. As you can see, the very last element in the array just happens to be our file name. To retrieve that value we need to first use the Ubound function to determine the index number of that item:

intIndex = Ubound(arrNames)

And then, with the index number in hand, we can echo back the file name:

Wscript.Echo "File name: " & arrNames(intIndex)

It's a tiny bit more complicated than using the FileSystemObject, and it gets even more complicated if we wanted to tease out, say, the file extension or the parent folder. But if all we want is the file name it's not so bad.

Does that answer your question, SD? If it does, well, we hope you'll remember the Scripting Guys when it comes time to elect inductees to the Scripting Hall of Fame. And if it doesn't, well, maybe you could still vote for us when it comes time to elect inductees to the Scripting Hall of Fame. After all, we're pretty sure that's where Ted Williams would have preferred to be immortalized. Too bad he had to settle for the Baseball Hall of Fame.


How Can I Find All the Files in a Directory Tree that Contain a Specific Word or Phrase?

Hey, Scripting Guy! How can I find all the files in a directory tree that contain a specified phrase?

-- PB

Hey, PB. OK, so picture this: for the first time in a couple of months, the Scripting Guy who writes this column and the Scripting Son are playing basketball. The Scripting Son has a comfortable lead, but the Scripting Guy who writes this column makes a desperate comeback, sinking four three-pointers in a row and tying the score. With the gym about to close for the night, everything has come down to this: next basket wins.

As it turns out, the Scripting Guy who writes this column has the ball. He dribbles down to the baseline and then, for some stupid reason, he stops dribbling. (If you’re not a basketball fan, picking up your dribble while down on the baseline isn’t a particularly good thing to do.) Thanks to his own stupidity, the Scripting Guy who writes this column is now trapped: he has the baseline on one side of him and the Scripting Son, who’s a good 7 inches taller than his father, on the other side. Thinking quickly (or at least as quickly as a Scripting Guy can think) he pivots one way, pivots back the other way, jumps up in the air, and then flips the ball, underhanded and left-handed, beneath the Scripting Son’s outstretched arm. The ball rattles off the backboard and drops through the hoop. Game over!

“That was so lucky,” said the Scripting Son.

Luck?!? Obviously the Scripting Son has no idea what luck is. Making a basket in basketball isn’t lucky. No, lucky would be someone writing in to ask how he or she could locate all the files in a directory tree that contain a specified phrase, and then just happening to have a script lying around that does that very thing.

You know, a script like this one:

On Error Resume Next

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

objRecordSet.Open "SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE Contains('Alice') " & _
    "AND System.ItemPathDisplay LIKE 'C:\Scripts\%'", objConnection

objRecordSet.MoveFirst

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("System.ItemPathDisplay")
    objRecordset.MoveNext
Loop

Before we go any further we should note that this script uses Windows Desktop Search 3.0, a relatively new, fully-scriptable search technology that is included in Windows Vista (and can be downloaded and installed on other versions of Windows). We opted to use this approach for two reasons:

· It’s much easier to access all the folders in a folder tree using Desktop Search than to use WMI or the FileSystemObject. Performing this task using WMI or the FileSystemObject would require writing some crazy recursive subroutine that can get at each and every folder in a folder tree. Despite our reputation, the Scripting Guys don’t like to do crazy things. (Not that we don’t actually do them, mind you. But we don’t like to do them.)

· Windows Desktop Search can open – and search through – all sorts of file types, including Word documents, Excel spreadsheets, PowerPoint presentations, etc. Without using Desktop Search we’d be limited to searching through text files. (OK, in theory we could search other document types, but we’d have to include code that identifies, opens and searches Word documents plus code that identifies, opens and searches Excel spreadsheets plus code that identifies, opens and searches – well, you get the idea.)

Anyway, while there might be other ways we could perform this task, we decided that the easiest way to perform this task was to use Desktop Search. And, for the Scripting Guys at least, the easiest way is almost always the best way.

So how exactly do we use Desktop Search to perform this task? Well, we start out by creating a pair of ADO (ActiveX Data Objects) objects; we’ll use the ADODB.Connection object to connect to the Desktop Search file index, and we’ll use the ADODB.Recordset object as a storehouse for any information returned by our query. After creating the two objects we then use the Connection object’s Open method to bind us to Windows Desktop Search:

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

Note. What all is going on in that line of code? Well, to be honest, we don’t really know for sure. Nor do we really care. That’s because this is simply boilerplate code: use this code exactly as-is any time you want to use Desktop Search and everyone will live happily ever after.

After we open the data store we come to the heart-and-soul of any search script: the part where we specify exactly what it is we’re searching for. In this case, we’re calling the Recordset object’s Open method, passing the method two parameters: the SQL query that contains our search criteria and an object reference to the Connection object (objConnection):

objRecordSet.Open "SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE Contains('Alice') " & _
    "AND System.ItemPathDisplay LIKE 'C:\Scripts\%'", objConnection

As far as we’re concerned, the one part of this command that truly matters is the SQL query itself:

"SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE Contains('Alice') " & _
    "AND System.ItemPathDisplay LIKE 'C:\Scripts\%'"

Let’s see if we can figure out what’s going on here. For our purposes today, the only thing we care about is the file path to any file that contains the word Alice. (By the way, this search is not case-sensitive; the search will locate Alice, alice, ALICE, or any other variation of the word.) When working with Desktop Search you need to specify each and every property value you want returned. Because the property System.ItemPathDisplay contains the file path we start our query out like this:

SELECT System.ItemPathDisplay

The next part of the query (FROM SYSTEMINDEX) is also boilerplate code; Desktop Search only has a single “table” (SYSTEMINDEX) that we can query. That brings us to our WHERE clause, a clause that features a pair of criteria that must both be met for a file to be returned:

· The file must include the word Alice. That’s what the syntax Contains('Alice') is for: it tells the script to read the contents of the file and see if the word Alice can be found anywhere in those contents. (OK, technically Desktop Search has already read through the file; the script doesn’t need to do so. But you get the idea.)

· The file must be located somewhere in the C:\Scripts folder tree. The syntax System.ItemPathDisplay LIKE 'C:\Scripts\%' states that the file path (System.ItemPathDisplay) must begin with C:\Scripts\. The LIKE operator enables us to use a wildcard in our query; that wildcard happens to be the percent sign (%), which serves the same purpose as the asterisk does in, say, the dir command (e.g., dir C:\Scripts\Test.*). In other words, the percent sign causes this clause to be read as follows: “Show me any file where the path starts with C:\Scripts\, regardless of what (if anything) comes after C:\Scripts\.” In turn, thus enables us to locate files found in the C:\Scripts folder (e.g., C:\Scripts\Test.txt) as well as files found in a subfolder of C:\Scripts (e.g., C:\Scripts\Subfolder1\Test.txt).

Much easier than writing a recursive subroutine that accomplishes the same seemingly-trivial feat of accessing all the files in a folder tree.

After executing the query we use the MoveFirst method to move to the first record in the returned recordset. (We’re not 100% sure that this is necessary, but better safe than sorry, right?) We then set up a Do Until loop that runs until we reach the end of the recordset; that is, until the recordset’s EOF (and-of-file) property is True:

Do Until objRecordset.EOF

Inside this loop we don’t do much; we simply echo back the value of the System.ItemPathDisplay property, then call the MoveNext method to move on and repeat the process with the next record in the recordset:

Wscript.Echo objRecordset.Fields.Item("System.ItemPathDisplay")
objRecordset.MoveNext

When all is said and done, we should get back a report similar to this:

C:\Scripts\Scores.xls
C:\Scripts\Alice2.txt
C:\Scripts\Decoded.txt
C:\Scripts\Decrypt.txt
C:\Scripts\New Folder\New Folder\New Text Document.txt

Notice two things here. For one, the first file in the list is actually an Excel spreadsheet file; as we noted earlier, Desktop Search has the ability to search inside Microsoft Office documents. (Among other things, Desktop Search can also search inside Adobe .PDF files.) Also note that the last file in the list is located in a sub-subfolder of C:\Scripts, which means that we really are searching the entire C:\Scripts folder tree.

And that should do it, PB. Like we said, there might be other ways to accomplish this task, but Desktop Search makes the process very fast and very easy. And because Desktop Search has so many other cool capabilities, it’s worth looking into anyway. (For more information, and more sample code, see our article Seek and Ye Shall Find.)

In case any of you are wondering, we should come clean and admit that the Scripting Son was right: the shot his father made to win the game really was pure luck. In fact, the Scripting Guy who writes this column wasn’t even trying to make a basket: he was just hoping that, maybe, he could throw the ball off the backboard and then grab the rebound. No one was more surprised that he was when the ball actually went through the hoop.

But, remember, that’s between us: the Scripting Son doesn’t really need to know how lucky the Scripting Guy who writes this column was. Instead, let him think that – for once – his Dad actually knew what he was doing.

You know, you’re right: there probably isn’t much chance of that happening, is there?

And for good reason.


How Can I Get a List of All the

How Can I Get a List of All the .dll Files in a Folder, Along with Their Version Numbers?

Hey, Scripting Guy! How can I get a list of all the .dll files in a folder, along with their version numbers?

-- DD

Hey, DD. You know, when we sit down to write this column we usually try to start off with an amusing and at least semi-relevant anecdote. In this case, that would mean a cute story about file versions. Unfortunately, none of the Scripting Guys could come up with an amusing anecdote about file versions; instead, the only things we could recall were nightmare scenarios involving mismatched, out-of-date, or incorrect .dll files. Talk about dredging up unpleasant memories: we didn’t think poor Dean would ever stop crying!

In other words, it’s safe to say that having a list of all the .dll files in a folder, along with the version number of each of those files, would be a pretty handy little thing to have. And what better way to retrieve that data than by using a script:

strComputer = "."

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

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

For Each objFile In colFileList
    If objFile.Extension = "dll" Then
        Wscript.Echo objFile.Name & " -- " & objFile.Version
    End If
Next

As you can see, we chose to tackle this problem by using a WMI script. Admittedly, we could have achieved the same results by using the FileSystemObject. We opted to go with WMI for one simple reason: that enables us to run this script against remote computers. (By contrast, the FileSystemObject pretty much limits us to working with the local computer.) Need to get file information off a remote computer named atl-fs-01? No problem; all you have to do is take the preceding script and change line 1 to look like this:

strComputer = "atl-fs-01"

And, yes, that’s exactly why we like WMI so much.

The script itself starts off by connecting to the WMI service on the local computer. We then run into this crazy-looking line of code:

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

It’s not pretty, but this is how you retrieve a collection of all the files in a specified folder (in this case, C:\Windows\System32). Basically what we’re saying here is this: “Bring me back all the CIM_DataFile objects associated with the directory named C:\Windows\System32.” Because CIM_Datafile is the WMI class that represents files, this query – crazy as it might look – simply brings back a collection of all the files found in C:\Windows\System32. What if you wanted a collection of all the files found in D:\Test? In that case you’d modify your query to look like this:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='D:\Test'} Where " _
        & "ResultClass = CIM_DataFile")

If you want to know more about Associators Of queries then check out the WMI SDK on MSDN. Alternatively, don’t worry too much about the nitty-gritty details; just remember that all you have to do is set Win32_Directory.Name to the name of the folder you want to work with.

From here on it’s a piece of cake. Once we have our collection we set up a For Each loop to walk us through each item (each file) in the collection. Inside that loop we use this line of code to check the value of the Extension property:

If objFile.Extension = "dll" Then

If Extension is equal to dll (note that this is just the three letters dll, without a dot; don’t include the dot when specifying the file extension in WMI) we then use this line of code to echo back the Name and Version:

Wscript.Echo objFile.Name & " -- " & objFile.Version

And what if the file extension isn’t equal to dll? No problem; in that case we just loop around and repeat the process with the next item in the collection.

When all is said and done we’ll get back information similar to this:

C:\Scripts>cscript dlls.vbs
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

c:\windows\system32\6to4svc.dll -- 5.1.2600.2180 (xpsp_sp2_rtm.040803-2158)
c:\windows\system32\aaaamon.dll -- 5.1.2600.0 (xpclient.010817-1148)
c:\windows\system32\acctres.dll -- 6.00.2600.0000 (xpclient.010817-1148)
c:\windows\system32\acledit.dll -- 5.1.2600.0 (xpclient.010817-1148)
c:\windows\system32\aclui.dll -- 5.1.2600.2180 (xpsp_sp2_rtm.040803-2158)
c:\windows\system32\activeds.dll -- 5.1.2600.2180 (xpsp_sp2_rtm.040803-2158)

Believe it or not, that’s all it takes. (Boy, could we have used a script like this a few years ago!) Now, we should point out that this script – as requested – only retrieves information about the .dll files found in a single folder; the script does not check for .dll files in any subfolders of that folder. Could we modify the code and get the script to search all the subfolders of the target folder? Sure; as a starting point, take a look at this Hey, Scripting Guy! column, which shows you how to retrieve a list of all the files in a folder and its subfolders. All you need to do is take that script and look for the line of code that echoes back the file name:

Wscript.Echo objFile.Name

Modify that line to echo back the file name and the file version, and you’ll be in business:

Wscript.Echo objFile.Name & " -- " & objFile.Version

Got all that? Great. As for the Scripting Guys, we’re off to buy Dean an ice cream cone; we’re hoping that will cheer him up a bit. Poor little guy ….


How Can I Get a List of All the Files in a Folder and Its Subfolders?

Hey, Scripting Guy! How can I use a script to show me all the files in a folder? And then how can I modify that script so it shows me all the files in any subfolders of that folder?

-- CS

Hey, CS. Yesterday we showed everyone a script that changed all the files in a folder from read-only to read-write. We also promised that, in today’s column, we’d explain how we managed to get a list of all the files in a folder in the first place. Your questions are the perfect lead-in to that explanation.

Let’s start with the easy one: a script that simply lists all the files in a folder. This script reports back the file name of all the files found in the folder C:\Scripts:

Set objFSO = CreateObject("Scripting.FileSystemObject")
objStartFolder = "C:\Scripts"

Set objFolder = objFSO.GetFolder(objStartFolder)

Set colFiles = objFolder.Files
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

As you can see, there really isn’t much to this. We create an instance of the FileSystemObject, and then we use the GetFolder method to bind to folder C:\Scripts. Pretty straightforward. If we wanted to bind to, say, the Windows folder, all we’d have to do is change the path accordingly, something we do by assigning a different value to objStartFolder:

objStartFolder = "C:\Windows"

After we’ve connected to the folder, we then create a reference to the Files property using this command:

Set colFiles = objFolder.Files

That gives us back a collection consisting of all the files found in the folder. (But– and this has important implications for your second question – this collection does not include files found in any subfolders of C:\Scripts.) At this point the rest is child’s play: we can now use a For Each loop to loop through the collection of files and – if we choose – do something to each and every one. Because you asked how to get a list of all the files in a folder, all we do is echo the name of the file. But we could do a lot more than that; for example, we could report back the DateCreated property or the Size property. For a more complete rundown of the FileSystemObject and how to use it, you might want to check out the Script Runtime Primer in the Microsoft Windows 2000 Scripting Guide.

In other words, getting back a list of all the files in a folder is trivial. Getting back a list of all the files in a folder plus all the files in any subfolders of that folder can be a bit trickier. To do that you need to use a recursive script. We’re not going to explain recursion here; for more details, check out this portion of the Microsoft Windows 2000 Scripting Guide. (Yes, we do seem to be promoting the book quite a bit today, don’t we?) Basically a recursive function is a function that can automatically call itself as many times as needed. That might not make much sense, but think of it this way. The script we showed you above lists all the files in a folder and then stops. It doesn’t matter if there are subfolders in the folder; the script doesn’t really care.

A recursive function, by contrast, does care: it will keep working until it has done everything you asked of it, and more. A recursive function will list all the files in a folder, and then check to see if that folder has any subfolders. Suppose it finds subfolders A and B. In that case, the function will call itself, and list any files found in Subfolder A. And what if Subfolder A has a sub-subfolder C? No problem: the function will call itself again, and list all the files in sub-Subfolder C. This will continue until no more subfolders can be found. At that point, the function will loop back, and start working its way through Subfolder B. Furthermore, it will dutifully keep working until it has gone through each and every subfolder and sub-subfolder and sub-sub-subfolder and – well, until every last file has been listed.

It all sounds terribly complicated, and it is. Fortunately, though, VBScript hides most of this complexity from you. Hey, would we kid you about something like that? Look, here’s a script that uses a recursive function – ShowSubFolders – to list all the files in the folder C:\Scripts and all the files found in any subfolders of C:\Scripts:

Set objFSO = CreateObject("Scripting.FileSystemObject")
objStartFolder = "C:\Scripts"

Set objFolder = objFSO.GetFolder(objStartFolder)
Wscript.Echo objFolder.Path
Set colFiles = objFolder.Files
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next
Wscript.Echo

ShowSubfolders objFSO.GetFolder(objStartFolder)

Sub ShowSubFolders(Folder)
    For Each Subfolder in Folder.SubFolders
        Wscript.Echo Subfolder.Path
        Set objFolder = objFSO.GetFolder(Subfolder.Path)
        Set colFiles = objFolder.Files
        For Each objFile in colFiles
            Wscript.Echo objFile.Name
        Next
        Wscript.Echo
        ShowSubFolders Subfolder
    Next
End Sub

As you can see the first part of the script is exactly the same as the one we saw before (with one exception: this one echoes the folder path so we know which folder we’re looking at). After showing all the files in C:\Scripts, it then calls the recursive function ShowSubFolders. This function then starts sifting through all the subfolders and reports back any files found. All this “recursion” happens automatically; all you have to do is just sit back and watch.

If you don’t fully understand what’s happening here, don’t worry about it; it takes awhile to fully grasp how recursive functions work. In the meantime, just copy the preceding script and use it any time you need to do something to all the files in a folder and its subfolders.


How Can I Get a List of All the PDF Files in a Folder and Its Subfolders?

Hey, Scripting Guy! How can I get a list of all the .PDF files (only the file names; I don’t want the file path or file extension) in a folder and its subfolders, and then put that list into a text file?

-- PS

Hey, PS. You know, PS, this is actually a hard question; far harder and far more complicated than it might sound. On top of that, the Scripting Guy who writes this column is on vacation; he doesn’t really have time to be bothered with stuff like this.

Well, OK, technically he’s not really on vacation; technically he’s attending – and working at – TechEd 2007. But, come on PS. The Scripting Guy who writes this column is in Orlando; you know, Disney World, Universal Studios, Sea World. The truth is, there’s no way he’s going to spend his time in Orlando writing scripts that can retrieve a list of all the PDF files in a folder and its subfolders. Sorry, but it’s just not going to happen.

Or at least we didn’t think it would happen. By an astonishing coincidence, however, Epcot Center just opened a brand-new attraction: Dr. Scripto’s Wild Ride. And guess what happens when you ride this new ride? That’s right: you get a queasy feeling in your stomach. However, you also get a script that can retrieve a list of all the PDF files in a folder and its subfolders (it’s kind of a weird ride):

strComputer = "."

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

strFolderName = "C:\Test"

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.CreateTextFile("C:\Scripts\Test.txt")

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

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

For Each objFile in colFiles
    If objFile.Extension = "pdf" Then
        objTextFile.WriteLine objFile.FileName
    End If
Next

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

Sub GetSubFolders(strFolderName)

    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name

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

    For Each objFile in colFiles
        If objFile.Extension = "pdf" Then
            objTextFile.WriteLine objFile.FileName
        End If
    Next

        GetSubFolders strFolderName
    Next
End Sub

We tried to warn you: this is a complicated procedure, far more complicated than it probably should be. That’s because neither WMI nor the FileSystemObject provide a simple, straightforward way to work with folders and subfolders. (This, by the way, is one area where Windows PowerShell represents a big improvement over previous technologies.) Because of that, we need to use a recursive function in order to get at the files in a folder, the files in that folder’s subfolders, and the files in the sub-subfolders of those subfolders. We don’t have time to explain recursion in any detail today. (Did we mention that we’re on vacation?) However, you can get an overview of recursion and recursive functions by taking a peek at the Microsoft Windows 2000 Scripting Guide.

All right, let’s get down to business and try to get through this before the amusement parks close for the day. We start out by connecting to the WMI service on the local computer. (Although by assigning a computer name to the variable strComputer we could just as easily run this script against a remote machine.) We then assign the parent folder name to the variable strFolderName. Because we want to look at the folder C:\Test (as well as all its subfolders) we assign strFolderName the value C:\Test:

strFolderName = "C:\Test"

Makes sense, right? And because this is as good a time as any, we next create an instance of the Scripting.FileSystemObject and use the CreateTextFile method to create a text file named C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.CreateTextFile("C:\Scripts\Test.txt")

That brings us to this line of code:

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

What we’re doing here is using an Associators Of query to get a list of all the subfolders of the folder C:\Test. It might not look like it, but what our query is really saying this: Get me a list of all the items associated with the directory C:\Test, provided that those items are subdirectories (Where AssocClass = Win32_Subdirectory).

When all is said and done this query brings back a list of all the top-level subfolders in C:\Test; for example, C:\Test\Folder1 and C:\Test\Folder2. What it doesn’t bring back are any second-level folders; for example, we won’t get back a folder like C:\Test\Folder1\SubfolderA. To get at these sub-subfolders (that is, subfolders of a subfolder) we need to use a recursive query. That’s what the subroutine GetSubFolders is for. Programmatically, we pass this subroutine the name of each subfolder we find (for example, C:\Test\Folder1 and C:Test\Folder2); in turn, the subroutine queries each of these subfolders for PDF files and for any sub-subfolders. If there are some sub-subfolders, the subroutine will automatically call itself and look for any sub-sub-subfolders.

Confused? To be honest, so are we. But don’t worry about it; just leave the code as-is and give it a try. To search a different folder (that is, a folder other than C:\Test) simply change the value of the variable strFolderName to the name of the folder you want to work with. For example, if you want to search C:\Windows then use this line of code:

strFolderName = "C:\Windows"

Whoa, wait a second; we aren’t done yet. Before we start calling subroutines we need to list any PDF files that happen to live in our parent folder, C:\Test. To do that, we use this line of code to return a collection of all the files (instances of the CIM_DataFile class) found in C:\Test (which is, as you recall, the value we assigned to the variable strFolderName):

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

Next we use this block of code to check each file and see if it has the file extension PDF (note that, in WMI, the dot is not part of the file extension):

For Each objFile in colFiles
    If objFile.Extension = "pdf" Then
        objTextFile.WriteLine objFile.FileName
    End If
Next

If we do have a PDF file then we use the WriteLine method to write the FileName to our text file:

objTextFile.WriteLine objFile.FileName

Needless to say, the FileName property is just the name of the file, without the path or the file extension. For example, if our first file is C:\Test\MyFile.pdf we’ll end up writing the following to the text file:

MyFile

That takes care of all the files in the parent folder, C:\Test. Now, what about files in any subfolders of C:\Test? That’s what this block of code is for:

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

What we’re doing here is looping through the collection of subfolders; for each subfolder in C:\Test we’re calling the recursive function GetSubFolders, passing the folder path as the sole parameter:

GetSubFolders strFolderName

This is difficult to visualize, but GetSubfolders is going to do the exact same thing we just finished doing. Let’s say that our first subfolder is C:\Test\Folder1. The subroutine is going to start out by looking for all the PDF files in C:\Test\Folder1; if it finds any it’s going to write the names of those files to the text file.

After that, GetSubFolders is going to run a query and see if C:\Test\Folder1 has any subfolders of its own. Suppose it does; what happens then? Well, in that case the subroutine is going to call itself, this time passing the name of each sub-subfolder as the subroutine parameter. Like we said, this can give you a headache just thinking about it. But suppose we have a sub-subfolder named C:\Test\Folder1\SubfolderA. The subroutine will call itself and then look for PDF files in this sub-subfolder; after that, it will check to see in SubfolderA has any sub-sub-subfolders, and then, if necessary, the subroutine will call itself yet again. This will continue until its reached the end of the line and run out of folders.

Crazy, huh? Fortunately, VBScript does all the hard work here. All you need to do is call the recursive function (which, now that we think about it, is really a recursive subroutine, isn’t it?). VBScript will take care of the rest, keeping track of which folders you’ve looked at and which folders you haven’t looked at. Amazingly enough, by the time it finishes, this script will have looked at every single folder and subfolder in C:\Test (even if these subfolders go several levels deep) and will have written the names of any and all PDF files to the text file C:\Scripts\Test.txt. If you don’t believe us (and we don’t blame you for being skeptical) give it a try and see for yourself.

Before you start making vacation plans we should add that we were just kidding: Epcot Center does not have a new attraction called Dr. Scripto’s Wild Ride. Based on today’s column, that’s probably just as well: we doubt that even Disney could afford the liability insurance for a ride this wild.

Note. By the time most people read this, TechEd 2007 will be over and the Scripting Guys will be headed for the airport. But just because Teched is over that doesn’t mean the fun is over; be sure and check out our TechEd 2007 page for new articles, TechEd updates, and your chance to win a Dr. Scripto bobblehead doll. With TechEd 2007 the fun never ends!


How Can I Get the “Date Picture Taken” Property From a .JPG File?

Hey, Scripting Guy! How can I get the Date Picture Taken property from a .JPG file?

-- MW

Hey, MW. Before we answer your question we should point out that this is likely to be the last Hey, Scripting Guy! column for quite some time to come. Not that we’re going anywhere, mind you. It’s just that on Saturday, the Seattle Seahawks will host the Dallas Cowboys in a nationally-televised football game. And that’s a problem. Early in December the Seahawks hosted Green Bay in a nationally-televised game and the Seattle area was promptly hit with a freak ice storm that shut the city down for two days. Shortly before Christmas the Seahawks hosted the San Francisco 49ers in a nationally-televised game. That same evening a “once-in-a-century” windstorm ripped through the region, and, among other things, the Scripting Guy who writes this column lost electricity for 6 long, cold days. Granted, the odds suggest that nothing that bad could possibly happen again. Nevertheless, the Scripting Guy who writes this column has been building an ark in his back yard, just in case. All he has to do now is round up two of every kind of animal and he’ll be in business.

Of course, as long as we have a little time before the next disaster strikes we might as well see if we can find a solution for you. Admittedly, a month or so ago we could have answered this question without any problem: “Sorry, MW, but there’s no way to get at that information, at least not by using a script.” However, that was before Microsoft released Windows Desktop Search 3.0. As it turns out, now there is a way to use a script to determine the date a digital photo was taken:

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

objRecordSet.Open "SELECT System.FileName, System.Photo.DateTaken FROM SYSTEMINDEX " & _
    "Where System.ItemFolderPathDisplay = 'D:\Europe' and System.FileExtension = '.jpg'", _
    objConnection

objRecordSet.MoveFirst

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("System.FileName"), _
        objRecordset.Fields.Item("System.Photo.DateTaken")
    objRecordset.MoveNext
Loop

A few caveats before we explain how this script works. First and foremost, this solution does require you to install Windows Desktop Search 3.0; you can get more information on how to do that here. But that’s not a bad thing: setup is quick and easy, and Desktop Search 3.0 has all sorts of cool uses beyond determining the date a digital photo was taken. Second, this solution works only on Windows XP, Windows Server 2003, and Windows Vista; that’s because Desktop Search 3.0 is only available on those platforms. Will that change in the future? To be honest, we don’t know. But we hope so, because this really is a nice little technology.

As for the script itself, we start out by creating two objects: ADODB.Connection and ADODB.Recordset. If these names ring a bell, well, that’s not surprising: these are the two fundamental objects used with ActiveX Data Objects (ADO), the scripting technology that lets you connect to and work with such disparate data sources as text files, Active Directory, and, of course, databases. That’s one of the things we really like about Desktop Search 3.0: if you know how to write ADO scripts then you pretty much know how to write Desktop Search scripts as well.

After creating the Connection and Recordset objects we use the Open method to make a connection to the Desktop Search data store. Don’t worry about the details of this code; just consider it boilerplate text that you can paste in any time you want to use Desktop Search 3.0:

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

Once we make the connection we then use the following code to return the FileName and DateTaken properties for all the .JPG files found in the folder D:\Europe:

objRecordSet.Open "SELECT System.FileName, System.Photo.DateTaken FROM SYSTEMINDEX " & _
    "Where System.ItemFolderPathDisplay = 'D:\Europe' and System.FileExtension = '.jpg'", _
    objConnection

And, sure, we could easily return this information for a single file, or for all the .JPG files on the computer. (Wouldn’t that script take forever to run? Obviously you haven’t tried Desktop Search 3.0, have you?) For more information on writing queries for Desktop Search 3.0 see our article Seek and Ye Shall Find. For now, simply note that we’re using a basic SQL statement that retrieves two property values (System.FileName and System.Photo.DateTaken) from the SYSTEMINDEX data store (the only data store available when using Desktop Search). Likewise, note the Where clause, which limits returned data to items found in the folder D:\Europe (that is, items where the ItemFolderPathDisplay is equal to D:\Europe) and have a FileExtension equal to .jpg.

And yes, unlike WMI you do need to include the dot as part of the file extension.

When we execute the Open method Desktop Search returns a recordset containing all the .JPG files found in the folder D:\Europe (just like we asked it to). All we have to do at that point is set up a Do Until loop that loops through that recordset (that is, that runs until the recordset’s EOF – End-of-File – property is True). Inside that loop we simply echo back the file name and the date the picture was taken; we then use the MoveNext method to move to the next item in the recordset and echo back that same information. That block of code looks like this:

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("System.FileName"), _
        objRecordset.Fields.Item("System.Photo.DateTaken")
    objRecordset.MoveNext
Loop

And that’s all we have to do. When we run the script, we should get back information similar to this:

europe001.jpg 8/16/2005 11:22:30 AM
europe002.jpg 8/16/2005 11:22:49 AM
europe003.jpg 8/16/2005 11:23:02 AM
europe004.jpg 8/16/2005 11:24:41 AM
europe005.jpg 8/16/2005 11:25:14 AM

Best of all, we get this information back in just a few seconds. Very cool.

So are the Scripting Guys getting nervous as we get closer and closer to game time? No, not really. After all, Microsoft would never let anything happen to its most-valued employees; therefore, we simply plan on watching the game with those guys.

Note. On the bright side, come hell or high water (neither of which would surprise us at this point) this will be the last home game for the Seahawks this year. Thank goodness they were so mediocre this season!


How Can I Get the Names of 10 Randomly-Selected Files in a Folder?

Hey, Scripting Guy! How can I get the names of 10 randomly-selected files in a folder? I need to do that so I can test and verify our backup and restore policies.

-- CS

Hey, CS. You know, if you’re interested in things that are random, haphazard, arbitrary, or downright chaotic, well, you came to the right place. For example, any time we Scripting Guys have a contest we select the winners via random drawing. When it comes time to write a new Hey, Scripting Guy! column we randomly choose a question to answer. Remember the Butterfly Effect, the idea that a random butterfly flapping its wings in Brazil could be responsible for a blizzard in New England? That was our butterfly!

Note to the people of New England. The Scripting Guys apologize for any inconvenience the blizzard might have caused. We still can’t figure out how that butterfly got out of its jar.

Now, admittedly, making it snow in New England isn’t all that big of a deal; it’s kind of like bragging that you can make it rain in Seattle. So can we do something really challenging, something like randomly grab file names from a folder? Surprisingly enough, it turns out that we can:

strComputer = "."

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

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

intFiles = colFiles.Count

Set objDictionary = CreateObject("Scripting.Dictionary")

intHighNumber = intFiles
intLowNumber = 1

For i = 1 to 10
    x = 0
    Do Until x = 1
        Randomize
        intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)
        If objDictionary.Exists(intNumber) Then
            x = 0
        Else
            objDictionary.Add intNumber, intNumber
            x = 1
        End If
    Loop
Next

i = 1

For Each objFile in colFiles
    If objDictionary.Exists(i) Then
        Wscript.Echo objFile.Name
    End If
    i = i + 1
Next

Yes, you’re right: this script is a bit crazy-looking, at least at first glance. So let’s see what we can do to clear up the confusion a little.

We actually start off in straightforward fashion: we simply connect to the WMI service on the local computer (although we could just as easily connect to the WMI service on a remote computer). We then use this line of code to retrieve a collection of all the files found in the folder C:\Scripts:

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

Once we have that collection in tow we then use the value of the Count property to determine the total number of files in that folder, a value we store in a variable named intFiles:

intFiles = colFiles.Count

Why do we need to know the total number of files in the folder? Hang on; we’ll explain that in just a moment. Finally, we create an instance of the Scripting.Dictionary object:

Set objDictionary = CreateObject("Scripting.Dictionary")

Why do we do that? Be patient; sooner or later this will all make sense.

Or so we hope.

We’re now ready to start generating 10 random numbers, numbers we’ll use to indicate the files that we want to grab from the folder. To do that, our first step is to assign values to a pair of variables:

intHighNumber = intFiles
intLowNumber = 1

As you can see, we assign the number of files in the folder (the value of intFiles) to intHighNumber and the value 1 to the variable intLowNumber. Why? Well, we need to pick a random number, and that random number needs to fall between two values: 1 and the total number of files in the folder. For example, if we have 39 files in the folder we need to randomly pick a number between 1 and 39; in a case like that it doesn’t do us any good to pick file number -83 or file number 765. After all, how could we randomly select file 765 if there are only 39 files to begin with?

Note. Yes, we could have used the variable intFiles in the equation, and we could have hard-coded the value 1 into the equation. We used the variables intHighNumber and intLowNumber to help make it clear what those values represent.

That brings us to this big, bad For Next loop:

For i = 1 to 10
    x = 0
    Do Until x = 1
        Randomize
        intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)
        If objDictionary.Exists(intNumber) Then
            x = 0
        Else
            objDictionary.Add intNumber, intNumber
            x = 1
        End If
    Loop
Next

What we’re doing here is looping around 10 times; each time through the loop we’ll generate a new random number. Inside the loop the very first thing we do is set the value of a counter variable named x to 0 (and, yes, we’ll explain why in just a second). We then set up a Do Until loop that runs into x = 1. (Don’t panic; we’re getting to that, too.)

Inside the Do Until loop we use these two lines of code to generate a random number between 1 and the total number of files in the folder:

Randomize
intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

Note. We’re not going to explain the random number process in any detail today; for more information, see this not-so-randomly selected Hey, Scripting Guy! column.

We now have a random number (let’s say it’s 13). Because we want to randomly select 10 files, that means we need to generate 10 random numbers. That also means we need to temporarily store our random numbers until we have all 10 of them. We decided that the easiest way to do that is to store all the random numbers in an instance of the Dictionary object.

Of course, there is a catch here: items in the Dictionary must be unique (we can’t store the number 13 multiple times). Because of that we use the Exists method to see if the random number we just generated is already in the Dictionary:

If objDictionary.Exists(intNumber) Then

Let’s say that it is. That means we can’t add the number to the Dictionary again; remember, no duplicate items. Therefore, we set the value of x to 0; because x is 0 that means our Do Until loop will simply run again, and continue to run until we generate a random number that isn’t in the Dictionary. As soon as we find such a number we execute these two lines of code:

objDictionary.Add intNumber, intNumber
x = 1

The first line simply adds the randomly-generated number to the Dictionary (using the number itself as both the Dictionary key and item). After that we set the value of x to 1. Very good; that’s exactly why we do that. As you figured out, our Do Until loop is designed to run until x equals 1. Now that x does equal 1, we’ll break out of the loop and return to the next iteration of the For Next loop. Make sense? Good. This process continues until we have 10 items (numbers) in the Dictionary.

That leaves us with this block of code, which is where we actually grab the 10 file names:

i = 1

For Each objFile in colFiles
    If objDictionary.Exists(i) Then
        Wscript.Echo objFile.Name
    End If
    i = i + 1
Next

The first thing we do here is set the value of the counter variable i to 1; we’ll use this variable to track which file (i.e., file 1, file 2, file 3, etc.) we’re dealing with. (We set i equal to 1 simply because we’ll be starting with the first file in the collection.) After setting up a For Each loop to walk through the collection of files we then encounter this line of code:

If objDictionary.Exists(i) Then

All we’re doing here is checking to see if the value of i (which, the first time through the loop, is equal to 1) is in the Dictionary. What if it is? Well, then that can only mean one thing: 1 is one of our randomly-generated numbers. In turn, we echo back the Name of the first file in the collection. If number 1 is not in the Dictionary then we don’t do anything at all. We only echo back the names of the files whose file number can be found in the Dictionary.

Regardless of whether or not the value exists in the Dictionary we increment i by 1 (making i equal to 2), loop around, and check to see if the value 2 is in the Dictionary. This continues until we’ve walked through the entire collection of files. When all is said and done, we should have a list of 10 randomly-selected file names:

c:\scripts\book1.xls
c:\scripts\employees.log
c:\scripts\imapi.vbs
c:\scripts\read-write.vbs
c:\scripts\tee.txt
c:\scripts\test.csv
c:\scripts\test.txt
c:\scripts\test.txt.doc
c:\scripts\test.xml
c:\scripts\x.txt

Cool.

Hey, that’s OK, CS: no need to thank us. Like we said, we enjoy experimenting with randomness and with Chaos Theory. For example, we recently tried to determine, once and for all, whether a million monkeys typing on a million computers could produce the complete works of Shakespeare. As it turned out, they couldn’t. But at least we did get the latest Sesame Script column out of the deal. Thanks, guys!


How Can I Identify the Last File in a Sequential List of Files?

Hey, Scripting Guy! I have a folder which contains thousands of files, all numbered sequentially. How can I tell which file is the last file in the list?

-- AC

Hey, AC. You know, one of the fun things about being a Scripting Guy is that we get to spend a lot of time shattering myths. For example, when we first started at Microsoft there was a myth that system administrators had no interest in (or aptitude for) scripting and automation. Shattered! Likewise it was a well-accepted “fact” that WMI was way too difficult for script writers to learn. Shattered! Today we tackle yet another well-entrenched myth: if you want to do something right then you should work hard while doing it.

Need we say it? Shattered!

Note. Technically, the Scripting Guys have nothing against hard work. As long as we don’t have to any of it, well, what difference does it make to us?

As you noted, AC, you have a folder that contains thousands of files, each having a name similar to this:

200500001.pdf
200500002.pdf
200500003.pdf
200500004.pdf
...
200509999.pdf

In the example above, file 200509999.pdf is the last file in the sequential list. The question is, how do you know that? How can you write a script that can identify the last file in a sequential list of files?

As you might expect, the Scripting Guy who writes this column took one look at this question and immediately came up with a solution. Of course, it was a complicated and convoluted solution, and our hero knew it would take some real effort to create the script and then write the column that explained how the script worked. But was the prospect of a little hard work enough to scare off the Scripting Guy who writes this column? You better believe it was.

In fact, instead of rolling up his sleeves and getting down to work, the Scripting Guy who writes this column found all sorts of other things to do, things which he hadn’t thought about in months but which suddenly became high-priority, must-do items. But here’s the moral of the story: being lazy turned out to be a good thing. Had he been the earnest, hard-working type, the Scripting Guy who writes this column would have come up with a script that would have been far more complicated than it needed to be. By contrast, shirking his responsibilities for awhile gave him time to realize that this problem could be solved in far-easier fashion, and without resorting to arrays, bubble sorts, and several eyes of newt, all of which were required in his initial solution. Being lazy turned into a win-win situation: the Scripting Guy who writes this column didn’t have to work very hard, and you ended up with a script that’s short, sweet, and to the point.

In other words, a script that looks like this:

strComputer = "."

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

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

For Each objFile In colFileList
    strFile = objFile.FileName
Next

Wscript.Echo strFile

So how did our hero almost turn such a simple script into a nightmarish morass of code? Well, the problem was that he thought about it too hard. He knew that, in order to determine which file comes last in the list he would need to sort the files by file name. He also knew that once the files were sorted he had to be able to identify the very last file in the alphabetical list. Determined to make a mountain out of a molehill, he immediately envisioned a solution involving arrays, disconnected recordsets, and an army of highly-trained St. Bernards, none of which were actually needed.

So why weren’t they needed? Two reasons. First, there’s no need to sort the files by file name; if you use WMI to retrieve the files they will, by default, already come sorted by file name. Had he acted on his first impulse, the Scripting Guy who writes this column would have written some fancy-schmancy code whose sole purpose was to sort a collection that was already sorted. That seemed a tad bit silly, even for a Scripting Guy.

Second, it’s true that we need to identify the last file in the collection. And, admittedly, we could do that by storing all the files in an array, using the Ubound function to identify the last item in that array, and then echoing back the value of that last item. But, again, that’s way more work than we need to put in. We already have a collection of pre-sorted files; instead of adding all those files in an array, why not just quickly rifle through the collection? As soon as we run out of files we’ll know which one is the last file in the list: that’ll be the file we just finished looking at.

And so that’s exactly what we do. This far-simpler script begins by connecting to the WMI service on the local computer (although it could just as easily perform this same task on a remote computer). We then use this crazy-looking Associators of query to return a collection of all the files in the folder C:\PDFs:

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

Note that we’re assuming that all the files in this folder have been named using the sequential-numbering scheme. If there are other files in this folder you’ll need to modify your For Each loop to discard files that aren’t part of the target collection.

Speaking of For Each loops, that’s our next step: we set up a For Each loop to walk through the collection of files, files – we might add – that are already sorted for us by file name (first 200500001.pdf, then 200500002.pdf, etc.). Notice that, inside the loop, we only do one thing: we assign the FileName of the current file to a variable named strFile:

strFile = objFile.FileName

Why do we do that? Well, let’s say we have three files in the collection. The first time through the loop strFile will be assigned the name of the first file in the collection (2000500001.pdf). The second time through the loop, strFile gets assigned the name of the second file in the collection (200500002.pdf). And – oh, you’re way ahead of us, aren’t you? Yes, the third and final time through the loop strFile gets assigned the name of the third file in the collection. At that point we exit the loop, with strFile equal to 200500003.pdf.

Believe it or not that is a big deal. We’re looking for the name of the last file in the sequence, right? Well, guess what: we just found it. The last file we looked at is also the last file in the sequence; to get our answer all we have to do is echo back the value of strFile:

Wscript.Echo strFile

It’s not fancy, but it does the trick, and – best of all – it does it with a minimum amount of effort. And it’s reasonably fast, too: we tried it with 5,000 files in a folder and the script completed its work in less than 15 seconds. That was good enough for us.

Of course, we should point out that this script works only because your naming system includes leading zeroes in the file names. Unfortunately, this script will not work if you have file names similar to this:

20056.pdf
20057.pdf
20058.pdf
20059.pdf
200510.pdf

Why not? Because these names are going to be sorted alphabetically and not numerically, which means they’ll be sorted like this:

200510.pdf
20056.pdf
20057.pdf
20058.pdf
20059.pdf

Unfortunately, the script will identify file 20059.pdf as the last file in the sequence, which is definitely not the case. But we shouldn’t blame the script; type dir c:\pdfs and the files will be sorted in the same fashion: alphabetic rather than numeric.

So is it possible to deal with file names that don’t include leading zeroes? Yes, although that would require quite a bit of effort, and you already know how the Scripting Guys feel about hard work and effort. But if that’s something you really need to know, drop us a line and we’ll see what we can do. We’ll start resting up, just in case.


How Can I Add a COUNTIF Formula to an Excel Spreadsheet?

Hey, Scripting Guy! How can I add a COUNTIF formula to an Excel spreadsheet?

-- MF

Hey, MF. Thanks for reminding us: yes, we are getting closer and closer to the actual 500th Hey, Scripting Guy! Celebration .

Editor’s Note. There was a bunch of stuff here about the celebration, but it’s over now so we took it out. That should explain the small gap in content here. Any other gaps in content are purely coincidental.

Anyway, thanks again, MF; we almost forgot to mention all that. As a token of our appreciation, we’d like to give you a little something in return. Obviously you would never take our money … hey, how about this: how about a script that adds a COUNTIF formula to an Excel spreadsheet? It’s not much, but, then again, it’s the thought that counts. Right?

As you noted in your email, MF, you tried adding a COUNTIF formula (a formula that simply counts the number of cells in a range whose value begins with the letters NK) using this line of code:

objExcel.Cells(6, 1).Formula = "=COUNTIF(A1:A4,'NK*')"

That looks pretty good, but - as you found out - it doesn’t work. You’d like to know two things: why doesn’t this work, and what can you do to make it work. Can we help you with those questions? Hey, does a bear swim in the woods?

Note. Actually, no, we have no idea what we would do if someone told us that, from now on, these columns had to start making sense.

Let’s start off by explaining why your code doesn’t work. To begin with, take a look at the COUNTIF formula as typed directly into Excel:

If you look closely, you’ll notice that the criteria (NK) is surrounded by double quotes: "NK". Are those double quotes important? You bet they are; if we replace the double quotes with single quotes we no longer have a formula:

As you found out.

Of course, when you first sat down to write your script you probably tried inserting double quotes, most likely by using code similar to this:

objExcel.Cells(6, 1).Formula = "=COUNTIF(A1:A4,"NK*")"

There’s probably no need to tell you that that’s going to generate an “Expected end of statement” error. Why? Well, the double quote mark is used to indicate the beginning and the end of string values. Because of that, VBScript thinks that your string value is this:

"=COUNTIF(A1:A4,"

That’s fine; it’s perfectly valid VBScript code. However, that valid string value is immediately followed by this:

NK*")"

Does that look like gibberish to you? Well, unfortunately, it looks like gibberish to VBScript as well; that’s why the script blows up. One of the many immutable laws of the universe is this: you can’t embed double quotes inside double quotes.

At least not if you want your script to actually run.

To your credit, though, we have to admit that it wasn’t a bad idea to try replacing the double quotes in the formula with single quotes; sometimes (especially when you’re working with HTML tags) you can get away with that. For the most part, though, that won’t work: if something expects to see double quotes, then you have no choice but to give it double quotes.

But didn’t we just that you can’t embed double quotes inside double quotes? Probably; that sounds like something we’d say. But that’s OK; we aren’t going to embed double quotes inside double quotes. Instead, we’re going to use a little workaround like this:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objExcel.Cells(1, 1).Value = "nkabc"
objExcel.Cells(2, 1).Value = "abcnk"
objExcel.Cells(3, 1).Value = "abnkc"
objExcel.Cells(4, 1).Value = "nk"

strFormula = "=COUNTIF(A1:A4," &
Chr(34) & "NK*" & Chr(34) & ")"
objExcel.Cells(6, 1).Formula = strFormula

Two quick notes before we explain how this script works. First, there are at least two ways to get around the fact that you can’t embed double quotes inside double quotes. We’ve chosen the method that we believe is easier for most people. If you prefer a different method, that’s fine.

Second, we actually added an extra step to our script. Where? Right near the end, where we stored our COUNTIF formula in a variable and then assigned that variable to a specific cell in the spreadsheet. We could have saved ourselves a step by assigning the formula directly to the cell, without ever storing that formula in a variable. We didn’t use that shortcut simply because we thought it was a little easier for people to read and understand the script if we broke that task into two steps.

But enough about that. We’ve now reached the point where we explain how this script works, something we’ll do by first telling you that we aren’t going to explain how this script works. Or at least not all of it. As it turns out, 80% of the script has nothing to do with inserting a formula; instead, it’s code that simply creates an Excel spreadsheet and adds some test values to four of the cells. We won’t explain those lines of code in any detail; if you aren’t familiar with writing scripts that use Microsoft Excel then you might want to take a look at the articles in the Office Space Archive. (In fact, we even have one article in there that talks about inserting formulas into an Excel spreadsheet.)

To make a long story short, we start off by creating a visible instance of Microsoft Excel, and then creating a new workbook. After binding to sheet 1 in that workbook we then use these four lines of code to add four values (two of which start with the letters NK and two of which don’t) into cells A1 through A4.

objExcel.Cells(1, 1).Value = "nkabc"
objExcel.Cells(2, 1).Value = "abcnk"
objExcel.Cells(3, 1).Value = "abnkc"
objExcel.Cells(4, 1).Value = "nk"

That leaves us with only two lines of code to worry about:

strFormula = "=COUNTIF(A1:A4," & Chr(34) & "NK*" & Chr(34) & ")"
objExcel.Cells(6, 1).Formula = strFormula

The first line is the important one: that’s the line where we construct the COUNTIF formula. To do that we need to build the formula in pieces. As you might recall, the finished product needs to look like this:

"=COUNTIF(A1:A4,"NK*")"

To get to that point, we start with this string:

"=COUNTIF(A1:A4,"

Note that this is a valid string: it begins and ends with double quote marks. We then need to insert a double quote mark; to do that we use the Chr function, which converts an ASCII value to an actual character. As it turns out, the ASCII value for the double quote mark is 34; that means that this piece of code takes the beginning portion of the string and then adds a double quote mark at the end:

"=COUNTIF(A1:A4," & Chr(34)

Got that? Good. Next up: NK*. No problem: we just put those characters in a string and add that to the formula:

"=COUNTIF(A1:A4," & Chr(34) & "NK*"

Make sense? Now we need a second double quote mark, which means it’s time for another Chr(34):

"=COUNTIF(A1:A4," & Chr(34) & "NK*" & Chr(34)

All that’s left now is the closing parenthesis, which we can add as a little string value all its own:

"=COUNTIF(A1:A4," & Chr(34) & "NK*" & Chr(34) & ")"

Admittedly, this can be a bit bewildering if you haven’t done much string concatenation. If that’s the case, then you can do two things that might help. First, take a look at the relevant section in the Microsoft Windows 2000 Scripting Guide. Second, think of this in terms of an addition problem:

"=COUNTIF(A1:A4,"
+                Chr(34)
+                  "NK*"
+                Chr(34)
+                    ")"
________________________

  "=COUNTIF(A1:A4,"NK*")"

To turn this into a line of code just replace the plus signs with ampersands.

Fortunately all that’s left now is to assign the formula to cell A6:

objExcel.Cells(6, 1).Formula = strFormula

That should do it, MF. Again, thanks for your help.

Editor’s Note. More stuff about the celebration was here. It’s gone now.


How Can I List All the Files in a Folder, Ordered By Creation Date?

Hey, Scripting Guy! How can I list all the files in a folder, ordered by creation date?

-- CL

Hey, CL. You know, if we were ambitious and hard-working, we’d sit down and write you a script that uses WMI to return all the files in a folder. That script would grab information about all those files and store that data in a disconnected recordset. We’d then set the sort order on that recordset to arrange the files by creation date and time. (Well, after we went through a lot of gyrations to convert WMI’s date-time values to a readable date-time format.) Finally we’d echo all the values in the recordset back to the screen. It would take a lot of time and involve a lot of coding, but in the end you’d have your sorted list of files and everyone would say, “Wow, those Scripting Guys really go the extra mile for their readers, don’t they?”

As it turns out, though, we aren’t ambitious and hard-working; instead, we’re the Scripting Guys. And as the Scripting Guys we’re always looking for the quickest and easiest way to solve a problem. Therefore, instead of writing a long and complicated script we grabbed a copy of Log Parser 2.2 and dashed off just a few lines of code:

Set objLogParser = CreateObject("MSUtil.LogQuery")
Set objInputFormat = CreateObject("MSUtil.LogQuery.FileSystemInputFormat")
objInputFormat.Recurse = 0

Set objOutputFormat = CreateObject("MSUtil.LogQuery.NativeOutputFormat")
objOutputFormat.rtp = -1

strQuery = "SELECT Name, CreationTime FROM 'C:\Scripts\*.*' " & _
    "WHERE NOT Attributes LIKE '%D%' ORDER BY CreationTime"
objLogParser.ExecuteBatch strQuery, objInputFormat, objOutputFormat

And guess what? Not only does this script work, but it returns the list of files – sorted by creation date and time – in practically no time whatsoever. Very cool.

Admittedly, we usually don’t recommend solutions that aren’t built into the operating system; that’s because we hate to make people download and install something if it’s not absolutely necessary. When it comes to enumerating files, however, it’s more than worth your while to download and install Log Parser; any time you have to grab information about a set of files you’ll find Log Parser way better than either WMI or the FileSystemObject. Seven or eight lines of Log Parser code versus 60 or 70 lines of WMI code? We’ll leave that decision up to you.

We won’t take time to explain everything there is to know about Log Parser; for more information, you might want to see the Tales from the Script column All You Need is Log (Well, Log Parser). For now, we’ll just note that the script begins by creating an instance of the Log Parser object, better known by the catchy name MSUtil.LogQuery. We then create two additional objects, the first specifying the object we’re working with (in this case, the file system, though we could also work with event logs, Active Directory, the registry, and other items), the second indicating the output type we want to use (in this sample script, all we’re doing is writing data to the command window). These two lines of code create the input object and tell Log Parser not to retrieve files from any subfolders:

Set objInputFormat = CreateObject("MSUtil.LogQuery.FileSystemInputFormat")
objInputFormat.Recurse = 0

What if we did want to retrieve values for any and all subfolders? In that case all we’d have to do is set the value of the Recurse property to -1:

objInputFormat.Recurse = -1

Meanwhile, these two lines of code create the output object, and tell Log Parser to display all the data without pausing:

Set objOutputFormat = CreateObject("MSUtil.LogQuery.NativeOutputFormat")
objOutputFormat.rtp = -1

Alternatively, we could have told Log Parser to show us 10 rows of data, pause until we pressed a key on the keyboard, then show us the next 10 rows of data. To show data in batches of 10 all we’d have to do is set the value of the rtp property to 10:

objOutputFormat.rtp = 10

Next we configure the SQL query for retrieving file information. If you have a little knowledge about SQL this query should be relatively easy to parse; as you can see, we’re just asking for the Name and CreationTime of all the files in C:\Scripts. In addition, we want the returned data ordered by creation date and time, with the oldest files listed first:

strQuery = "SELECT Name, CreationTime FROM 'C:\Scripts\*.*' " & _
    "WHERE NOT Attributes LIKE '%D%' ORDER BY CreationTime DESC"

The only unusual thing in this query is our WHERE clause: WHERE NOT Attributes LIKE '%D%'. Without going into a long explanation, this clause filters out folders, and thus returns only files. A file system object that contains the Directory attribute is a folder; because we don’t want folders we use the WHERE NOT syntax to eliminate any object possessing the Directory (%D% for short) attribute.

Finally we call the ExecuteBatch method to run the query and write the returned data to the command window. And, in a second or two, we get back something that looks like this:

And, no, we didn’t have to enter any special commands to get this nice tabular output; Log Parser takes care of all that for us. That is really nice of it, isn’t it?

Granted, we didn’t have to work very hard to get back these results. But it’s quick and it’s easy. And look at it this way: nobody has to know we didn’t work very hard, do they?


How Can I List the Files in a Folder and All Its Subfolders?

Hey, Scripting Guy! How can I list all of the files in a folder, as well as all the files in any subfolders of that folder?

-- MA

Hey, MA. This is a question we get asked quite a bit, and one which have avoided answering up till now. That’s because there is no nice, simple answer to this one: a script that can carry out this task is bound to be a little bit confusing, and isn’t going to lend itself to the easy-to-explain approach we like to take with this column. On the other hand, the customer is always right: if you guys want a script that can list all the files in a folder, as well as all the files in any subfolders of that folder, well, who are we to argue?

Before we begin there are two issues we have to grapple with here. First, we need to pick a scripting technology. WMI, the FileSystemObject, and the Shell object are all capable of listing the files in a folder, as well as listing the subfolders in a folder. However, none of these technologies have a mechanism to automatically list the files in those subfolders (not to mention any sub-subfolders that might be found there). Any of these technologies will do the trick, but none will do it very easily.

We opted to go with WMI. The resulting script is perhaps a little more complicated than a similar script using the FileSystemObject or the Shell object, but a WMI script that can retrieve this kind of information on the local computer can just as easily retrieve this kind of information from a remote computer. That’s not the case with either the FileSystemObject or the Shell object. We voted for the flexibility of WMI.

Second, we noted that none of the scripting technologies has a built-in way to iterate through a folder, list the file names, and then automatically iterate through any and all subfolders and list the files found there. Because of that, we need to use a recursive function to perform this task. Explaining recursion is a bit beyond what we can do in this column; for a brief explanation, check out the Microsoft Windows 2000 Scripting Guide. Suffice to say that we’re creating a function that can call itself as many times as needed. In other words, if we have a function that accesses a folder and lists all its files, that function can call itself to access a subfolder and list all the files found there. And then call itself again to access a sub-subfolder. It’s hard to visualize, but it works.

There’s another complication, too, but we’ll deal with that in a minute. For now, let’s take a look at a script that lists a folder and any and all of its subfolders (although this first sample doesn’t list any files found in those folders):

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

strFolderName = "c:\scripts"

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

Sub GetSubFolders(strFolderName)
    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name
        Wscript.Echo objFolder2.Name
        GetSubFolders strFolderName
    Next
End Sub

What we’re doing here is using an AssociatorsOf query to get a list of all the subfolders of the folder C:\Scripts. Basically our query says this: Get me a list of all the items associated with the directory C:\Scripts, provided those items are subdirectories (Where AssocClass = Win32_Subdirectory).

This gets us a list of all the top-level subfolders: for example, C:\Scripts\Folder1 and C:\Scripts\Folder2. What it doesn’t get us are any second-level folders; this query does not return a folder like C:\Scripts\Folder1\SubfolderA. To get at these sub-subfolders (subfolders of a subfolder) we need to use a recursive query. That’s what the subroutine GetSubFolders does. We pass it – one-by-one – the name of each subfolder we find (like C:\Scripts\Folder1 and C:\Scripts\Folder2) and let it query each of those subfolders for any sub-subfolders. If there are any sub-subfolders, the function will automatically call itself and look for any sub-sub-subfolders.

Confused? Don’t feel bad; you aren’t the only one. But don’t worry about it; just leave the code as-is and give it a try. To search a different folder (that is, one other than C:\Scripts) simply change the value of the variable containing the folder you want to search. For example, if you want to search C:\Windows, then use this line of code:

strFolderName = "c:\windows"

Now, what about the files found in all these folders? Well, this is where things really get complicated. That’s because we need to make an additional query: we use one query to get the names of all the subfolders, and then a second query to get the collection of files found in each folder. That second query happens to look a lot like this:

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Path = '" & strPath & "'")

That’s not too bad, except for this: in a query such as this, we must “escape” (double-up) any \’s found in the file paths. We can’t use C:\Scripts\Folder1\ in our query; we have to use C:\\Scripts\\Folder1\\ instead. You’ll see code in the script that replaces each \ with \\; that’s just something we need to do when referencing a file path in a query like this. A good chunk of the script is doing nothing more than converting the folder path names so they can be used in the query.

So much for all the caveats. Here’s the script everyone has been dying to get their hands on:

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

strFolderName = "c:\scripts"

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

Wscript.Echo strFolderName

arrFolderPath = Split(strFolderName, "\")
strNewPath = ""
For i = 1 to Ubound(arrFolderPath)
    strNewPath = strNewPath & "\\" & arrFolderPath(i)
Next
strPath = strNewPath & "\\"

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Path = '" & strPath & "'")

For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

Sub GetSubFolders(strFolderName)
    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name
        Wscript.Echo
        Wscript.Echo objFolder2.Name
        arrFolderPath = Split(strFolderName, "\")
        strNewPath = ""
        For i = 1 to Ubound(arrFolderPath)
            strNewPath = strNewPath & "\\" & arrFolderPath(i)
        Next
        strPath = strNewPath & "\\"

        Set colFiles = objWMIService.ExecQuery _
            ("Select * from CIM_DataFile where Path = '" & strPath & "'")

        For Each objFile in colFiles
            Wscript.Echo objFile.Name
        Next

        GetSubFolders strFolderName
    Next
End Sub

We told you it was complicated. But it works. This script will bind to the C:\Scripts folder and echo the names of all the files found there; the script will then get a list of all the subfolders found in C:\Scripts. From there we loop through the collection of subfolders, calling the recursive function GetSubFolders for each one. That function will list all the files found in the subfolder, then check to see if the subfolder has any sub-subfolders. If it does, the recursive function will be called again, and the process will continue to repeat itself until we reach the end of the line: until we’ve listed every file found in C:\Scripts and in all its subfolders.

Tomorrow we should go back to an easy one. Anybody out there want to know how to get the name of the local computer using a script?


How Can I Locate All the Instances of a Particular File on a Computer?

Hey, Scripting Guy! How can I locate all the instances of a particular file (like budget.xls) on a computer?

-- MN

Hey, MN. Did you know that former heavyweight boxing champion George Foreman has 10 children, 5 sons and 5 daughters? It’s true. But that’s not the best part; the best part is that all 5 of his sons are named George Edward Foreman. (And, just for good measure, one of his daughters is named Georgette.) Apparently that’s the sort of thing that can happen when you get hit in the head over and over again.

Of course, at least George (the original George) can use boxing as an excuse. By contrast, the Scripting Guy who writes this column also tends to use the same name over and over again, and he’s never been hit in the head. (Though many people have been tempted to do so.) For example, when he tackled this question he used Test.vbs as the file name to look for. The end result: he found 36 instances of Test.vbs, including:

c:\scripts\test.vbs
d:\itunes\test.vbs
d:\keep these\101 things\test.vbs
d:\keep these\2003 stuff\test.vbs
d:\keep these\adsi security\backup\test.vbs
d:\keep these\adsi security\test.vbs
d:\keep these\agent\test.vbs
d:\keep these\database webcast\test.vbs
d:\keep these\hta helpomatic\test.vbs
d:\keep these\miscellaneous\scripting guide\wmi presentation\test.vbs
d:\keep these\miscellaneous\test.vbs

Etc., etc., etc.

Note. Yes, it is a good thing that the Scripting Son is an only child.

In other words, at least some of us are very familiar with the concept (problem?) of having multiple files with the same name, all of these files scattered every-which-way on a computer. We won’t spend any time debating whether that’s a good thing or a bad thing (primarily because that’s a debate we can’t imagine winning). Instead, we’ll spend our time talking about the following script:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where FileName = 'test' and Extension = 'vbs'")

For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

We hope you weren’t expecting a really complicated script here. For one thing, we don’t need a very complicated script; this is actually a pretty easy problem to solve. For another, remember that this Scripting Guy has given 36 different files the name Test.vbs. As you might expect, “complicated” isn’t a word he’s too terribly familiar with.

The script starts out by connecting to the WMI service on the local computer. Could we perform this same search on a remote computer? You bet we could; just assign the name of that computer to the variable strComputer:

strComputer = "atl-fs-01"

After connecting to the WMI service we then issue the following query:

Set
colFiles = objWMIService.ExecQuery _
("Select * From CIM_DataFile Where FileName = 'test'
and Extension = 'vbs'")

As you can see, we’re querying the CIM_DataFile class, which just happens to be the WMI class responsible for managing all the files on a computer. Of course, we aren’t interested in all the files on the computer, just those named Test.vbs. That’s why we tack on a Where clause that includes two stipulations:

· All the files returned by the query must have a FileName equal to test; the FileName property is simply the name of the file minus the file extension. That’s an important consideration: many people who work with the CIM_DataFile class assume that the FileName is the “complete” file name (e.g., Test.vbs). It’s not. And don’t use the Name property in your query, either. We’ll explain what the Name property is for in just a minute.

· All the files returned by the query must have an Extension equal to vbs. Again, be careful here: without thinking most people include the period when specifying the file extension (e.g., .vbs). Don’t do that.

Note. OK, don’t do that unless it doesn’t matter to you whether or not your query returns the expected information. If you’re running the script just for the pure joy of running a script, well, feel free to specify any file extension you want. But if you’d really like the script the find all instances of Test.vbs, leave out the period.

You’re right: CIM_DataFile can be a little tricky to work with, can’t it? About all we can say is that you should always wear gloves and safety goggles before using CIM_DataFile in a script. Oh, and check out the Microsoft Windows 2000 Scripting Guide if you need information about what the various properties really mean.

After our query finishes executing (a process that could take a minute or two, depending on the size of your disk drives and on the number of files stored on those drives) we set up a For Each loop to walk through the items in the collection, echoing back the Name of each file:

For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

And, yes, it probably would have been better if this property was called Path rather than Name; that’s because the Name really is the full path to the file (e.g., c:\scripts\test.vbs). But with CIM_DataFile, you need to expect the unexpected.

There you go, MN; that should help you track down all the files named Test.vbs. And if you’re looking for all the people in the world named George, we’d recommend starting at George Foreman’s house. You ought to be able to get at least half of them right there.


How Can I Make a Read-Only File Writeable?

Hey, Scripting Guy! Is it possible to use a script to change the read-only attribute of a file?

-- ZO

Hey, ZO. Of course, you can do this; the FileSystemObject can do anything!

Well, OK, it can’t do either of those things. And, no, it can’t do that, either. But it can change a read-only file to a writeable file, and with just a few lines of code.

Here’s a script that binds to the file C:\Scripts\Computers.txt, checks to see if the file is read-only and, if it is, makes it writeable:

Const READ_ONLY = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.GetFile("C:\Scripts\Computers.txt")

If objFile.Attributes AND READ_ONLY Then
    objFile.Attributes = objFile.Attributes XOR READ_ONLY
End If

Pretty easy, huh? The only tricky part comes with this line of code, when we check to see if the file is read-only:

If objFile.Attributes AND READ_ONLY Then

The way we determine whether or not a file is read-only is by checking the appropriate switch in the Attributes bitmask. If this switch (which has the value of 1; that’s why we created a constant named READ_ONLY and assigned it the value 1) is enabled, then the file is read-only. If the switch is not enabled then the file is writeable.

This line of code is a bit confusing (welcome to the world of Boolean logic!) but it can be read like this: if the read-only switch within the Attributes bitmask is enabled, then run the next line of code. The syntax is kind of clumsy-looking, but it’s just something we have to live with.

And before you ask, what if you wanted to turn a writeable file into a read-only file? Well, in that case, we’d want to determine if the read-only switch is not set. Thus we’d use this line of code instead:

If Not objFile.Attributes AND READ_ONLY Then

If it turns out that we have a read-only file, the script then uses the Boolean operator XOR to “flip” the read-only switch. This line of code executes only if the read-only switch is enabled; consequently, it’s going to turn this switch off, thus making the file writeable. The XOR operator just flips things from on to off and vice-versa. That’s why we check to see if the read-only switch is enabled before we call XOR. Suppose we didn’t check and suppose the file was writeable. In that case, XOR would flip the switch, and make our writeable file read-only.

Yes, it’s confusing. For a very brief primer on bitmask properties and Boolean logic, you might take a look at this portion of the Microsoft Windows 2000 Scripting Guide.

Of course, instead of simply changing one read-only file to a writeable file, a more common task is to go through a folder and make all the read-only files writeable. Can a script do that? You bet it can. Here’s a script that uses WMI to retrieve a list of all the read-only files found in the folder C:\Scripts (by checking for files where the Writeable property is FALSE). The script then takes each of those files, passes the file name to the FileSystemObject, and proceeds to make the file writeable. We won’t explain how all the WMI code works today; if you’re interested in that, you might take a look at this Hey, Scripting Guy! column.

Oh, right; here’s the script:

Const READ_ONLY = 1

strComputer = "."
Set objFSO = CreateObject("Scripting.FileSystemObject")

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

Set colFiles = objWMIService. _
    ExecQuery("Select * from CIM_DataFile where Path = '\\Scripts\\' " & _
        "AND Drive = 'C:' AND Writeable = FALSE")

For Each objFile in colFiles
    Set objReadOnlyFile = objFSO.GetFile(objFile.Name)
    objReadOnlyFile.Attributes = objReadOnlyFile.Attributes XOR READ_ONLY
Next


How Can I Move Files Based on Their File Extension?

Hey, Scripting Guy! I have a folder with a bunch of files in it. I need to move all those files; the only problem is that the files need to be moved to different folders depending on their file extensions. For example, I want all the .log files to go here, all the .bak files to go there, etc. How can I do that using a script?

-- SH

Hey, SH. There are at least three ways to do this that we know of: you can use WMI; you can use the FileSystemObject; or you can use the Shell object. Is any one method better than the other? Well, ultimately all three will accomplish the task; the one advantage WMI has over the other two is that WMI can accomplish this task just as easily on remote computers as it can on the local computer. Of course, the one disadvantage WMI has is that it doesn’t actually have a built-in method for moving files. But, hey, we’ve never let a little thing like that stop us before, have we?

Let’s take a look at a script that retrieves a list of all the files in the C:\Payroll folder, and then moves any file with a .log file extension to the folder D:\Operation Logs:

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

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

For Each objFile in colFiles
    If objFile.Extension = "log" Then
        strCopy = "D:\Operation Logs\" & objFile.FileName _
            & "." & objFile.Extension
        objFile.Copy(strCopy)
        objFile.Delete
    End If
Next

Important. If you run this script, make sure you use valid paths. Suppose you try copying files to D:\Operation Logs, only you don’t actually have a D drive on your machine. In that case, the Copy command will fail, but the Delete command will succeed. Thus your file will be deleted without being copied anywhere.

We begin by connecting to the WMI service, then use an ASSOCIATORS OF query to retrieve all the files found in the folder C:\Payroll. We then walk through the collection of files, checking to see if any of the files have the file extension log. That’s what we do here:

If objFile.Extension = "log" Then

Notice that we’re not looking for files with a .log file extension; make sure you leave the dot (.) out.

And what happens if we find a file that has a log file extension? Well, as we mentioned earlier, WMI doesn’t have a built-in method for moving files. We’re going to work around this by copying the file from the C:\Payroll folder to the D:\Operation Logs folder. For a brief moment we’ll have two copies of the file: one in C:\Payroll and one in D:\Operation Logs. But don’t worry: in the very next line of code we’ll delete the copy found in C:\Payroll. When we do that we’ll have only a single copy of the file, the one found in D:\Operation Logs. In effect, we’ve “moved” the file even though we never used a Move method of any kind.

Yes, very sneaky.

The only tricky thing here is that WMI requires a complete path name when copying a file. Suppose we find a file named March.log. To copy this file we need to use the complete path: D:\Operation Logs\March.log. So we use this line of code to construct that path:

strCopy = "D:\Operation Logs\" & objFile.FileName _
    & "." & objFile.Extension

All we’re doing here is assigning a value to the variable strCopy. That value happens to consist of D:\Operation Logs\ plus the name of the file (March) plus a period (.) plus the file extension (log). Put them all together and it spells D:\Operation Logs\March.log. And that’s exactly the path we need to pass to the Copy method.

And, yes, as you probably noticed, the file extension is a separate property from the file name. That’s why we have to combine the FileName property plus a period plus the Extension property. It’s a bit of a hassle in this script, but it does make it easier to write other scripts, like a script that returns a list of all the files on a computer that have a log file extension.

As soon as we’ve constructed the path we can copy the file to drive D and then delete the original file from drive C. That’s what we do in these two lines of code:

objFile.Copy(strCopy)
objFile.Delete

And that’s it: the script loops around until it has checked each file in the folder, and until it has moved all the files with a log file extension to D:\Operation Logs.

In order to keep the sample script as short as possible, we check for only a single file extension. However, it’s easy to check for additional file extensions and to move those files to the appropriate folders. Here’s a revised script that also checks for .bak files and copies them to D:\Operation Backups:

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

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

For Each objFile in colFiles
    If objFile.Extension = "log" Then
        strCopy = "D:\Operation Logs\" & objFile.FileName _
            & "." & objFile.Extension
        objFile.Copy(strCopy)
        objFile.Delete
    End If
    If objFile.Extension = "bak" Then
        strCopy = "D:\Operation Backups\" & objFile.FileName _
            & "." & objFile.Extension
        objFile.Copy(strCopy)
        objFile.Delete
    End If
Next

If you want to check for even more file extensions, then just add some additional If-Then statements. That’s all there is to it.


How Can I Move Files Off a User’s Desktop and Into Specified Folders?

Hey, Scripting Guy! For various reasons, we don’t want our users storing “loose” files on their desktop. How can I write a script that will take all the files on the user’s desktop and, based on the file extension, move those files to the appropriate desktop folder? For example, if I have a file with a .doc file extension I’d like to move it to a folder named Doc. If I have a file with a .xls file extension I’d like to move it to a folder named XLS. And so on.

-- GA

Hey, GA. You know, this is a great time to be alive, at least for those of us in the Seattle area. For example, on Saturday our very own Washington Huskies – oh, never mind: after leading Ohio State 7-3 at the half, the Huskies fell completely apart in the final two quarters and ended up losing their first game of the year. However, our very own Seattle Seahawks – well, the less said about our very own Seattle Seahawks the better. (And people thought it was impossible to lose to the Arizona Cardinals.) Fortunately, though, we still have our very own Seattle Mariners, who are right in the thick of the baseball pennant race.

Or at least they were, until they lost 17 out of their past 22 games.

Note. On the bright side, the Mariners became the first team in baseball history to be more than 20 games over .500 going into late August and then lose 13 out of 14 games (and, in the process, fall hopelessly out of the race). So at least we got to see history being made.

Yet another note. Did it help that Scripting Guy Dean Tsaltas, who’s moved back to Halifax, Nova Scotia, sent us a photo this morning, a photo showing Nova Scotians frolicking at the beach over the weekend? Oh, sure, that helped a lot ….

All right, so maybe this isn’t a great time to be alive. But, then again, there are far more important things in life than sports, right? Not that we can think of any right offhand, mind you – oh, wait: we have one. What about this, what about a script that can move files off the desktop and into folders that correspond to the file extensions?

You know, a script like this one:

Const DESKTOP = &H10&

Set objShell = CreateObject("Shell.Application")

Set objFolder = objShell.Namespace(DESKTOP)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

strComputer = "."

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

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

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each objFile in colFiles
    strDesktopFolder = strPath & "\" & objFile.Extension
    If objFSO.FolderExists(strDesktopFolder) Then
        strTarget = strDesktopFolder & "\"
        objFSO.MoveFile objFile.Name, strTarget
    Else
        Set objFolder = objFSO.CreateFolder(strDesktopFolder)
        strTarget = strDesktopFolder & "\"
        objFSO.MoveFile objFile.Name, strTarget
    End If
Next

As you can see, we start things off by defining a constant named DESKTOP and setting the value to &H10&; we’ll need this constant when we make our connection to the user’s desktop folder. And that’s a good point: we should note that this script only moves files stored on the user’s desktop folder (e.g., C:\Documents and Settings\kenmyer\Desktop). As you know, the Windows desktop actually displays items drawn from two locations: the individual user’s desktop folder and the All Users desktop folder (C:\Documents and Settings\All Users\Desktop). What if you want to move files from the All Users desktop folder as well the user’s desktop folder? Well, in that case, the easiest thing to do is to make a duplicate copy of the script, then replace the first line of code with this:

Const ALL_USERS_DESKTOP = &H19&

And, of course, make sure you replace any instances of the constant DESKTOP with the constant ALL_USERS_DESKTOP.

If you don’t care about the All Users desktop, well, then you don’t have to do anything at all.

After defining the constant, we create an instance of the Shell.Application object, then use the following three lines of code to bind to the desktop folder, retrieve the path to that folder, and store the path (e.g., C:\Documents and Settings\kenmyer\Desktop) in a variable named strPath:

Set objFolder = objShell.Namespace(DESKTOP)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

If you’re familiar with the Shell.Application object you’re probably thinking, “Hmmm, the Shell object is designed to work with the local computer; does that mean this script is designed to work with the local computer?” The answer to that is this: yes, this script is designed to work with the local computer. There are a couple reasons for this, with the big one being the fact that it’s not always easy to identify who’s logged on to a remote machine; in turn, that makes it really hard to determine which folder happens to be that user’s desktop folder. This isn’t necessarily impossible, but it’s a bit tricky, at best. Therefore, we decided to limit this script to working on the local computer only.

Note. So does that mean you’re out of luck when it comes to managing remote computers? Of course not; just run this script as a user logon or logoff script and everything will work just fine.

After we retrieve the path to the user’s desktop folder, we connect to the WMI service on the local computer. We then use this query to return a collection of all the files found in the desktop folder:

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

As soon as we have our collection in hand we create an instance of the Scripting.FileSystemObject object (the technology of choice for moving files around). Once that object has been created we’re ready to set up a For Each loop to loop through the collection of files and – tah-dah! – start moving these files to their designated folders.

See? As the saying goes, ‘tis far better to strike a match than curse the darkness.

Although, after this past weekend, it’s probably a good thing that the Scripting Guy who writes this column doesn’t actually have any matches.

In order to move files off the desktop, the first thing we do inside the loop is execute this line of code:

strDesktopFolder = strPath & "\" & objFile.Extension

As you might recall, we want to move all the desktop files into the appropriate folder. What is the appropriate folder? That’s easy: it’s the folder that has the same name as the file extension. (For example, all .doc files need to be moved into a folder named Doc.) What we’re doing with the preceding line of code is taking a peek at the file’s Extension property (which is simply the file extension, minus the period), then using that value to construct a file path similar to this one:

C:\Documents and Settings\kenmyer\Desktop\Doc

As you can see, that’s nothing more than a folder named Doc, located in the user’s desktop folder.

Of course, we can’t move a file into a folder unless that folder exists. (Something to do with the whole space-time continuum, which is yet another thing to Scripting Guy who writes this column is depressed about.) Therefore, our next step is to call the FolderExists method and determine whether or not we already have a Doc folder on the desktop:

If objFSO.FolderExists(strDesktopFolder) Then

Let’s assume that we do. In that case, we’re going to run these two lines of code:

strTarget = strDesktopFolder & "\"
objFSO.MoveFile objFile.Name, strTarget

In the first line, we’re simply tacking a \ onto the end of our Doc folder; that’s going to result in the variable strTarget being assigned this value:

C:\Documents and Settings\kenmyer\Desktop\Doc\

Why do we do that? Well, this ensures that our file gets copied into the Doc folder. If we leave the \ off, we’ll get a “File already exists error.” Why? Because, without the trailing slash, the file system object will try to rename our .doc file to the name of the folder (C:\Documents and Settings\kenmyer\Desktop\Doc). Granted, that’s a little weird (and even more irritating), but that’s also the way it works. But, hey, there’s no point in getting upset over a little thing like that; just put a \ at the end of the folder path and everything will work just fine.

Note. Save your disgruntlement for something important. Like football games.

In line 2, we call the MoveFile method and actually move the file off the desktop and into the designated folder. And then we simply loop around and repeat the process with the next file in the collection.

That’s nice, but what if the folder Doc doesn’t exist? Once again, no problem; in that case we run this block of code instead:

Set objFolder = objFSO.CreateFolder(strDesktopFolder)
strTarget = strDesktopFolder & "\"
objFSO.MoveFile objFile.Name, strTarget

As you can see, we really only do one thing different here: in the first line we call the CreateFolder method to create the new desktop folder. (Remember? C:\Documents and Settings\kenmyer\Desktop\Doc?) As soon as the folder is created we can assign the folder path (plus the trailing \) the variable strTarget, then call the MoveFile method to move the file into the folder.

And then it’s back to the top of the loop where we repeat the process with the next file in the collection.

We hope that answers your question, GA. And we hope that things are going better for you than they are for the Scripting Guys. But, then again, we have to admit that, other than a few lost football games and baseball games, things are going pretty good for the Scripting Guys. Take the weather, for example. Yesterday it – well, OK, yesterday it rained. And it’s raining today. And it’s supposed to rain again tomorrow. But that’s all right: even in Seattle it can’t rain forever, can it?

Really? We were afraid you were going to say that.

If anyone needs us, we’ll be in the corner of our office.

Feeling sorry for ourselves.


How Can I Move the Oldest File in a Folder to a Different Folder?

Hey, Scripting Guy! How can I move the oldest file in the folder C:\Upload to C:\Download?

-- DD

Hey, DD. Funny you should mention the word “oldest.” The Scripting Guy who writes this column recently attended a family reunion and, for the first time ever, came face-to-face with the whole idea of getting old. This Scripting Guy has fond memories of family reunions past, recalling the good times he had with his Scripting Siblings and Scripting Cousins. Of course, there was always one problem with family get-togethers: you had to try and steer clear of all the old people, the grownups who wanted to rub your head, tell you that they could remember back when you were in diapers, and insist on telling you a long, confusing story about your dad, a story that didn’t make a bit of sense. (It would only be sometime later when you’d find out they had gotten you mixed up with someone else, and the story was really about your grandpa or uncle or some obscure cousin you never heard of.) But as long as you stayed away from the grownups section, well, family reunions were fun.

At this latest family reunion, however, this Scripting Guy noticed something very disturbing: his siblings and cousins had turned into the old people! Now they were the ones who, instead of playing Wiffle ball or badminton, sat around and talked, swapping recipes and complaining about their knees. They shied away from the cake (too rich for my blood!) and the fried chicken, and spent most of their time drinking coffee, despite the fact that the temperature was right around 100 degrees. Somewhere along the line, all the Scripting Siblings and Cousins had gotten old!

Fortunately, that didn’t happen to the Scripting Guy. He’s as young and as vibrant as ever.

And because he is still as young as ever, this Scripting Guy has no qualms about writing a script that gets rid of something old; after all, what does being old have to do with him? For example, here’s a script that moves the oldest file in the folder C:\Upload to the folder C:\Download:

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Upload")

Set colFiles = objFolder.Files

dtmOldestDate = Now

For Each objFile in colFiles
    If objFile.DateCreated < dtmOldestDate Then
        dtmOldestDate = objFile.DateCreated
        strOldestFile = objFile.Path
    End If
Next

objFSO.MoveFile strOldestFile, "C:\Download\"

Before we explain how this script works we should note that DD specifically talked about moving a file from one folder on the local computer (C:\Upload) to another folder on the same computer (C:\Download). Because of that we went ahead and used the FileSystemObject to tackle this chore. By using the FileSystemObject we were able to quickly piece together a script that not only works but is also fairly short and easy to understand. The only drawback to using the FileSystemObject? That pretty much limits the script to running on the local computer.

But what if you need to run this script against a remote computer? Well, at the end of this column we’ll show you a variation of this script that uses WMI; that enables the script to be used against remote machines. Of course, because of a few WMI eccentricities, that also makes the script a tad bit longer and a shade more complicated. We won’t take the time today to explain how the WMI script works; if you need that kind of clarification let us know and we’ll revisit this sometime in the near future.

No thank you, no potato salad for us. We’ll just have coffee.

As for the script that we will try to explain, our first thought upon reading this question was this: we’ll just write a script that grabs all the files in the folder, sorts them by file creation time, and then grabs the oldest file from that sorted list. Admittedly, that’s probably the way you should do it. Unfortunately, though, VBScript doesn’t have any sorting capabilities built into it; that means we’d have to write code that used something like a bubble sort or a disconnect recordset in order to get a sorted list of files. (For more information about these sorting techniques, see the Scripting Week 2 webcast Things the Scripting Guys Never Told You.)

Either of those techniques will work, of course. However, some of that coding can be a bit complicated, and to what end? After all, we don’t really need a sorted list of files; we just need to determine the oldest file in the folder. Therefore, we decided to take a less-elegant – but much easier – approach: instead of sorting all the files we’d simply look at the creation time for each file, one-by-one. We’d start with file 1 and keep a note of the file path; after all, seeing as how it’s the only file we’d looked at so far it would, by default, be the oldest file. We’d then look at the creation time for file 2; if that time was older than file 1’s creation time then we’d keep a note regarding the path to file 2 instead. This process would continue until we’d looked at all the files and determined the oldest one in the bunch.

Which will probably be the cranky old file telling the younger files to go play in the front yard so the grownups can talk.

To do all that we begin by creating an instance of Scripting.FileSystemObject; we then use this line of code to bind to the folder C:\Upload:

Set objFolder = objFSO.GetFolder("C:\Upload")

After we’ve made a connection to the Upload folder we can then use this line of code to retrieve a collection of all the files found in the folder:

Set colFiles = objFolder.Files

Now the fun begins. What we need to do is pick a baseline time to start with; if the creation time for the first file in the collection is older than the baseline time then we’ll call file 1 – for the time being – the oldest file in the collection. To get the starting baseline, we create a variable named dtmOldestDate and assign that variable the current date and time:

dtmOldestDate = Now

Once we’ve established this baseline time we set up a For Each loop to loop through all the files in the collection. For the first file in the collection we use this line of code to see if the value of the file’s DateCreated property is older than the baseline value dtmOldestDate:

If objFile.DateCreated < dtmOldestDate Then

Needless to say, it’s likely that this first file will have a creation time older than dtmOldestDate. But suppose it doesn’t; what then? No big deal; in that case we simply loop around and check the next file in the collection.

For the sake of argument, however, let’s say that dtmOldestDate has the value 7/25/2006 8:00 AM and the first file in the collection (a.txt) has a creation date of 7/24/2006 7:30 AM. In this case, the file a.txt truly is older than the baseline value. Therefore, we execute these two lines of code:

dtmOldestDate = objFile.DateCreated
strOldestFile = objFile.Path

We’re doing two things here. First, we’re assigning dtmOldestDate a new value, the value of a.txt’s DateCreated property. Why? Because this value now represents the oldest file in the folder. Second, we assign the path of the file (C:\Upload\a.txt) to a variable named strOldestFile. We use the variable dtmOldestDate to keep track of the oldest creation time; likewise, we use the variable strOldestFile to keep track of which file (by file path) is the oldest file in the folder.

And then, of course, we simply loop around and repeat this process with the next file in the collection. When all is said and done strOldestFile will contain the path to the oldest file in the folder. In turn, that means we can move the file using one line of code:

objFSO.MoveFile strOldestFile, "C:\Download\"

Nothing too complicated there: we simply call the MoveFile method, using strOldestFile to represent the file we want to move, and using C:\Download\ (note the \ at the end) to indicate the Folder we want to move the file to. There you have it: the oldest file in C:\Upload will now reside in C:\Download.

Now, where were we? Oh, that’s right: we were going to tell you a story about your mother, weren’t we? Well, you won’t believe this, but your mother was a bit of a rapscallion when she was younger; she was always getting into mischief. For example, one time she wrote a WMI script that could identify and move the oldest file in a folder. That’s right: your mother did that. The script looked something like this:

Const CONVERT_TO_LOCAL_TIME = True

Set dtmOldestDate = CreateObject("WbemScripting.SWbemDateTime")
dtmOldestDate.SetVarDate Now, CONVERT_TO_LOCAL_TIME

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

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

For Each objFile In FileList

    If objFile.CreationDate < dtmOldestDate Then
        dtmOldestDate = objFile.CreationDate
        strOldestFile = objFile.Name
    End If
Next

strFile = Replace(strOldestFile, "\", "\\")

Set colFiles = objWMIService. _
    ExecQuery("Select * from CIM_DataFile where Name = '" & strFile & "'")

For Each objFile in colFiles
    strOldName = LCase(objFile.Name)
    strNewName = Replace(strOldName, "c:\upload", "c:\download")
    objFile.Copy(strNewName)
    objFile.Delete
Next

Of course, because this script uses the SWbemDateTime object it runs on only Windows XP and Windows Server 2003. But that was just like your mother: all she ever talked about was Windows XP this and Windows Server 2003 that. For example, there was this one time – this was shortly after the war, mind you – when she came to our house – that was back when we lived on 38th, in the blue house, you remember, your dad used to bring you by every now and then and you’d pick blackberries in the backyard – boy, you liked those blackberries although you didn’t like the thorns, there was this one time – this is such a cute story – there was this one time ….


How Can I Provide a Visual Indicator When Copying Files?

Hey, Scripting Guy! How can I provide a visual indicator of some kind when copying a large file or large set of files?

-- SK

Hey, SK. Before we do anything else we should note that the Scripting Guys – or at least the Scripting Guy who writes this column – don’t believe in moral victories. It’s nice that the University of Washington took USC – the No. 3-ranked college football team in the country – right down to the wire on Saturday, coming within 15 yards – and a few seconds – of pulling off the upset. And while it’s encouraging to see the Huskies go toe-to-toe with a team that’s won 49 of its last 51 games, well, a close, hard-fought loss is still just that: a loss. But that’s the way it goes.

Of course, now that we think about it, if we were smart then we would believe in moral victories. After all, in that case we wouldn’t have to write a script that could provide a visual indicator of some kind when copying a large file or large set of files; it would be enough to say that we tried to write a script that could provide a visual indicator of some kind when copying a large file or large set of files. Did we take the garbage out? No, we didn’t. But the important thing is that we tried to take the garbage out; we just couldn’t quite get around to it. Same for getting the oil changed or mowing the lawn. We didn’t exactly do it, but the fact that we even thought about doing it should be considered a moral victory.

But, like we said, Scripting Guys don’t believe in moral victories. With that in mind, here’s a script that displays the Copying dialog box while it copies files from one location to another:

Const FOF_CREATEPROGRESSDLG = &H0&

strTargetFolder = "D:\Scripts"

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.NameSpace(strTargetFolder)

objFolder.CopyHere "C:\Scripts\*.*", FOF_CREATEPROGRESSDLG

As we noted, the Scripting Guys don’t believe in moral victories; we also don’t believe in working particularly hard. Although we could have used a combination of HTML and ActiveX controls to cobble together our own progress bar we took the easy way out: we simply used the Windows Shell object to copy the files. Why is that so easy? Because one of the options offered by the Shell object is to display the Copying dialog box while copying files or folders. Consequently, with a minimal amount of coding and with no need for HTML pages or ActiveX controls, we were able to come up with a file copying script that displays a dialog box similar to this:

Sudden thought. If you try really hard, yet fail, you get credit for a moral victory. So then what do you get when you don’t really try all that hard and yet succeed anyway?

Editor’s Note: That’s what’s called a real victory. Something the Scripting Guys know very little about – they’ve got the “don’t really try all that hard” part down, it’s the rest that tends to be a problem.

Let’s talk about how the script works. We start out by defining a constant named FOF_CREATEPROGRESSDLG and set the value to &H0&; in a minute or two, we’ll use this constant to tell the script that we want to display the Copying dialog box while the files are being copied. We then assign the value D:\Scripts to a variable named strTargetFolder. This is the folder that we want to copy the files to; we won’t specify the folder we want to copy files from until we actually do the copying.

With our constant and variable taken care of the next step is to create an instance of the Shell.Application object. From there, we use this line of code to create an object reference to our target folder (D:\Scripts):

Set objFolder = objShell.NameSpace(strTargetFolder)

Believe it or not, the only thing left to do is copy all the files and folders found in C:\Scripts (that is, C:\Scripts\*.*) to the target folder. (And, of course, display the Copying dialog box as we do so.) That takes just one line of code:

objFolder.CopyHere "C:\Scripts\*.*", FOF_CREATEPROGRESSDLG

As you can see, we simply call the CopyHere method, followed by the file or set of files we want to copy (in this case, C:\Scripts\*.*) and the constant FOF_CREATEPROGRESSDLG. That’s it.

Whoa, whoa, hold on; one question at a time. Let’s start with you in the back. Does the Cancel button in the dialog box actually work? Yes, it does: if you click Cancel the copy operation will terminate. (However, it won’t roll anything back: files that were copied to the target folder before the button was clicked will not be deleted from the target folder.) If you’d prefer to not give people the option of cancelling the operation you have at least two choices:

· Leave off the constant FOF_CREATEPROGRESSDLG when calling the CopyHere method. The copy operation will still take place; you just won’t see the Copying dialog box while it takes place.

· Take a different approach to copying files, and use a custom progress bar to serve as your visual indicator.

Who’s next? Yes, you in the blue shirt. “Can you use this technique to copy files from your local machine to a remote computer?” Yes, you can; you just need to use a UNC path when specifying the folder on the remote computer. For example, this script takes all the files found in C:\Scripts on the local computer and copies them to C:\Scripts on the remote computer atl-fs-01. Notice that, in this case, we use the administrative share C$ as part of the folder path:

Const FOF_CREATEPROGRESSDLG = &H0&

strTargetFolder = "\\atl-fs-01\C$\scripts"

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.NameSpace(strTargetFolder)

objFolder.CopyHere "C:\Scripts\*.*",
FOF_CREATEPROGRESSDLG

We seem to have time for one more. “Can we also flip this around and copy files from a remote machine to the local computer?” Of course you can. Again, the secret there is to use UNC paths. In this sample script we copy only the .VBS files (*.vbs) from atl-fs-01’s shared folder Public to C:\Scripts on the local computer:

Const FOF_CREATEPROGRESSDLG = &H0&

ParentFolder = "c:\scripts"

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.NameSpace(ParentFolder)

objFolder.CopyHere
"\\atl-fs-01\public\*.vbs", FOF_CREATEPROGRESSDLG

Sorry, that’s all the time we have for today.

We hope that helps, SK, and we hope the University of Washington gets over its disappointment and gets back on the winning track on Saturday. And now that we check the clock, it looks like we have time for one last question after all. How about the young man in the crimson and silver shirt? What’s that? “How did Washington State University do this weekend?” Washington State University? There must be some mistake; everyone knows there’s only one university of the state of Washington.


How Can I Read Text From a File and Then Rename the File Using the Text I Just Read?

Hey, Scripting Guy! I would like to be able to open a file, read the first 10 characters, and then rename the file to those 10 characters plus a .txt file extension. How can I do that?

-- KA

Hey, KA. Ah, for once a text file question that can be answered without some weird and convoluted workaround. This is actually pretty easy: we can use the FileSystemObject to open the text file and read in the first 10 characters, then use the FileSystemObject to rename the file to those 10 characters plus a .txt file extension. As you can see, all that takes just a few lines of code:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Log.txt", ForReading)
strCharacters = objFile.Read(10)
objFile.Close

strNewName = "C:\Scripts\" & strCharacters & ".txt"

objFSO.MoveFile "C:\Scripts\Log.txt", strNewName

We begin by defining a constant named ForReading and assigning it the value 1; this constant is used when we open the text file for reading. We then create an instance of the FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Log.txt. With the file open we then read the first 10 characters and store that data on the variable strCharacters:

strCharacters = objFile.Read(10)

As you can see, we can use the Read method to read a specified number of characters from a text file. Suppose we wanted to read the first 37 characters from the file? No problem; we’d just use this line of code:

strCharacters = objFile.Read(37)

After closing the file we construct a new file name, one consisting of the folder path (C:\Scripts\), the 10 characters we just read in (represented by the variable strCharacters), and the new file extension (.txt). That, too requires just one line of code:

strNewName = "C:\Scripts\" & strCharacters & ".txt"

All that’s left is to rename the file. We can do that by using the FileSystemObject’s MoveFile method. (Don’t let the name fool you; in this case the file is simply being “moved” from its old name to its new name; it’s not changing locations within the file system). We pass MoveFile two parameters: the current path to the file, and the new path to the file. Sounds crazy, but “moving” a file within the same folder has the net effect of renaming it. Weird but true!


How Can I Remove Blank Spaces From File Names?

Hey, Scripting Guy! I have a folder with a bunch of files in it, files with names like Doc 1.pdf, Doc 2.pdf, and Doc 3.pdf. How do I remove the blank spaces from all those file names?

-- JRP

Hey, JRP. You know, today’s column represents a landmark in Hey, Scripting Guy! history. Why’s that? Because the Scripting Guy who writes this column actually used this script to solve a problem he faced. The Scripting Guys actually practicing what they preach? Bet you never thought you’d see that, did you?

Of course, not being the smartest of Scripting Guys he came very close to not using the script to solve his problem. As is his wont, this Scripting Guy read your email and wrote a script several days before he actually sat down to write this column. In the interim, he faced a situation very similar to yours: he had a folder with over 100 files in it, files that had names like this:

Slide 1.gif
Slide 2.gif
Slide 3.gif

The problem with that? Because these files were destined for the Web, all the blank spaces in all those file names had to be removed. As you might expect, this Scripting Guy opened the folder in question, right-clicked the first file, clicked Rename, and began manually renaming each and every file.

It was right then that a little voice in his head said, “How dumb are you?” At first he ignored the little voice; after all, he’s been asked that question so many times he no longer bothers to answer. (Editor’s Note: And usually the voice is coming from the Scripting Editor, who he tends to ignore anyway.) This time, though, the voice grew a little more insistent. “Why are you manually removing blank spaces from all these file names? Why don’t you just use the script you wrote the other day?”

“Um, what script is that, little voice?”

“The script that removes blank spaces from file names!”

Oh. This script:

strComputer = "."

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

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

For Each objFile In colFileList
    arrNames = Split(objFile.Name, "\")
    intIndex = Ubound(arrNames)
    arrNames(intIndex) = Replace(arrNames(intIndex), " ", "")
    strNewName = Join(arrNames, "\")
    errResult = objFile.Rename(strNewName)
Next

With a little help from that voice inside our head let’s see if we can explain how this script works. We begin by connecting to the WMI service on the local computer (although this script can also be run against a remote computer). We then use this query to return a collection consisting of all the files in the folder C:\Data:

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

Now it gets interesting. To begin with, we set up a For Each loop to loop through the collection of files. Inside this loop we start out by executing this line of code:

arrNames = Split(objFile.Name, "\")

What we’re doing here is using the Split method to split the file path (or, as WMI refers to it, the Name property) on the \. For example, if the first file in the collection is C:\Data\File 1.pdf we’ll end up with an array (arrNames) consisting of the following elements:

C:
Data
File 1.pdf

Why do we bother with that? Because we need to separate the file name (File 1.pdf) from the rest of the path, something that has to be done before we start removing blank spaces. What difference does that make? In this particular script, no difference. But suppose our file had this file path:

C:\Documents and Settings\Ken Myer\My Scripts\File 1.pdf

If we did a blanket replacement of all the blank spaces in the path we’d end up with this:

C:\DocumentsandSettings\KenMyer\MyScripts\File1.pdf

That’s why we need to extract the file name, remove the blank spaces just from the file name, and then put the complete file path back together.

We see we have a question: how does putting the file name into an array help us with this chore? Well, we know that the file name has to be the last item in the file path; that means it will also be the last item in the array. By determining the Ubound (upper bound) value of the array, we can determine the index number for the file name:

intIndex = Ubound(arrNames)

Once we know the index number we can use this line of code to remove any blank spaces found in the file name:

arrNames(intIndex) = Replace(arrNames(intIndex), " ", "")

It’s a complicated-looking line of code, but it’s actually doing something quite simple. What we’re doing here is assigning a new value to the last item in the array, arrNames(intIndex). What’s the new value being assigned? This:

Replace(arrNames(intIndex), " ", "")

All we’re doing here is using the Replace function to replace any blank spaces (" ") in arrNames(intIndex) with, well, nothing (""). The net effect: any blank spaces found in the file name will be removed.

With the blank spaces gone we can then use the Join function to reconstruct the file path:

strNewName = Join(arrNames, "\")

Notice that we pass Join an optional, second parameter: “\”. Why? Well, by default, the Join method puts a blank space between each of the items being joined; that would mean we’d end up with a file path like this:

C: Data File1.pdf

And you’re right: that isn’t much of a file path, is it? By using this second parameter we’re telling the Join method to place a \ between each item instead of a blank space. That gives us a file path like this:

C:\Data\File1.pdf

Which is more like it.

As soon as we have a new file path (with the blank spaces removed from the file name) we can then use this line of code to rename the file C:\Data\File 1.pdf to C:\data\File1.pdf:

errResult = objFile.Rename(strNewName)

And then we loop around and repeat the process with the next file in the collection.

There you go, JRP; we hope this script proves as useful to you as it did to us. And speaking of us, now the little voice inside our head is telling us to go get a couple doughnuts and then sneak out early. And while we probably shouldn’t do it, well, the voice was right about using this script, wasn’t it?


How Can I Rename a File Every Day, Using the Current Date as Part of the File Name?

Hey, Scripting Guy! I have a file that has the name BackupFile-06122007.txt, where the 06122007 represents yesterday’s date (June 12, 2007). Each morning I would like to run a script that changes the name of that file so that it uses today’s date: BackupFile-06132007.txt. How can I do that?

-- GZ

Hey, GZ. You know, one thing you can count on is that the Scripting Guys will always be the same, no matter where they are or what they’re doing. For example, last week at TechEd 2007 Scripting Guys Jean Ross and Greg Stemp managed to get themselves caught in a torrential downpour while visiting Disney’s Animal Kingdom. Considering they had to walk for about half an hour or so before they could find a taxi to take them back to the hotel, they got completely and totally soaked.

Note. OK, so maybe they didn’t have to walk for half an hour; truth be told, the walk would have been considerably shorter had they not walked right past the exit and essentially circled the entire park before finding the way out. But, then again, if they were capable of doing a simple thing like following the signs that say Exit, well, they really wouldn’t be Scripting Guys, would they?

And while he didn’t say a word, we have no doubt that the taxi driver was thrilled to have two sopping wet Scripting Guys pile into the back of his previously-dry cab.

The very next day the Scripting Guys showed up at the TechEd 2007 attendee party, and rode Popeye and Bluto’s Bilge-Rat Barges at Universal Studio’s Island of Adventure theme park. As you might expect, they got completely and totally soaked.

Note. Here’s a handy travel tip. Theme parks can be expensive; there’s no two ways about that. However, with a little ingenuity you can experience many theme park rides in the comfort of your own home. For example, to recreate the experience of riding Popeye and Bluto’s you can do this: sit down in a chair, and use a makeshift seatbelt to strap yourself in. Then have someone dump 15 or 20 buckets of water over your head. That’s pretty much the whole ride right there.

And, yes, now that you mention it, the Scripting Guys did ride this ride several times. In a row.

After that somewhat damp experience what do you suppose Scripting Guy Greg Stemp did upon arriving back in Seattle? That’s right: he went to his son’s baseball game, a game played (at least until the umpires finally called it off) in a driving rainstorm. And needless to say, a game where he got completely and totally soaked.

Apparently it really is true: the more things change the more they stay the same.

Fortunately, that’s also true when it comes to answering scripting-related questions. Before the Scripting Guys left for TechEd they tried to answer a new question each and every day. And now that they’re back, well, like they say, the more things change, the more they stay the same:

strComputer = "."

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

strMonth = Month(Date - 1)

If Len(strMonth) = 1 Then
    strMonth = "0" & strMonth
End If

strDay = Day(Date - 1)

If Len(strDay) = 1 Then
    strDay = "0" & strDay
End If

strYear = Year(Date - 1)

strFileName = "C:\\Test\\BackupFile-" & strMonth & strDay & strYear  & ".txt"

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where Name = '" & strFileName & "'")

For Each objFile in colFiles

    strMonth = Month(Date)

    If Len(strMonth) = 1 Then
        strMonth = "0" & strMonth
    End If

    strDay = Day(Date)

    If Len(strDay) = 1 Then
        strDay = "0" & strDay
    End If

    strYear = Year(Date)

    strNewFileName = "C:\\Test\\BackupFile-" & strMonth & strDay & strYear  & ".txt"

    errResult = objFile.Rename(strNewFileName)
Next

Sorry; hopefully you can read the script code OK. Looks like we’re still dripping a little bit. And Greg’s hat may never be the same again.

But enough about all that. As you can see, even it first glance, this is a slightly-more complicated script than you might have expected. Why? Well, before we can rename a file we have to bind to that file using a script; in order to do that, we need to know the complete file path. And that’s a problem: after all, each day the file is going to have a different name. (For example, if today this is named BackupFile-06122007.txt, then tomorrow it will be named BackupFile-06132007.txt, and so on.) Thus we have to do two things here: figure out the current name of the file, and then rename it.

Sounds hard, doesn’t it? However, because GZ needs to run this script every single day this turns out to be surprisingly easy.

Let’s show you what we mean by that. Our script starts off by connecting to the WMI service on the local computer (although this script works equally well against a file stored on a remote computer). This is the point where we would typically bind to the file in question. So why don’t we do that now? That’s easy: we don’t know which file to bind to. Therefore, before we can do anything else we need to figure out the file path for our target file. And because part of that path (and part of the file name) consists of yesterday’s date, we run this block of code:

strMonth = Month(Date - 1)

If Len(strMonth) = 1 Then
    strMonth = "0" & strMonth
End If

strDay = Day(Date - 1)

If Len(strDay) = 1 Then
    strDay = "0" & strDay
End If

strYear = Year(Date - 1)

What are we doing here? Well, to begin with, we use the Month function to determine the month for yesterday’s date; that’s what the Date – 1 is for (the current date minus 1 day). The Month function is going to return an integer value representing the month; for example, the function returns 10 if the yesterday’s month fell in October, 12 if yesterday’s month fell in December, and 6 if yesterday’s month fell in June.

And yes, you’re right: we already have a problem, don’t we? Suppose we used this date in the file name: 1112007. So is that 1/11/2007, or is that 11/1/2007? Truthfully, there’s no way to tell. Therefore, we need to make sure that the months (and days) are listed as two-digit values, with leading zeroes used to pad the single-digit months. (In other words, modifying the month of June to be 06 rather than 6.)

Fortunately, that’s pretty easy to take care of: we just use the Len function to determine the number of characters in the month (technically, in the variable strMonth). If there’s only one character in the month we then use this block of code to add a leading 0, changing 6 to 06:

strMonth = "0" & strMonth

What if there are already two characters in the variable strMonth? Then we don’t do anything at all; we’re fine. And if there are three characters in the month? Then that means the Scripting Guys really need to stop taking their laptops on water rides like Popeye and Bluto. (It’s just so hard to tear yourself away from email, you know?)

We then repeat this process for the day, using the Day function to extract the day portion of yesterday’s date and, again, adding a leading zero if needed:

strDay = Day(Date - 1)

If Len(strDay) = 1 Then
    strDay = "0" & strDay
End If

Finally, we use the Year function to assign the year for yesterday’s date to a variable named strYear:

strYear = Year(Date - 1)

In this case there’s no need to add a leading zero because, by default, years are reported back as four-digit values.

After we have the individual pieces we can then construct yesterday’s date – and, in turn, the current file path – by using this line of code:

strFileName = "C:\\Test\\BackupFile-" & strMonth & strDay & strYear  & ".txt"

As you can see, this is a pretty straightforward equation: we’re just combining the file path and the first part of the file name (BackupFile-) with the variables representing the month, day, and year, tacking the .txt file extension on the end. The only tricky part here is that the \ is a reserved character in WMI; among other things, that means that this character (like all reserved characters) must be “escaped” before that character can be used in a WMI Where clause. So how do you escape a character in WMI? That’s easy: you just preface it with a \. If you’ve been sitting there wondering, “Why C:\\Test\\BackupFile-? Why not C:\Test\BackupFile-?” well, now you know.

After we’ve figured out the current name of our file we then use a regular old ExecQuery call to bind to the file, using the variable strFileName to represent the file path:

Set colFiles = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where Name = '" & strFileName & "'")

Assuming all goes according to plan (and with the Scripting Guys everything always goes according to plan; we just have a tendency to make really bad plans) we should get back a collection (with the name colFiles) consisting of all the files with the designated file path. And because file paths must be unique on a computer that means we’ve located – and connected to – our target file.

That also means that we’re just about ready to rename the file. Before we can do that, though, we need to create a new name for our file. Fortunately we don’t have to put much thought into that: we already know that the file is going to be named C:\Test\BackupFile-insert current date here-.txt. With that in mind we go ahead and repeat the same process we just finished: we grab the month, day, and year, albeit this time for today’s date, not yesterday’s date. Once we have all that data we construct a new file name and store that value in the variable strNewFileName:

strNewFileName = "C:\\Test\\BackupFile-" & strMonth & strDay & strYear  & ".txt"

Now all we need is one line of code and the Rename method and we can rename the file:

errResult = objFile.Rename(strNewFileName

And there you have it.

Incidentally, we realize that many of you were unable to make it to the Scripting Son’s baseball game, the one played in the torrential downpour. (But don’t worry: he’s got a good 50-60 games left this season.) If you’re interested in knowing what the game was like, here’s a handy hint: get in the car, drive for 50 miles, then sit down in a chair and have someone pour buckets of water over your head for the next two hours. And then get in the car and drive back home. That’s pretty much all there was to it.

Note. So are the Scripting Guys a little tired of spending all their time underwater? No, not really. After all, our careers have been underwater for years; we might as well join them.


How Can I Rename Files Based on the File’s Creation Date?

Hey, Scripting Guy! I have a folder that contains a bunch of log files. I would like to rename each file based on the date the file was created; for example, if a file was created on December 1, 2004, I’d like to rename it 20041201.log. Is that possible?

-- CK

Hey, CK. Listen, when it comes to scripting, anything is possible. (Well, except for the things that aren’t possible, of course.) And while dates can often times be tricky to work with in your WMI scripts, this is one time when WMI’s date format actually makes it easy to carry out a task.

To help explain how to do this, let’s break the task into two pieces. First, how do we list all the files in a folder, as well as their creation dates? Well, one way is to use a script similar to this:

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

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

For Each objFile In FileList
    Wscript.Echo objFile.CreationDate
Next

In this script, we bind to the WMI service and then use an association query to return a collection of all the files in the folder C:\Logs. If you aren’t familiar with association queries, they do pretty much what the name implies: they provide a way to associate two separate WMI classes, in this case, Win32_Directory and CIM_DataFile. The net effect is that this enables us to get back a collection of all the files in the associated folder.

After we have all the files, we simply create a For-Each loop, and then echo the value of the CreationDate property for each of these files. Because WMI uses the UTC (Universal Time Coordinate) format, we’ll get back values that look like this:

20041201202723.695209-480

Sure, that’s weird-looking, but guess what: the first eight characters happen to correspond to the year, month, and day, the very values you want to use as file names. You want a file name like 20041201? Take a look at the first eight characters in our UTC date: 20041201202723.695209-480. Just what the doctor ordered.

In fact, to get the file name, all we need to do is use VBScript’s Left function to grab those first eight characters in our UTC date. This modified script takes those first eight characters, stores them in a variable named strDate, and then echoes that value:

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

Set FileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='c:\Logs'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In FileList
    strDate = Left(objFile.CreationDate, 8)
    Wscript.Echo strDate
Next

So much for step 1. Now it’s time for the second – and final – step: renaming each of the files. Let’s show you the script, and then explain how it works:

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

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

For Each objFile In FileList
    strDate = Left(objFile.CreationDate, 8)
    strNewName = objFile.Drive & objFile.Path & strDate & "." & "log"
    errResult = objFile.Rename(strNewName)
Next

The tricky part here lies in constructing the new file name. When you rename a file using WMI, you can’t just specify a new name; for example, we can’t simply rename the file Wednesday.log to 20041201.log. Instead, we have to specify the entire path, and rename the file C:\Logs\20041201.log. That’s what this line of code does:

strNewName = objFile.Drive & objFile.Path & strDate & ".log"

What we’re doing here is combining the drive where the file resides (C:), the folder path on that drive (\Logs\), the new name for the file (20041201, the value of our variable strDate), and the new file extension (.log). Put those all together, and you end up with C:\Logs\20041201.log, which just happens to be the complete path we want to assign the file. All we have to do now is call the Rename method and we’ll change the file name.

Incidentally, we know it seems a bit odd to have to supply the entire path just to rename a file. If there’s a bright side to this, however, it’s the fact that you can use this same approach to move files to another folder. For example, suppose you not only wanted to rename the files, but you also wanted to move them to a folder name C:\Backups. In that case, you could use a script similar to this, which replaces the current file path (\Logs\) with the new path (\Backups\). Run this script, and the files will not only be renamed, they will also be moved to C:\Backups:

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

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

For Each objFile In FileList
    strDate = Left(objFile.CreationDate, 8)
    strNewName = objFile.Drive & "\Backups\" &  strDate & ".log"
    errResult = objFile.Rename(strNewName)
Next

The only problem with the script we’ve shown you is that it will blow up if you have files with the same creation date; after all, the script will try to create two files named, say, 20041201.log, and the file system just doesn’t like to have identically-named files in the same folder. If that’s an issue, here’s a sample script that checks to see if a file name is unique before it actually renames the file. For example, suppose the folder already has a file named 20041201.log. If that happens, this script (which we won’t explain in detail today) will create a new file name: 20041201_2.log and check to see if that file exists. If it does, it will try 20041201_3.vbs, and continue trying until it creates a unique name. At that point it renames the file:

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

Set FileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='c:\Logs'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In FileList
    strDate = Left(objFile.CreationDate, 8)
    strNewName = objFile.Drive & objFile.Path & _
       strDate & "." & "log"
    strNameCheck = Replace(strNewName, "\", "\\")

    i = 1
    Do While True
        Set colFiles = objWMIService.ExecQuery _
            ("Select * from Cim_Datafile Where Name = '" & strNameCheck & "'")
        If colFiles.Count = 0 Then
            errResult = objFile.Rename(strNewName)
            Exit Do
        Else
            i = i + 1
            strNewName = objFile.Drive & objFile.Path & _
                strDate & "_" & i & "." & "log"
            strNameCheck = Replace(strNewName, "\", "\\")
        End If
    Loop
Next

Yes, it’s a little complicated, which is why we’re saving the explanation for another day. Hey, you don’t really expect to put in any overtime on this column, do you?


How Can I Rename Files Like _JKG1234

How Can I Rename Files Like _JKG1234.jpg to 272_DSC_1234.jpg?

Hey, Scripting Guy! How can I rename files like _JKG1234.jpg to 272_DSC_1234.jpg?

-- NA

Hey, NA. You know, NA, we feel sorry for you. Why? Well, on the one hand you’re unbelievably lucky. After all, the Scripting Guys never write scripts that they actually use, yet you somehow managed to hit upon an exception. Talk about defying the odds! At the same time, however, you’re unbelievably unlucky: after all, the forces of the cosmos were perfectly aligned for you, yet you used up all that good karma on sending a question to the Scripting Guys instead buying yourself a lottery ticket. Ouch!

Two years ago one of the Scripting Guys went to Japan to watch his son play baseball. (Let’s face it, there’s a baseball angle to almost everything this Scripting Guy does.) While in Japan our Scripting Guy took a bunch of digital photos, photos that were automatically assigned names like 100_1023.jpg and 100_1024.jpg. This Scripting Guy was way too lazy to rename each and every photo, but he did think it might be useful if he at least modified the name to make it clear that the picture was part of the Japanese baseball collection. In other words, he wanted to take a photo named 100_1023.jpg and rename it Japan_1023.jpg. And, believe it or not, he actually used a script to perform this task.

And you guys thought the Scripting Guys talked the talk without walking the walk.

Not this Scripting Guy, though (or at least not on this one occasion). Instead, he used a script very similar to this one to rename his picture files:

strComputer = "."

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

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

For Each objFile In colFiles
    strEnd = Right(objFile.Name, 8)
    strNewName = objFile.Drive & objFile.Path & "272_DSC_" & strEnd
    errResult = objFile.Rename(strNewName)
Next

A couple things to note here. First, we’re assuming that you want to rename all the files in a particular folder. If that’s not the case, then you’ll have to include additional code to filter out files that shouldn’t be renamed. Second, we’re assuming that each photo ends with four digits and a .jpg file extension. If that’s not true, (for example, if you have files with names like _JKG1.jpg), well, then this particular script won’t help you. You can still rename files that have names like that, but the code is a bit more complicated. But seeing as how all the digital cameras we’re familiar with tack a four-digit number at the end of the file name, we’ll assume that’s true in your case as well.

The script begins by connecting to the WMI service on the local computer, then uses an Associators Of query to return a collection of all the files in the folder C:\Photos:

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

Admittedly, an Associators Of query is weird-looking. But don’t worry about that; instead, just replace C:\Photos with the path to your picture files and leave everything else alone.

We then use this block of code to walk through the collection of picture files:

For Each objFile In colFiles
    strEnd = Right(objFile.Name, 8)
    strNewName = objFile.Drive & objFile.Path & "272_DSC_" & strEnd
    errResult = objFile.Rename(strNewName)
Next

Inside the loop we take a look at the Name of the first file; that’s going to be something like C:\Photos\_JKG1234.jpg. (In WMI, the Name property of a file is what we would call the file path.) We then use the VBScript Right function to grab the rightmost eight characters in the Name; in this case, that will be 1234.jpg (C:\Photos\_JKG1234.jpg). Those are eight characters we want to preserve (we don’t want to change the numbering or the file extension), so we stash them in a variable named strEnd.

Next we construct a new name for our file; that’s what we do here:

strNewName = objFile.Drive & objFile.Path & "272_DSC_" & strEnd

When you rename a file using WMI you need to specify the complete file path; you can’t just rename a file something like NewName.jpg. Because of that we need to concatenate the drive letter for the file (C:), the path (in WMI terms, the folder path without the drive letter, or \Photos\), the new file name prefix (272_DSC_), and the eight characters we preserved from the existing file name. In other words:

C: + \Photos\ + 272_DSC_ + 1234.jpg

Put them all together and they spell C:\Photos\272_DSC_1234.jpg. And that’s good, because that’s exactly what we want the new file path to be.

From there we simply call the Rename method, passing the new file path as the sole method parameter. We then loop around and repeat the process for the remaining files in the collection.

And you know what, NA? We agree with you: knowledge like this is far more valuable than a winning lottery ticket. Just keep telling yourself that: far more valuable.


How Can I Rename Files Using File Names I’ve Written to a Text File?

Hey, Scripting Guy! I have a bunch of files with names like 001.jpg and 002.jpg. I also have a text file that includes the names I would like each of these files to have; for example, 001.jpg should be named Picture_of_Family_Members.jpg. How can I write a script that reads information from the text file and then renames each picture accordingly?

-- RG

Hey, RG. You know, one day Paul Simon was in a Chinese restaurant when he noticed that one of the selections on the menu was “Chicken With Egg.” Duly inspired, a short time later he wrote the classic song Mother and Child Reunion. As Casey Stengal said, you can look it up.

Now, no doubt you’re wondering what this has to do with your question. To tell you the truth, we have no idea; it’s just a piece of trivia we’ve always found interesting.

And no, it never did occur to us that other people might not find it so interesting. We’ll have to think about that a bit.

While we ponder that possibility, here’s a little something that might be a bit more relevant to your question:

Const ForReading = 1

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

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\names.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    arrParts = Split(strLine, ",")
    strFile = "C:\\Pictures\\" & arrParts(0)
    Set colItems  = objWMIService.ExecQuery _
        ("Select * From CIM_Datafile Where Name = '" & strFile & "'")
    For Each objItem in colItems
        strNewName = "C:\Pictures\" & arrParts(1)
        objItem.Rename strNewName
    Next
Loop

objFile.Close

Before we go any further we should note that this script is perhaps a tad bit more complicated than needed; had we chosen to use the FileSystemObject to rename the files we could have cut down on both the number of lines of code as well as the cryptic nature of some of those lines. So then why didn’t we use the FileSystemObject? Well, we wanted a script that could rename the files even if those files were stored on a remote computer. Because the FileSystemObject isn’t designed for remote operations, that left us with WMI as the technology of choice. And that’s both good and bad. The good part? This script can easily be modified to work against a remote machine. The bad part? You have to be willing to put up with an eccentricity or two when it comes to locating and renaming files using WMI.

As for the code itself, we start out by defining a constant named ForReading and setting the value to 1; we’ll use this constant later on to open the text file for reading (as opposed to opening it for writing or for appending). We then use these lines of code to connect to the WMI service on the local computer:

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

Note. What’s that? Connect to the WMI service on a remote machine? We never said – oh, that’s right: we did say something about using this script against remote machines, didn’t we? Well, that’s easy enough: just set the value of the variable strComputer to the name of the remote machine. For example, this line of code will cause the script to connect to (and thus rename the files on) a remote computer named atl-fs-01:

strComputer = "atl-fs-01"

Next we need to open our text file. Here we use – well, what do you know: the FileSystemObject. Like we said, we don’t want to use this object to rename the files; however, it’s about our only recourse when it comes to reading a text file. With that in mind, these two lines of code create the FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Names.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\names.txt", ForReading)

Now let’s roll up our sleeves and get to work. We’re assuming your text file looks something like this, with the current file name listed first and the preferred file name listed second:

001.jpg,Picture_of_Family_Members.jpg
002.jpg,At_the_Airport.jpg
003.jpg,Our-Hotel.jpg

With the file open we set up a Do Until loop that will simply read the file in line-by-line until there is nothing left to read. (Or, if you want to impress all your friends and neighbors, reads the file line-by-line until the AtEndOfStream property is true.) To do that, we start out by reading the first line in the file and storing the value in a variable named strLine:

strLine = objFile.ReadLine

That means that strLine is equal to this:

001.jpg,Picture_of_Family_Members.jpg

And that means we need to separate the current file name from the preferred file name. Can we do that? Heck no. But the VBScript Split function can:

arrParts = Split(strLine, ",")

What the Split function does is examine a string and chop it into separate pieces, placing each piece in an array we named arrParts. How does the function know where to make each chop? That’s easy: the second parameter passed to Split (“,”) tells the function to make a chop every time it encounters a comma. With the first line read in from the text file that means we’ll end up with an array consisting of two items:

· 001.jpg

· Picture_of_Family_Members.jpg

Because the first item in a VBScript array always has the index number 0, we now know two things: item 0 – or arrParts(0) – represents the current file name, and item 1 – arrParts(1) – represents the desired file name.

And once we know that we can better understand what this line of code does:

strFile = "C:\\Pictures\\" & arrParts(0)

In a moment we’re going to use WMI to connect to the file C:\Pictures\001.jpg, the first file listed in the text file and the first file to be renamed. What we’re doing here is creating the full path to the file, something we do by combining C:\\Pictures\\ and the name of the file (which, as we know, is item 0 in our array).

Good question: why do we have \\’s in the path? Well, we’re going to use this path in a WMI query and, as it turns out, the \ is a reserved character in WMI. The only way to use a reserved character in a WMI query is to “escape” it; that is, to preface it with a \. As you can probably guess, a \ prefaced with another \ gives us a \\. It’s crucial that we escape each \ in the path. (And, yes, if we were dealing with a UNC path like \\atl-fs-01\public we’d have to convert it to this: \\\\atl-fs-01\\public. What a strange and wonderful world we live in.)

Speaking of WMI queries, here’s the one we use to connect to the file 001.jpg:

Set colItems  = objWMIService.ExecQuery _
    ("Select * From CIM_Datafile Where Name = '" & strFile & "'")

All we’re doing with this line of code is asking WMI to return a collection of files that have the Name C:\Pictures\001.jpg (although, for the purposes of the query, we have to call it this C:\\Pictures\\001.jpg). Because path names must be unique (you can’t have two files named C:\Pictures\001.jpg) we can rest assured that this query will bind us to the desired file.

Another good question: what if the file C:\Pictures\001.jpg doesn’t exist? That’s fine; in that case our collection will simply be a collection with 0 items in it. In turn, the script will skip the section of code that loops through the collection and instead simply go off and read in the next line from the text file.

In case you’re wondering, the section of code that loops through the collection looks like this:

For Each objItem in colItems
    strNewName = "C:\Pictures\" & arrParts(1)
    objItem.Rename strNewName
Next

We already know that, at most, we’re going to have a single item in the collection: the file 001.jpg. That means we can go ahead and rename the file. To do that we first construct a new file path, a path consisting of C:\Scripts plus the new file name (which, as you recall, is item 1 in our array):

strNewName = "C:\Pictures\" & arrParts(1)

At that point we call the Rename method, passing the variable strNewName as the sole parameter:

objItem.Rename strNewName

And then it’s back to the beginning of the loop, where we repeat the process with the next line in the text file.

Admittedly, this might seem a bit complicated. But all we’re really doing is this:

· Reading a line from a text file.

· Picking out the existing file name and the new file name.

· Connecting to the existing file.

· Renaming the existing file.

See? It’s really not all that bad after all.

Anyway, we hope that helps, RG. Right now we have to go look up more baseball and rock and roll trivia. You know, people here at Microsoft don’t appreciate all the hard work and research that goes into writing a column like this one ….


How Can I Replace a Local File with a Newer Version Found on a File Server?

Hey, Scripting Guy! How can I compare the modification date of the local file with its counterpart on a file server and then, if the local file is older, replace it with the version on the file server?

-- DC

Hey, DC. Let’s see, you want to take something old and replace it with something new. For some reason, that sends a shiver up and down our spines.

Um, not because the Scripting Guys are getting old, mind you; we’re not. After all, knees and backs are supposed to creak like that; how else would you know that they’re working?!?

Note. According to legend, when Eskimos become old and useless they are abandoned on an ice floe and left to fend for themselves. None of the Scripting Guys happens to be an Eskimo, but we all tend to wear coats, gloves, and stocking hats to work every day, summer included. Just in case.

But you’re probably more concerned with out-of-date files than you are with out-of-date Scripting Guys, aren’t you? Here’s a script that will replace the local file C:\Scripts\Test.txt if that file happens to be older than its counterpart on the server atl-fs-01:

Const
OverwriteExisting = TRUE

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objLocalFile = objFSO.GetFile("c:\scripts\test.txt")
dtmLocalDate = objLocalFile.DateLastModified

Set objServerFile = objFSO.GetFile("\\atl-fs-01\public\test.txt")
dtmServerDate = objServerFile.DateLastModified

If dtmLocalDate < dtmServerDate Then
objFSO.CopyFile objServerFile.Path, objLocalFile.Path,
OverwriteExisting
End If

As you can see, this is a pretty simple little script. (We have to admit that the fact that it’s really easy to get rid of old, worn-out things worries us a little bit.) We start out by creating a constant named OverwriteExisting and set the value to True; we’ll use this constant to tell the FileSystemObject that it’s OK to overwrite an existing instance of our target file. By default, the FileSystemObject will not copy a file from drive 1 to drive 2 if that file already exists on drive 2.

Speaking of the FileSystemObject, we create an instance of this object (Scripting.FileSystemObject) on the next line of the script. We then use these two lines of code to bind to our first file (C:\Scripts\Test.txt) and store the last modified date of the file (the DateLastModified property) in a variable named dtmLocalDate:

Set objLocalFile = objFSO.GetFile("c:\scripts\test.txt")
dtmLocalDate = objLocalFile.DateLastModified

We then repeat this process by creating an object reference to the server-version of Test.txt. Notice that we use a different variable name here: the object reference to the local file is stored in objLocalFile and the object reference to the server file is stored in objServerFile. Needless to say, we also use a different variable (dtmServerDate) to store the last modification date:

Set objServerFile = objFSO.GetFile("\\atl-fs-01\public\test.txt")
dtmServerDate = objServerFile.DateLastModified

Still with us? Next we need to determine whether the local file is older than the server file. That’s what this line of code is for:

If dtmLocalDate < dtmServerDate Then

Don’t be confused by the syntax here. It’s easy to assume that the older file would have a date greater than the newer file. But that’s not how things work with dates. Suppose File A has a modification date of 2/1/2006 and File B has a modification date of 2/15/2006. File A is older, which means its modification date is less than (that is, occurred before) File B.

So suppose our local file is older than its server counterpart. How do we replace the local file with the copy found on the server? Here’s how:

objFSO.CopyFile objServerFile.Path, objLocalFile.Path, OverwriteExisting

As you can see, all we have to do is call the CopyFile method and pass it three parameters:

· The path to the file we want to copy (that is, the version of Test.txt found on the server).

· The path we want to copy this file to. In this case, that will be the path to the local file.

· The constant OverwriteExisting, which tells the script to go ahead and replace the local file with the version copied over from the server.

And there you have it. Remember, though, this script works only with old files. You can’t use it to replace, say, old Scripting Guys. (Not that anyone would ever even dream of replacing the Scripting Guys, of course. But, just in case ….)


How Can I Retrieve a List of All the Files That Have No File Extension?

Hey, Scripting Guy! How can I retrieve a list of all the files that have no file extension?

-- JB

Hey, JB. Funny you should ask about no-hitters. Yes, it’s true: the Scripting Son was robbed of a no-hitter this past Saturday. With one out in the top of the seventh (you play just seven innings in Colt League baseball), the Scripting Son had a no-hitter going. The next batter dribbled a ball down the third base line. It was clearly a foul ball; at first the batter didn’t even run but just stood there and watched. Unfortunately, though, the home plate umpire didn’t see it that way. He called the ball fair and, after a mad scramble, the batter managed to get safely to first. Officially, it was a base hit and, officially, the no-hitter was over.

Note. Uh, no, it’s probably best if we don’t relate the opinions of the Scripting Son regarding this call. Let’s just say that he was happy the team won, and leave it at that.

What’s that? Yes, we noticed that the question said “no file extension” rather than “no-hitter.” We just assumed that was a typo. Surely no one could be thinking about work at a time like this. (The time, of course, being baseball season.)

On the other hand, it’s theoretically possible that someone might be more interested in a scripting than they are in hearing about the pitching exploits of the Scripting Son. (Unlikely, but still possible.) Just in case JB really did mean “no file extension,” here’s a script that will take care of that:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Extension = ''")

For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

Yes, it’s very easy. (After all, we’re entering the home stretch of the baseball season; we don’t have time to write long and complicated scripts!) As you can see, we begin by connecting to the WMI service on the local computer. Is it possible to connect to a remote computer and find all the files that don’t have file extensions? Of course it is: just assign the variable strComputer the name of the remote machine. For example, this modified line of code will cause the script to search the computer atl-fs-01 for files without file extensions (not to be confused with Doctors Without Borders, Parents Without Partners, or A Day Without Sunshine):

strComputer = "atl-fs-01"

After making the connection we then issue the following query:

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where Extension = ''")

What we’re doing here is querying the CIM_DataFile class, the WMI class used for managing files. Of course, we don’t want a list of all the files on the computer, just those that don’t have a file extension. To do that, we simply tack on a Where clause that limits returned data to files where the Extension property is equal to nothing (that is, an empty string: ‘’).

Our query returns a collection of all the files that don’t have a file extension. To review that list we set up a For Each loop to walk through the collection, and then echo back the Name (path) for each file found there. It’s that easy.

Note. Could we echo back other file properties besides Name? Sure; see the Microsoft Windows 2000 Scripting Guide for details.

Here’s an equally sad story for you. Last Thursday one of our pitchers had a no-hitter after four innings. In the top of the fifth, the leadoff batter hit a ground ball right to the shortstop. Our shortstop took a little too much time, and then kind of lobbed the ball over to first. The batter beat it out for an infield single, the first – and only – hit the other team had all night. In other words, that’s twice in the span of just three days that we could have – and probably should have – had no-hitters. But we ended up with nothing at all.

Maybe we’ll give our pitchers a copy of the script that finds files that don’t have file extensions. That should cheer them up!


How Can I Retrieve the Subject Property for a .MSI File?

Hey, Scripting Guy! How can I retrieve the Subject property for a .MSI file?

-- J L-G

Hey, J L-G. Before you ask, the Scripting Guy who writes this column has no comment on the January 8th Florida-Ohio State football game. That’s primarily because this Scripting Guy refuses to recognize the “Bowl Championship Series” as being anything other than a bunch of football games. National championship? Not without a playoff; no way. (And had there been a playoff, USC would likely have won it all, as painful as that is to admit.)

Besides, the Scripting Guy who writes this column doesn’t like either Florida or Ohio State; he only watched the game because, well, what else are you supposed to do in the evenings?

What’s that? Read a book, do household chores, spend quality time with the family? Hey, we’re supposed to be the comedians around here!

Note. For those of you keeping track, yes, just yesterday the Scripting Guy who writes this column noted that he didn’t like the Dallas Cowboys. And yes, he’s also pointed out that he doesn’t like the Oakland Raiders. And – well, to simplify matters, let’s put it this way: other than the Washington Huskies and (usually) the Seattle Seahawks and the Seattle Mariners, the Scripting Guy who writes this column doesn’t like anybody or anything.

And if the Mariners continue to shell out millions of dollars to relief pitchers who go 1-2 with an 8.68 ERA, well ….

Of course, if we aren’t going to talk about college football then what can we talk about? Retrieving the Subject property for a .MSI file? Well, sure, we can do that. But we have to warn you, there really isn’t much to talk about:

Set objInstaller = CreateObject("WindowsInstaller.Installer")
Set objProduct = objInstaller.SummaryInformation("C:\Scripts\FP11.MSI")

Wscript.Echo "Subject: " & objProduct.Property(3)

Like we said, there isn’t much to discuss here. We start out by creating an instance of the WindowsInstaller.Installer object; that’s an object that can bind to – and retrieve information from –.MSI files. We then use this line of code to grab all the SummaryInformation values for the file C:\Scripts\FP11.MSI:

Set objProduct = objInstaller.SummaryInformation("C:\Scripts\FP11.MSI")

After that we simply echo back the value of property 3, which just happens to be the value of the Subject property:

Wscript.Echo "Subject: " & objProduct.Property(3)

That’s going to give us output similar to this:

Subject: Microsoft Office FrontPage 2003

Pretty slick, huh?

And sure, you can retrieve other types of summary information using the WindowsInstaller.Installer object. In fact, you can retrieve all these summary information values:

Set objInstaller =
CreateObject("WindowsInstaller.Installer")
Set objProduct =
objInstaller.SummaryInformation("C:\Scripts\FP11.MSI")

Wscript.Echo "Code page: " &objProduct.Property(1)
Wscript.Echo "Title: " & objProduct.Property(2)
Wscript.Echo "Subject: " & objProduct.Property(3)
Wscript.Echo "Author: " & objProduct.Property(4)
Wscript.Echo "Keywords: " & objProduct.Property(5)
Wscript.Echo "Comment: " & objProduct.Property(6)
Wscript.Echo "Template: " & objProduct.Property(7)
Wscript.Echo "Last Author: " & objProduct.Property(8)
Wscript.Echo "Revision number: " & objProduct.Property(9)
Wscript.Echo "Edit Time: " & objProduct.Property(10)
Wscript.Echo "Last Printed: " & objProduct.Property(11)
Wscript.Echo "Creation Date: " & objProduct.Property(12)
Wscript.Echo "Last Saved: " & objProduct.Property(13)
Wscript.Echo "Page Count: " & objProduct.Property(14)
Wscript.Echo "Word Count: " & objProduct.Property(15)
Wscript.Echo "Character Count: " & objProduct.Property(16)
Wscript.Echo "Application Name: " & objProduct.Property(18)
Wscript.Echo "Security: " & objProduct.Property(19)

Two things we should note. First, this only works for .MSI files; you can’t use same script to retrieve summary information for, say, a .JPG file. (So how can you retrieve summary information for a .JPG file? Well, here’s one way.) Second, some of these property names are misleading; for example, Word Count actually refers to the type of source image file you’re dealing with, and has nothing to do with the number of words in the .MSI file. For more information, take a peek at the Windows Installer Reference documentation on MSDN.

Oh, and one more thing: this object works only on the local computer. For better or worse, you can’t create a remote instance of WindowsInstaller.Installer.

Now, where were we? Oh, yes. To be honest, the Florida-Ohio State game was pretty dull; there was a flurry of action early on, but after that Ohio State all-but disappeared. From then on the only excitement for the evening occurred, at least for those of us in the Seattle area, when commercials for one of the local news channel aired. “LOWLAND SNOW!!!!! GUSTS OF WINDS TOPPING 40 MILES AN HOUR!!!! BITTER ARCTIC COLD!!!!! WE’RE ALL GOING TO DIE!!!!!”

Holy smokes; are we headed for the Apocalypse or what?

As it turns out, the Seattle area could get some high winds, though nothing like the sustained windstorm of a few weeks ago. And, after the wind dies down, we might get a little snow, though, then again, we might not. As for bitter, arctic cold? Well, that part is true, as long as you consider highs in the 30s and lows in the mid 20s as being “bitter arctic cold.” Let’s just hope that the Seattle area never gets hit by a real emergency. After all, instead of being the voice of calm and reason our local news channels will probably encourage everyone to riot in the streets and to loot all the local stores. “IT’S EVERY MAN FOR HIMSELF!!!!! KILL OR BE KILLED!!!!! EVERYONE START PANICKING RIGHT NOW!!!!!”

On the bright side, though, having previously hosted the World Trade Organization Ministerial Conference, well, rioting and looting is one thing we Seattleites are prepared for.


How Can I Save a File Based on the Day of the Week?

Hey, Scripting Guy! I need to save data to a text file each evening. How can I determine the day of the week and then save the file in the appropriate folder? For example, when the script runs on a Thursday then the file should be saved to C:\Logs\Thursday\Results.txt.

-- TW

Hey, TW. You know, the problem with the Scripting Guy who writes this column is that he – OK, fine: one of the problems with the Scripting Guy who writes this column is that he’s never in the right place at the right time. For example, a year or two ago there was a research study in which people were paid not to use the Internet. Or at least that was the idea: the researchers weren’t more than a day or two into the study before their subjects began to experience withdrawal symptoms. In the end, the researchers had to merely limit Internet use: none of their subjects could stop using the Internet altogether, even though they were being paid to do so.

Meanwhile, the Scripting Guy who writes this column just came back from a week off, a week spent partly at Mom and Dad’s and partly at home. Did he use the Internet at any time during this week? No. Did he use a computer at any time during this week? No. In fact, other than the lawn mower (which he only used grudgingly), he didn’t use much technology of any kind during those nine days. But did anyone pay him for staying away from technical gizmos and gadgets? You already know the answer.

Editor’s Note: We’re betting the Scripting Guy used the refrigerator quite a bit. Given that Mom and Dad live a hundred miles or so away we’re betting he didn’t walk there, so some technology was involved in the trip. There’s probably some sort of filtration system on the swimming pool he’s been bragging about. It was a paid vacation. No, we’re not feeling too sorry for the Scripting Guy.

All of which means it’s important to be in the right place at the right time: it’s important to the Scripting Guy (who could have been paid for doing nothing had he been in the right place at the right time) and it’s important for you: you need to save your files into the right place at the right time. The problem for the Scripting Guy is that no one will pay him for doing nothing. (Well, technically Microsoft pays him and, in return, he usually does nothing. But that’s different.) Your problem, meanwhile, is this: how do you determine the day of the week and then save your files to the appropriate folder? Here’s how:

strWeekDay = Weekday(Date)
strDay = WeekdayName(strWeekDay)

strPath = "C:\Logs\" & strDay & "\Results.txt"

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile =
objFSO.CreateTextFile(strPath)

objFile.WriteLine "This is a test."
objFile.Close

Of course it’s a simple little script. After all, the Scripting Guy who writes this column was only gone for a week; that’s not enough time to forget how to be lazy. That’s why this was the perfect question for him: he didn’t have to do much work, yet he was still able to provide a solution. And best of all, he got paid for it.

Note. Or at least he assumes he got paid for it. After all, Microsoft uses electronic deposit, and this Scripting Guy never bothers to verify that his check actually got deposited. Maybe the folks at Microsoft are smarter than he gave them credit for ….

As it turns out, the key lines in this script come right at the beginning:

strWeekDay = Weekday(Date)
strDay = WeekdayName(strWeekDay)

In line 1, we use the Weekday function to determine the weekday integer value corresponding to the current date. For example, suppose the current date is Thursday, August 10. In that case, Weekday returns a 5, which happens to be the value for Thursday. Where can you find a complete set of integer values? Why, right here, of course:

Value | Day of the Week
1 | Sunday
2 | Monday
3 | Tuesday
4 | Wednesday
5 | Thursday
6 | Friday
7 | Saturday

Of course, that’s all fine and dandy, except for one thing: a 5 doesn’t really do you much good, does it? Because it’s a Thursday you want your data stored in the folder C:\Logs\Thursday, not C:\Logs\5. That’s a problem.

Or at least it would be, if it wasn’t for line 2, where we use the WeekdayName function to convert that 5 to the string value Thursday:

strDay = WeekdayName(strWeekDay)

After this line of code executes, the variable strDay will be equal to Thursday, which – fortunately for us – just happens to be the name of the folder where the file needs to be saved.

We should point out that, yes, we used two lines of code to retrieve the string value of Thursday. However, we could have performed this same task in a single line by “nesting” the functions:

strDay = WeekdayName(Weekday(Date))

In this construction we use the Weekday function to determine the integer value of the current date; we then immediately apply the WeekdayName function to the returned value in order to get the string Thursday. We’re able to do this because any time you nest functions VBScript begins with the innermost set of parentheses and works its way out. In other words, the script first runs the Weekday function and grabs the integer value for the current date; only after that function is complete does the script run the WeekdayName function and determine the string value corresponding to the integer value we got back.

Why did we use two lines when one line would do? We like to make things as clear as possible when trying to explain these scripts and how they work. If that means writing an extra line of code or two, well, that’s fine.

After all, it’s not like we’ve got too much other work to do.

After we have the string value corresponding to the day then we use this line of code to construct the file path:

strPath = "C:\Logs\" & strDay & "\Results.txt"

Nothing too special there: we simply combine the following items in order to create the path:

· C:\Logs\

· Thursday (the value of the variable strDay)

· \Results.txt

The rest of the script then creates a sample text file and saves it to the proper folder:

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile(strPath)

objFile.WriteLine "This is a test."
objFile.Close

That’s not very exciting, but we tossed it in just so you could see that the script really does work.

Well, OK: come to think of it is a little exciting, isn’t it?

Note: One thing to keep in mind is that this script doesn’t actually create the folder. (What do you expect right after vacation?) If the folder C:\Logs\Thursday doesn’t already exist when you run this script, you’ll get a “Path not found” error.

This should work just fine, but there is one caveat here: because you’re using the generic file name Results.txt it won’t be long (a week, in fact) before you start running into file name collisions. For example, on August 10th you save a file to C:\Logs\Results.txt. On August 17th you’re going to try to do the same thing, except the file C:\Logs\Results.txt will already exist. That might be fine: you might want to overwrite the old file, or you might have removed the old file sometime before the 17th.

If it’s not fine, well, no problem: here’s a slightly-modified script that tacks the date on to the end of the file name. That gives us a name like Results_08-10-2006.txt:

strWeekDay = Weekday(Date)
strDay = WeekdayName(strWeekDay)

strDate = Replace(Date,"/","-")

strPath = "C:\Logs\" & strDay & "\Results_" & strDate & ".txt"

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile(strPath)

objFile.WriteLine "This is a test."
objFile.Close

The main thing we’ve done here is add a line of code that takes the current date, replaces the /’s with hyphens, and then stores the resulting value in a variable named strDate:

strDate = Replace(Date,"/","-")

Why do we replace all the / characters? That’s easy: those characters aren’t allowed in file names. Hyphens are allowed, however, so we simply use the Replace function to swap in a hyphen anytime we see a /. We start out with a date like 8/10/2006, and end up with a string value like 8-10-2006.

Note. Depending on your Regional and Language options you might not have to do this; for example, some localized versions of Windows already use a hyphen to separate date parts. But that’s OK; this script will still work just fine. That’s because no error is generated if the Replace function can’t find any /’s to replace. Like the Scripting Guys, the Replace function is perfectly content to do nothing at all.

And, of course, we also need to add the variable strDate to the line of code where we construct the file path:

strPath = "C:\Logs\" & strDay & "\Results_" & strDate & ".txt"

Hope that answers your first question, TW. As to your unasked second question – whether or not the Scripting Guy who writes this column is happy to be back to work – let’s think about that for a moment. Last week he was out in the sun, taking an occasional dip in his parents’ swimming pool, playing Wiffle ball, staying up late and sleeping in. This week he’s getting up at 6:30 every morning and sitting in an office for 8 hours. You know, it’s hard to choose between those two options, isn’t it?


How Can I Search for Multiple File Extensions in a Single WQL Query?

Hey, Scripting Guy! How can I search for multiple file extensions in a single WQL query?

-- AH

Hey, AH. If we understand your question correctly, you want to know if it’s true that the Seattle Seahawks are playing in the Super Bowl this Sunday. Yes, it is true: our very own Seahawks will be taking on … well, some second-place team. This is actually a great time for sports fans to be living in Seattle: the Seahawks are in the Super Bowl; the University of Washington men’s basketball team is ranked 15th in the country; the UW’s women’s volleyball team won the national championship; the Seattle Sonics –

Um, did we mention that the women’s volleyball team won the national championship?

Actually, upon further review we suspect that maybe you’d like to know how to search for multiple file extensions in a single WQL query. Well, then why didn’t you say so:

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_Datafile Where Extension = 'mp3' OR Extension = 'wma'")

For Each objFile in colFiles
    Wscript.Echo "Name: " & objFile.Name
Next

As you can see, this is about as simple a WMI script as you can get. We start out by connecting to the WMI service on the local computer. We then issue a query that looks for files of two types: those with the file extension mp3 and those with the file extension wma. After retrieving the files we then set up a For Each loop to walk through the collection and echo back the name of each file.

There are two important things to note here. First, the name of the WMI property is Extension (and no, we don’t know why they didn’t call it FileExtension). Second, keep in mind that we are searching for files with the extension mp3; we are not searching for files with the extension .mp3. Make sure you leave the dot out; otherwise your script won’t return any data.

Note. One other thing: running queries against the entire file system can take a long time, something that depends on the number of files on your machine. That doesn’t mean you can’t or shouldn’t do it; we just wanted to remind everyone that a script like this could easily take several minutes (or more) to complete. (Editor’s Note: Now he tells us.)

So what’s the secret to searching for multiple file extensions? Well, think about your task: you want a list of files that have the file extension mp3 or that have the file extension wma. When you write the task out like this the key word is the word or; that’s also true when you write out the WQL query. Let’s take a look at the query again:

Set colFiles = objWMIService.ExecQuery _
("Select * from CIM_Datafile Where Extension = 'mp3' OR
Extension = 'wma'")

There you have it: Where Extension = 'mp3' OR Extension = 'wma'. The key is remembering that you want file extension mp3 or you want file extension wma. Often-times people make the mistake of thinking, “I want all the files that have the file extension mp3 and I want all the files that have the file extension wma.” As a result, they end up writing a query like this:

Set colFiles = objWMIService.ExecQuery _
("Select * from CIM_Datafile Where Extension = 'mp3'
AND Extension = 'wma'")

We don’t have any idea what files you have stored on your computer, but we already know how many files this query will find: 0. Why? Because what we’re saying here is this: find all the files that have the file extension mp3 and that also have the file extension wma. Needless to say, there aren’t going to be many files with two file extensions. Don’t get AND and OR mixed-up. Use OR when you want to expand the search, AND when you want to contract the search.

Note, too that you need to specify the property each time: Extension = 'mp3' OR Extension = 'wma'. If you try to take shortcuts your script won’t work: Extension = 'mp3' OR 'wma'.

Finally, we should point out that you can use the OR operator with things other than file extensions. For example, here’s a query that returns a list of all the services that are either stopped or paused:

Set colFiles = objWMIService.ExecQuery _
("Select * from Win32_Service Where State = 'Stopped'
OR State = 'Paused'")

Likewise you can include more than just two OR clauses. Also interested in files with the extension wav? Then just add it to the query:

Set colFiles = objWMIService.ExecQuery _
("Select * from CIM_Datafile Where Extension = 'mp3' OR
Extension = 'wma' OR Extension = 'wav'")

Oh, and one other thing: Go Seahawks!


How Can I Take Ownership of a File or Folder By Using a Script?

Hey, Scripting Guy! How can I take ownership of a file or folder using a script?

-- RV

Hey, RV. Interesting question. In the past, the Scripting Guys have tried to take ownership of a Porsche 911, beachfront property in Hawaii, and the New York Yankees; all of those efforts failed. Considering our track record, we weren’t sure what would happen if we tried to take ownership of a file or folder. (Although as long as we weren’t issued yet another restraining order we figured it couldn’t be too bad.)

As it turns out, taking ownership of a file or folder is child’s play. (Assuming, of course, that the child in question has been granted the NTFS permission enabling him or her to take ownership of the file or folder.) The following script might not be as good as a beachfront property in Hawaii, but it’s awfully close:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

For Each objFolder in colFolders
    objFolder.TakeOwnershipEx
Next

If you’re thinking, “Man, this looks like a run-of-the-mill WMI script,” well, you’re right: it is a run-of-the-mill WMI script. We start off by connecting to the WMI service on the local computer (although we could just as easily run this script against a remote computer). We then use this line to retrieve a collection of folders; in this case, all the folders that have the name C:\Scripts:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

Two things to note here. First, because we want to take ownership of a folder we use the Win32_Directory class. What if we wanted to take ownership of a file? Well, then we’d need to use the CIM_DataFile class. Here’s an example of a query that would bind us to the file C:\Scripts\Test.vbs:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From CIM_DataFile Where Name = 'C:\\Scripts\\Test.vbs'")

Second, note that we have to double-up any \ characters in our path; thus C:\Scripts becomes C:\\Scripts. That’s because the \ is a reserved character in WMI; because of that, any time we need to use a \ in a WQL query we have to “escape” that character. In this case, “escape” is just a fancy way of saying use two \’s instead of one. (Or, in the case of a UNC path, four \’s instead of two; thus \\atl-fs-01\public becomes \\\\atl-fs-01\\public.)

As you know, calling the ExecQuery method gives us a collection of folders (although this particular collection contains only a single folder, C:\Scripts). To take ownership of each item in the collection we set up a For Each loop and then, for each of those items, call the TakeOwnershipEx method:

For Each objFolder in colFolders
    objFolder.TakeOwnershipEx
Next

The TakeOwnershipEx method enables us to take ownership not only of the folder C:\Scripts but also any subfolders found in C:\Scripts. If you don’t want to take ownership of the subfolders then use the TakeOwnership method instead. In that case your For Each loop would look like this:

For Each objFolder in colFolders
    objFolder.TakeOwnership
Next

So there you go, RV. The next time some guy whizzes past you in a Porsche remember this: he might have the fancy sports car, but you have a script that enables you to take ownership of a file or folder. If that doesn’t wipe the smile off the face of Mr. Porsche Driver we don’t know what will.


How Can I Tell If a File Exists on a CD or DVD Drive?

Hey, Scripting Guy! How can I tell whether a file exists on any CD or DVD drive connected to a computer?

-- GH

Hey, GH. Well, one way to do this would be to search the entire file system – including any mapped network drives – and check to see if a file (let’s call it Budget.xls) can be found anywhere. Assuming you find such a file, you can then use some additional code to determine whether or not that file happens to be on a CD or DVD drive.

That would work, but it’s not very elegant and – depending on the size of your file system – could take awhile to complete. A much better method would be to tell your script to look for that file only on CD or DVD drives. And guess what: that’s exactly the approach we’re going to take.

What, you expected the Scripting Guys to do something that wasn’t elegant and sophisticated?

We’ll want to start by looking for the CD/DVD drives that are connected to the computer in question, and discovering the drive letter for each of those items. Fortunately, that’s easy to do; this little WMI script reports back the drive letters for all the CD/DVD drives connected to a computer:

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

Set colItems = objWMIService.ExecQuery("Select * from Win32_CDROMDrive")

For Each objItem in colItems
    Wscript.Echo objItem.Drive
Next

After we’ve identified the drive letters we can put together a WMI query that looks for a file only on those specified drives. Let’s take a look at the completed script, and then we’ll explain how it works:

On Error Resume Next

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

Set colItems = objWMIService.ExecQuery("Select * from Win32_CDROMDrive")

For Each objItem in colItems
    strDrive = objItem.Drive
    Set colFiles = objWMIService.ExecQuery _
        ("Select * From CIM_DataFile Where FileName = 'Budget' " & _
            "AND Extension = 'xls' AND Drive = '" & strDrive & "'")

    If colFiles.Count > 0 Then
        For Each objFile in colFiles
            Wscript.Echo objFile.Name
        Next
    End If
Next

As you can see, we start off by retrieving a collection of all the CD/DVD drives on the computer. For each drive found (and there could be more than one), we assign the value of the Drive property (which, as we noted earlier, represents the drive letter) to a variable named strDrive. We then use a second WMI query to search that drive for a file named Budget.xls. Here’s the code that does that:

Set colFiles = objWMIService.ExecQuery _
        ("Select * From CIM_DataFile Where FileName = 'Budget' " & _
            "AND Extension = 'xls' AND Drive = '" & strDrive & "'")

Admittedly, the query looks a little convoluted; that’s because WMI treats the file name (Budget) and the file extension (xls) as two separate properties. Because of that we have to use three parameters in our WHERE clause: the file name (Budget); the file extension (xls); and the drive we want to search. Notice that we don’t hard-code in a drive letter but instead we use the variable strDrive. Like we said, at first glance it’s a little complicated, but it works.

After issuing the query, and after waiting for the script to search the CD/DVD drive (which shouldn’t take more than 15-20 seconds, tops) we check to see how many files with the name Budget.xls were found. If the collection contains at least one file (colFiles.Count > 0) we echo the Name property for each file found. (If Count is equal to 0 that means we couldn’t find any files named Budget.xls, so we don’t do anything at all.) The script then loops around and searches the next CD/DVD drive in the collection. This continues until each CD/DVD drive has been searched, and any successes reported back.


How Can I Tell Whether a .GIF File is a Regular Picture File or an Animated Picture File?

Hey, Scripting Guy! I have a silly question for you: how can I tell whether a .GIF file is a regular old picture file or an animated picture file?

-- WW

Hey, WW. To begin with, there’s no such thing as a silly question. We’ve always believed that – well, come to think of it, last summer a neighbor’s smoldering firework caught one of the Scripting Guys’ garages on fire. As he stood there in the middle of the night, talking to the fire chief and watching the fire fighters put out the fire, another neighbor rushed up to him and said, “Oh, my gosh: did you know that your house is on fire!?!”

So, in retrospect, maybe there is such a thing as a silly question. We stand corrected.

To tell you the truth, WW, we didn’t find your question silly. However, we did assume that there was no possible way to retrieve that information using a script. After all, we can’t manage our wireless settings using a script; what do you suppose the odds are that we could use a script to determine whether or not a .GIF file was animated?

As it turns out, those odds are surprisingly good:

Set objImage = WScript.CreateObject("WIA.ImageFile")
objImage.LoadFile("c:\scripts\dr_scripto_anim.gif")

If objImage.IsAnimated Then
    Wscript.Echo "This is an animated image."
Else
    Wscript.Echo "This is not an animated image."
End if

The trick here is to use the Windows Image Acquisition (WIA) 2.0 library. Now, it’s possible that you don’t have WIA installed on your machine; if that’s the case you can download everything you need from here. Note that, for some reason, you need to download the entire WIA SDK (which is just a 520KB .zip file). Download the SDK and extract all the files to a folder of your choice, then use Regsvr32.exe to register the Wiaaut DLL file :

regsvr32 wiaaut.dll

Note. How would you know that you don’t have WIA 2.0 installed on your computer? One easy way to tell is to try running the script. If it fails with a “can’t create object” error then you don’t have WIA 2.0 installed.

Once you have WIA installed the rest is easy. We start out by creating an instance of the WIA.ImageFile object, then use the LoadFile method to bind to the file we want to check. (In this case, that’s C:\Scripts\Dr_Scripto_Anim.gif.) We then check the value of the IsAnimated property. If IsAnimated is True then we’re dealing with an animated picture file. If IsAnimated is False then our picture file is not animated.

Like we said, we had no idea you could even do this, let alone know that it would be this easy.

To be honest, we haven’t played around with the WIA scripting library very much. It looks interesting, though: after all, if you have the right equipment, you can use scripts to control such things as scanners and Web cameras. If that piques your interest, take a look at the Windows Image Acquisition SDK on MSDN.

Of course, once we got started checking to see if our picture files were animated, well, it was pretty hard to stop. With that in mind we thought we’d toss in a bonus script, one that checks all the files in a folder and reports back any that happen to be animated picture files. We won’t discuss this script in any great detail, but we will tell you what it does:

· It uses the FileSystemObject to bind to the folder C:\Scripts (using the GetFolder method).

· It creates an object reference named colFiles that contains all the files found in C:\Scripts.

· For each file in the folder it tries loading the file using the LoadFile method. As you might expect, LoadFile can only load image files; it can’t load, say, Word documents or executable files. Therefore each time the script calls LoadFile it checks the value of the VBScript Err object. If Err is equal to 0 that means the file loaded successfully; if Err is anything but 0 that means the file could be loaded.

· For each file successfully loaded the script checks the value of the IsAnimated property; it then echoes back the file name if IsAnimated is True. If a file cannot be loaded the script simply skips this step altogether.

· The script resets the Err object back to 0, then loops around and repeats the process with the next file in the collection.

Here’s what the code looks like:

On Error Resume Next

Set objImage = WScript.CreateObject("WIA.ImageFile")

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Scripts")

Set colFiles = objFolder.Files

For Each objFile in colFiles
    strFile = "C:\Scripts\" & objFile.Name
    objImage.LoadFile(strFile)
    If Err = 0 Then
        If objImage.IsAnimated Then
            Wscript.Echo objFile.Name
        End If
    End If
    Err.Clear
Next

And, in case you’re wondering, yes, this Scripting Guy was aware that his house was on fire. Painfully aware, as they say.


How Can I Unhide All the Files in a Folder?

Hey, Scripting Guy! How can I run a script to unhide all the files in a folder?

-- GA

Hey, GA. You know, when one of the Scripting Guys was a bit younger he and the other kids in his neighborhood spent countless hours playing variations on hide-and-seek; in fact, this Scripting Guy still has vivid memories of lying in a shallow ditch and having members of the other team walk right over him, without even knowing that he was there. (That ploy worked so well that he’s seriously considering digging a shallow ditch in his office and hiding in there any time someone comes looking for him.)

Back in those days you wouldn’t have wanted a script that could automatically unhide everything; that might have been useful, but wouldn’t have been much fun. As a system administrator, however, you probably don’t find it all that enjoyable to play hide-and-seek with your files. You would probably welcome a script that could automatically unhide all the files in a folder, a script like, say, this one:

strComputer = "."

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

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

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each objFile In FileList
    Set objFile = objFSO.GetFile(objFile.Name)

    If objFile.Attributes AND 2 Then
        objFile.Attributes = objFile.Attributes XOR 2
    End If
Next

This is kind of an unusual script in that we combine WMI and the FileSystemObject. (Sure, it sounds dangerous, but it’s OK: after all, we’re trained professionals.) We start off by connecting to the WMI service on the local computer (we’ll talk about that in a minute), and then use this query to retrieve a collection of all the files (that is, all the instances of the CIM_DataFile class) found in the folder C:\Scripts:

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

We use WMI to retrieve the collection of files because we find it quick and easy and because – in most cases – we can use the same script to go out and retrieve a collection of files from a remote computer. Unfortunately, though, WMI can’t be used to unhide files; the CIM_DataFile class does include a property named Hidden, but that property is read-only. That’s why we have to use both WMI and the FileSystemObject: we use WMI to grab the collection of files, then we use the FileSystemObject to unhide those files.

Sadly, that also means that this script must run on the local computer; that’s because the FileSystemObject, unlike WMI, isn’t designed to work remotely. Bummer.

At any rate, after retrieving our collection we create an instance of the FileSystemObject and then set up a For Each loop to walk through the set of files. Inside that For Each loop we bind to each individual file using this line of code:

Set objFile = objFSO.GetFile(objFile.Name)

As you can see, we simply call the GetFile method, passing it the value of the Name property (which we retrieved using WMI). The Name property is equivalent to the file path; thus a file “name,” at least in WMI, will be something like C:\Scripts\My_file.txt.

After binding to a given file we then check to see whether the file is hidden or not; hidden or not hidden happens to be part of the file’s attributes. With this line of code we check to see if the “switch” for the hidden attribute is on or off. If the switch is on and the file is hidden, then this statement will be True. If the switch is off and the file is not hidden then this statement will be False:

If objFile.Attributes AND 2 Then

Note. File attributes are stored as a bitmask property. We can’t go through all the ins and outs of bitmask properties in this column, but you’ll find a reasonably good explanation of bitmasks and how to work with them in the Microsoft Windows 2000 Scripting Guide.

Why do we care whether the file is already hidden? Well, the easiest way to unhide a hidden file is to flip the switch from on to off; in fact, this line of code does that very thing:

objFile.Attributes = objFile.Attributes XOR 2

However, the XOR operator isn’t particularly intelligent: it just flips a switch from one state to another. If the switch is on, it turns it off; if the switch is off, it turns it on. That’s why we check the current state of the file. If the file is hidden then we want to use XOR to flip the switch from on to off. But what if the file isn’t hidden? In that case, we don’t want to flip the switch; that would result in us hiding the file. (Because XOR would flip the switch from off to on.) Thus we check the value of each file before we start flipping switches.

Run this script and – olly olly oxen-free! – all the files that were previously hidden will be visible once again. And then it will be your turn to hide and see if those files can find you.


How Can I Use Windows PowerShell to Delete All the .TMP Files on a Drive?

Hey, Scripting Guy! How can I use Windows PowerShell to locate and delete all the .tmp files on a drive?

-- OR

Hey, OR. You know, there’s an old saying: Give a man a fish and he’ll eat for a day; teach that man to fish and he’ll eat for the rest of his life.

Well, unless he’s like the Scripting Guy who writes this column and doesn’t like seafood. In that case he probably won’t even want to learn to fish. And there’s no point in giving him a fish, because he won’t eat it. Instead, he’ll end up starving to death and you’ll spend the rest of your life wracked by guilt. “Why did I give him a fish?” you’ll think. “Why didn’t I give him a hamburger or something? This is all my fault, isn’t it?”

Yes, it is.

The point is – well, to tell you the truth, we’re no longer sure what the point is. Oh, now we remember. Can we show you how to use Windows PowerShell to locate and delete all the .tmp files on a drive? Of course we can. But then you’d run a script for just one day. But if you learned Windows PowerShell then you could run scripts for the rest of your life. (You’ll still be hungry, but at least you’ll have something to take your mind off food.)

In other words, if you’re interested in Windows PowerShell then you owe it to yourself to attend Windows PowerShell Week, November 6-10, 2006. This week is designed with one purpose in mind: to introduce you to Windows PowerShell. We’ll have a series of webcasts (one each day, Monday through Friday); we’ll have a virtual lab where you can try your hand at Windows PowerShell; and we’ll have a few other assorted goodies (like a “labcast,” which is a combination virtual lab and webcast). It will be fun, it will be exciting, and, with any luck, it will be educational as well. We won’t teach you how to fish, but we will teach you everything you need in order to get started with Windows PowerShell.

Well, that’s a good point: you never know what Peter will do during a webcast, do you? So maybe Peter will teach you to fish; we’ll just have to wait and see.

Update: Windows PowerShell Week is over, but you can watch the webcasts on-demand anytime.

So, other than fishing, what kinds of things will you learn during Windows PowerShell Week? Why, things like how to locate and delete all the .tmp files on a drive:

get-childitem c:\ -include *.tmp -recurse | foreach ($_) {remove-item $_.fullname}

Believe it or not that is the entire command; as you can see, if you don’t enjoy typing then Windows PowerShell is like a dream come true. What we have here are actually two commands crammed into one: we first retrieve a collection of all the .tmp files on drive C, then we go ahead and delete each of those files. How do we do that? Let’s see if we can figure that out.

To begin with, we use the Get-ChildItem Cmdlet to retrieve a collection of all the .tmp files on drive C. That’s what we do here:

get-childitem c:\ -include *.tmp -recurse

This is actually fairly straightforward. We call Get-ChildItem (which, when working with the file system, functions somewhat similar to the dir command) and pass it three parameters:

· c:\, which represents the starting folder for our search.

· -include *.tmp, which tells Get-ChildItem to return only files that have a .tmp file extension. Suppose we also wanted to include files that have a .temp file extension. That’s fine; all we’d have to do is add .temp to the list of included files: -include *.tmp, *.temp.

· -recurse, which tells Get-ChildItem to look for files in all of the subfolders of C:\ (and all the sub-subfolders of those subfolders, and so on). And yes, that is cool: you don’t have to write some complicated recursive function to get at the files in the subfolders of a folder. Instead, just tack on the –recurse parameter.

After we’ve retrieved our collection we “pipe” it over to the second half of our command (the | character represents the pipeline). Needless to say, we’ll talk about piping in much more detail during Windows PowerShell Week. For now we’ll just say that, by itself, Get-ChildItem typically retrieves a collection and then displays the returned information onscreen. However, we aren’t interested in seeing the .tmp files displayed on screen; we want to delete those files. Therefore, instead of displaying the items on screen we ask Get-ChildItem to hand over (pipe) the entire collection to the second half of our command.

In case you’ve forgotten, the second half of our command looks like this:

foreach ($_) {remove-item $_.fullname}

What we’re doing here is setting up a For Each loop to loop through the collection handed over by Get-ChildItem. Don’t worry too much about the syntax for now; we’ll discuss For Each loops in much more detail during Windows PowerShell Week. We will note, however, that the $_ is a special Windows PowerShell variable that represents the individual items in the pipeline. When we say foreach ($_) that’s equivalent to a VBScript statement like this:

For Each objItem in colItems

In other words, we’re just looping through all the files in the collection.

And what are we going to do to each of those files? Why we’re going to use the Remove-Item Cmdlet to delete them, of course:

{remove-item $_.fullname}

Here we call Remove-Item and pass, as the sole parameter, the FullName property of each individual file in the collection. (FullName is equivalent to the file path.) Believe it or not, that’s all we have to do; Remove-Item takes care of the rest for us.

What’s that? You say this sounds cool, but, seeing as how you’re new to Windows PowerShell, you’re a little nervous about running a script that deletes a bunch of files? Hey, no problem. If that’s the case, just tack the –whatif parameter on to the Remove-Item command, like so:

get-childitem c:\ -include *.tmp -recurse | foreach ($_) {remove-item $_.fullname -whatif}

That –whatif parameter is pretty neat: instead of deleting the .tmp files it simply tells you what would happen if you really did ask Remove-Item to delete all the files. In other words, you’ll get back a report of the files that would be deleted if you ran the command for real:

What if: Performing operation "Remove File" on Target "C:\WINDOWS\SET3.tmp".
What if: Performing operation "Remove File" on Target "C:\WINDOWS\SET4.tmp".
What if: Performing operation "Remove File" on Target "C:\WINDOWS\SET8.tmp".

It’s like fantasy football, only with Windows PowerShell.

At any rate, we – oops, we need to go: Peter just walked by wearing hip waders and carrying a fishing pole. Admittedly, that’s not all that unusual for Peter. But we better find out what he’s up to, just in case.


How Can I Use Windows PowerShell to Locate All the .WMA Files on My Computer?

Hey, Scripting Guy! How can I use Windows PowerShell to locate all the .wma files on my computer?

-- LC

Hey, LC. Harry Potter. Hogwarts. Ron Weasley. Dumbledore. Quiddich.

What’s all that about? Oh, nothing: just a few words that idly floated through the head of the Scripting Guy who writes this column. And before you ask, no, those words are not a less-than-subtle jab at the Scripting Editor, with whom he recently had a … difference of opinion … a difference of opinion revolving, in part, around these fictional constructs. As you might expect, the Scripting Guy who writes this column has nothing but the utmost respect for the Scripting Editor and for her opinions.

Even if she is a bit of a Muggle.

So, no, there’s no hidden agenda here. If anything, the Scripting Guy who writes this column mentioned Harry Potter simply because Harry and his friends are well acquainted with magic, and the Scripting Guy likes to think that the answer to your question is equally magical:

Get-WMIObject Win32_LogicalDisk -filter "DriveType = 3" |
Select-Object DeviceID |
ForEach-Object {Get-Childitem ($_.DeviceID + "\") -include *.wma -recurse}

Note. Of course she’ll fall for that; why wouldn’t she?

As you’ve no doubt heard, Windows PowerShell is a new command shell/scripting language released by the good folks here at Microsoft. (If you haven’t heard about Windows PowerShell, you soon will; in fact, one of these days we’ll have an important announcement right here in the Script Center.) Windows PowerShell is an interesting technology, and it has some definite strengths. Does that make it “better” than VBScript? That’s like asking whether vanilla is better than chocolate: it depends on who you are and what you like. Either way, however, there’s no doubt that Windows PowerShell can easily solve this particular problem, and with a minimal amount of effort.

Note. For the record, chocolate is better than vanilla. Of course, the Scripting Editor will probably opt for flavorless gelatin as her favorite. But that’s OK; she’s fully-entitled to her opinion.

Even if it is completely wrong.

Let’s get down to business here. In order to locate all the .wma files on a computer we need to do three things:

· Obtain a list of all the hard drives installed on the computer.

· Grab the drive letter for each of these drives.

· Search each drive for all the .wma files.

With that in mind, why don’t we start with step 1: obtaining a list of all the hard drives installed on the computer. (By the way, even though this looks like a couple different lines, the entire command should be typed on the same line.)

Although there are a couple different ways we could retrieve the list of hard drives we decided to use WMI and the GetWMIObject cmdlet; that’s not only an approach that we knew would work, but also an approach that should be reassuringly-familiar to those of you who are proficient in VBScript but are new to Windows PowerShell. Here’s the command that retrieves the list of disk drives found on the local computer:

Get-WMIObject Win32_LogicalDisk -filter "DriveType = 3"

As you can see, there’s nothing too terribly difficult here. To begin with, we simply call the Get-WMIObject cmdlet, followed by the name of the WMI class we want to connect to (Win32_LogicalDisk). We then add the –filter parameter, which is pretty much a WMI Select * From query minus the Select * From and the word Where. In VBScript we’d use a query that looks like this:

"Select * From Win32_LogicalDisk Where DriveType = 3”

In Windows PowerShell we use a filter that looks like this:

"DriveType = 3"

See? Nothing even remotely difficult about that.

When we run this command we get back a collection consisting of all the properties of all the hard disks on the computer (any logical disk with a DriveType of 3 is a hard disk). That’s great, except that all we really want is the drive letter for each of these hard disks. Therefore, we “pipe” this collection to the Select-Object cmdlet (the | symbol represents the pipeline). What does that mean? That means we use Get-WMIObject to grab the collection, but instead of doing anything further using Get-WMIObject we hand the collection over to Select-Object. We then use that cmdlet to select DeviceID, the only property we care about:

Select-Object DeviceID

We still have a collection consisting of all the hard drives on the computer; it’s just that we’ve removed all the property values except for DeviceID. That leaves us with a collection similar to this:

C:
D:
E:

What we need to do now is pipe that collection once more, this time handing the trimmed-down collection to the ForEach-Object cmdlet. The ForEach-Object cmdlet will then loop through the set of hard disks, using the Get-ChildItem cmdlet against each drive in the collection. Get-ChildItem, as the name implies, is a cmdlet that can retrieve all the items found in a specified container, including all the files and subfolders in a folder.

Here’s what that command looks like:

ForEach-Object {Get-Childitem ($_.DeviceID + "\") -include *.wma -recurse}

In other words, for each drive we call Get-ChildItem, binding the cmdlet to the root folder (for example, C:\). In order to specify the root folder we use $_.DeviceID followed by a \. In case you’re wondering, in Windows PowerShell $_ is a special variable that represents the object currently in use; the first time through the loop, that means that $_.DeviceID will be C:, the DeviceID property for item one in the collection.

In addition we pass Get-ChildItem a pair of parameters:

· -include *.wma. This is a filter parameter that restricts returned data to files with a .wma file extension.

· -recurse. This parameter causes Get-ChildItem to automatically search all the subfolders of the target folder; because the target folder is C:\ that will result in the cmdlet searching all the folders on drive C.

Note. Yes, in order to search all the folders on drive C: all you have to do is add the –recurse parameter; there’s no need to write a recursive subroutine like you do when using WMI or the FileSystemObject. Hmmm, maybe this is magic after all!

After we run Get-ChildItem on drive C we then loop around and do the same thing with the next drive in the collection. Just exactly the same way a For Each loop works in VBScript. (The syntax is different, but the concept is the same.)

By the way, you should find that this script runs pretty quickly. Give it a try and see for yourself.

Now, where were we? Oh, that’s right: Hagrid. Gryffindor. Voldemort. Hermione Granger.

Note. Yes, we don’t doubt that the Scripting Editor has a few words “randomly” floating through her head right about now. But as the Scripting Editor she has to censor things like that and can’t say what she really wants to say, at least not here on TechNet. Take that, Scripting Editor!

Editor’s Note. Yes, the Scripting Editor does have plenty to say to the Scripting Guy that can’t go on TechNet. But here are some things that can: Lawsuit; lawyers; copyright; trademark; unemployment; bankruptcy; living out of your car; living out of the Scripting Son’s car. (We finally hit on a really scary one didn’t we?)


How Can I Verify That a Folder Has Files in It and, If So, Delete Those Files?

Hey, Scripting Guy! How can I verify that a certain folder (C:\TestLog) has files in it and, if so, delete all those files?

-- DC

Hey, DC. Before we begin we should point out that – when using WMI – it doesn’t matter whether or not a folder has any files in it: you can still tell WMI to delete all the files in that folder. If no such files exist that won’t phase WMI in the least. Why are we telling you this? Just to point out that, if your only concern is deleting files, then you don’t have to worry about verifying that the folder actually has files. Instead, just delete away.

Of course, you might have other reasons for wanting to know whether or not a folder has any files in it. With that in mind, we came up with this script:

strComputer = "."

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

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

If colFileList.Count = 0 Then
    Wscript.Echo "There were no files in the folder."
Else
    Wscript.Echo "There were " & colFileList.Count & " files in the folder."

    For Each objFile In colFileList
        objFile.Delete
    Next
End If

The script begins by connecting to the WMI service on the local computer. If you’re wondering why we chose to use WMI to carry out this task (as opposed to, say, the FileSystemObject) here’s one reason: although this script is designed to work against the local computer we can easily modify it to work against any computer where we have local Administrator rights. All we have to do is change the value of the variable strComputer from a dot (WMI-speak for the local computer) to the name of the remote machine. You can’t do that with the FileSystemObject, at least not as easily.

After connecting to the WMI service we then use this line of code to retrieve a collection of all the files in the folder C:\TestLog:

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

And, yes, Associators Of queries are kind of funny-looking, but don’t let that bother you. All you really need to know is this: working with any other folder (for example, D:\Logfiles) is as easy as replacing the path C:\TestLog with the path D:\Logfiles:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='D:\Logfiles'} Where " _
        & "ResultClass = CIM_DataFile")

Of course, that’s all well and good, but how do we know whether or not C:\TestLog has any files in it? Well, our query returns a collection consisting, in this case, of all the files (instances of the CIM_DataFile class) found in the folder C:\TestLog. As it turns out, WMI collections have a property named Count which returns the number of items in the collection. We can determine whether or not the folder has any files in it just by checking to see if the Count is equal to 0:

If colFileList.Count = 0 Then

If the Count is equal to 0 that means there were no files in the folder; in that case, we echo a message saying that no files were in the folder. If the Count is not equal to 0 then we do two things. First, we use this line of code to report back the number of files that were in the folder:

Wscript.Echo "There were " & colFileList.Count & " files in the folder."

Second, we set up a For Each loop to walk through the collection of files. For each file in the collection we call the Delete method and delete that file:

For Each objFile In colFileList
    objFile.Delete
Next

When we’ve finished with the For Each loop all the files will be gone, and everyone will live happily ever after. (Except, of course, for those poor files we just wiped out. Hopefully they’ve all gone to a better place.)


Can I Use a Script to Determine the Size of a Folder on a Remote Computer?

Hey, Scripting Guy! Can I use a script to determine the size of a folder on a remote computer?

-- SS

Hey, SS. Boy, you’d think you’d be able to do that, wouldn’t you? Especially if you’ve glanced through the WMI class Win32_Directory and noticed the property FileSize. Based on that, you might think, “Well, obviously I could just write a WMI script to get the folder size.” In turn you might write a script very similar to this one, which tries to determine the size of the folder C:\Scripts on the computer atl-ws-01:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:" & "!\\" & strComputer & "\root\cimv2")
Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory where Name = " _
        & "'c:\\scripts'")
For Each objFolder in colFolders
    Wscript.Echo objFolder.FileSize
Next

Unfortunately, this script won’t do you any good, not because of any problem with the scripting code or the scripting syntax, but simply because the FileSize property doesn’t work. It doesn’t matter what folder you connect to and it doesn’t matter whether the folder is on the local computer or a remote computer: the FileSize will always come back Null.

We should mention that there is also a FileSize property in the CIM_DataFile class, the WMI class used for managing files. That FileSize property works; it will actually tell you the size, in bytes, of a file.

So what do we do now? Well, you can always use the FileSystemObject to determine the size of a folder. Here, for example, is a script that tells you the size of our C:\Scripts folder:

Set objFolder = objFSO.GetFolder("C:\Documents and Settings")
Set objFile = objFSO.CreateTextFile("c:\scripts\folder_size.txt")
Wscript.Echo objFolder.Size

The only problem is that the FileSystemObject is designed to work locally, and the folder you want to size is located on a remote computer. So do we know of a way to work around this problem? Well, we know at least one.

If you’re using Administrative shares (you know, like C$ and D$), then you can get at this folder information by connecting to the appropriate Administrative share. In other words, you can use a similar to this, which uses the UNC path \\atl-ws-01\C$\Scripts to get at the C:\Scripts folder on the remote computer:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("\\atl-ws-01\C$\Scripts")
Wscript.Echo objFolder.Size

This works great, provided you’re using Administrative shares. But what if you’ve disabled those shares on your computers? What then?

Well, to tell you the truth, you might be out of luck, unless you want to run your folder size script as a logon or logoff script. If you have a shared folder of some kind on the remote computer, you could also copy the folder size script to the remote machine, run it locally, grab the data, and then delete it again; we showed a similar technique in an earlier Hey, Scripting Guy! column.

And what if you don’t have any shared folders on the remote computer? Unfortunately, we don’t have much in the way of suggestions, other than perhaps searching the Internet for a third-party utility that can retrieve the size of files and folders on a remote machine.


How Can I Automatically Run a Script Any Time a File is Added to a Folder?

Hey, Scripting Guy! Is it possible to have a script automatically run any time a file is added to a specific folder?

-- MB

Hey, WM. Yes, this is possible, thanks to the magic of WMI events, which allow you to write a script to monitor for something of interest (like a file being added to a folder) and then take some action any time an event like that occurs. We don’t have time to discuss WMI events in any detail in this column, but they can be incredibly useful to script writers. Consequently, it might be worth your while to check out this Tales from the Script column.

In the meantime, here’s a script that monitors the C:\Scripts folder. Any time a file is added to this folder, the script responds by echoing the name of that new file:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & _
        strComputer & "\root\cimv2")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")
Do
    Set objLatestEvent = colMonitoredEvents.NextEvent
    Wscript.Echo objLatestEvent.TargetInstance.PartComponent
Loop

Again, there’s too much going on here to go through the script step-by-step, but what we are looking for are instances of the __InstanceCreationEvent class; instances of this class are automatically created any time a new managed object (that is, something WMI knows about) is created on a computer. Although the query is a little complicated, it boils down to this: we want to be alerted to any new items that show up in C:\Scripts. (Note that C:\\\\Scripts is not a misprint; you really need all four slashes here.)

We also need to mention that a WMI script like this works by “polling;” it periodically goes out and checks to see if any new files have been added to the folder. For this sample, we’re checking every 10 seconds (that’s what the WITHIN 10 represents) to see if there are any new files in the folder. If that’s too fast or too slow, you can change that value to anything you want. Keep in mind two things, however. For one, if you poll too often (say, every second), you’ll have a script that is constantly running, and could theoretically put a drain on your system resources.

Conversely, if you make the value too long, you might miss new files, assuming they get added and then deleted before the polling time expires. For example, say your script checks every 5 minutes for new files. If you add 100 new files and then delete all those files 3 minutes later, the script will never know that those files were added to the folder. That’s because scripts like this work by comparing the files that currently in the folder with the files that were in the folder the last time the script checked. Play around with the polling interval and see what works best for you.

As we noted, this script simply echoes the name of any new file added to C:\Scripts. What if you wanted to do something a little fancier any time a file is added to the folder? No problem; just replace this line of code with the code you want executed any time a new file is detected:

Wscript.Echo objLatestEvent.TargetInstance.PartComponent

By the way, would you prefer to be notified any time a file is deleted from C:\Scripts? That’s easy enough; just monitor C:\Scripts for new instances of the __InstanceDeletionEvent class:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & _
        strComputer & "\root\cimv2")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceDeletionEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")
Do
    Set objLatestEvent = colMonitoredEvents.NextEvent
    Wscript.Echo objLatestEvent.TargetInstance.PartComponent
Loop

Hey, we told you WMI events were worth knowing about!


How Can I Connect to a Folder When There’s an Apostrophe in the Folder Name?

Hey, Scripting Guy! I’m using the WMI class Win32_Directory to get information about folders on a computer. It works great, except when there’s an apostrophe in the folder name; then, it doesn’t work at all. How can I use WMI to connect to folders that have apostrophes in their names?

-- JO

Hey, JO. Ah, the apostrophe (also known as the single quote mark). It seems like such a simple little character, and yet it has probably caused script writers more grief than any other character on the keyboard. (Yes, even more than ~.)

The problem is that the apostrophe is a “reserved” character in both VBScript and WMI; that means these scripting technologies want to reserve the use of the apostrophe for themselves. That’s fine, except when dealing with folder names like Ken Myer’s folder or people names like O’Donnell or O’Hara. You want to use the apostrophe, because how else can you write the name Ken Myer’s Folder? Meanwhile, VBScript and WMI want exclusive use of the apostrophe for other reasons. As you might expect, that leads to problems.

To explain what we mean, let’s take a look at a script that does work. This one connects to the folder C:\Scripts and tells us whether or not that happens to be a hidden folder:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

For Each objFolder in colFolders
    Wscript.Echo "Hidden: " & objFolder.Hidden
Next

Notice in our WQL query that single quote marks are used to indicate the name of the folder we want to connect to:

Select * From Win32_Directory Where Name = 'C:\\Scripts

That’s one of the jobs of a single quote mark: it delineates string values within a WQL query.

Now let’s take a look at a script that doesn’t work, one that tries to connect to a folder named C:\Ken Myer’s Folder:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * From Win32_Directory Where Name = 'C:\\Ken Myer’s Folder")

For Each objFolder in colFolders
    Wscript.Echo "Hidden: " & objFolder.Hidden
Next

If you run this script, you’ll get an error. That’s because of the apostrophe in Myer’s. Because apostrophes are used to delineate the beginning and ending of strings, the script thinks this is our WQL query:

Select * From Win32_Directory Where Name = 'C:\\Ken Myer'

See? It hits the apostrophe and thinks the query is complete. It then encounters the rest of the folder name --’s Folder – and has no idea what to do; after all, that’s not valid VBScript or WMI code. And so the script fails.

So can we work around this problem? Yep. All we need to do is “escape” the apostrophe in Myer’s. When you escape a character you essentially tell the script, “Look, the next character coming up is one of your reserved characters, but I don’t want you to use it as a reserved character; I want you to use it– in this case – as a plain old apostrophe.” To escape a reserved character in a WQL query, we just insert a \ in front of that character. In other words:

Select * From Win32_Directory Where Name = 'C:\\Ken Myer\'s Folder’

Yes, it looks weird, but it works. What if we had a folder name like O’Hara’s Folder, one that has two apostrophes? In that case you just need to escape each apostrophe, like so:

Select * From Win32_Directory Where Name = 'C:\\O\’Hara\’s Folder’

Wouldn’t it have been better if Windows didn’t allow apostrophes in folder names, or if VBScript and WMI used a different character to delineate string values? Sure. But for now ….

Here’s the complete script that connects to C:\Ken Myer’s Folder and tells us whether or not the folder is hidden:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * From Win32_Directory Where Name = 'C:\\Ken Myer\’s Folder'")

For Each objFolder in colFolders
    Wscript.Echo "Hidden: " & objFolder.Hidden
Next

Incidentally, if you’re using the FileSystemObject (another way to work with files and folders), you don’t have to worry about this; the apostrophe doesn’t seem to bother the FileSystemObject. For example, if you want to know the size of Ken Myer’s Folder, use this code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Ken Myer’s Folder")
Wscript.Echo "Size: " & objFolder.Size

No escaping or other coding tricks required.


How Can I Copy the Folder Structure of a Folder?

Hey, Scripting Guy! How can I copy the folder structure of a folder (that is, a folder and all its subfolders) without copying the files as well?

-- AS

Hey, AS. If you’re one of the many people who simply can’t get through the day without knowing what the Scripting Guy who writes this column is up to as he traipses through Italy, well, here’s the latest update: today the Scripting Family hops on a plane and heads for Venice. And what if you’re one of the many people who can (and would actually prefer to) get through the day without knowing what the Scripting Guy who writes this column is up to as he traipses through Italy? Well, we have good news for you, too: the Scripting Money is beginning to run out, so it’s just a matter of time before the Scripting Guy who writes this column comes home and stops bothering you with details about his trip to Europe.

At which point he resumes bothering you with details about the Scripting Son’s baseball games.

But what if you’re one of those people who just wants to know how to copy the folder structure of a folder without copying all the files as well? Well, guess what? There’s good news for you, too: we just happen to have a script that does that very thing.

Hey, Scripting Guy!, the daily column with a little something for everyone!

Including the following script:

On Error Resume Next

Dim arrFolders()
intSize = 0

strComputer = "."

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

strFolderName = "c:\scripts"

GetSubFolders strFolderName

Sub GetSubFolders(strFolderName)
    Set colSubfolders = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder in colSubfolders
        strFolderName = objFolder.Name
        ReDim Preserve arrFolders(intSize)
        arrFolders(intSize) = strFolderName
        intSize = intSize + 1
        GetSubFolders strFolderName
    Next
End Sub

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each strFolder in arrFolders
    strFolderName = strFolder
    strNewFolder = Replace(strFolderName, "c:\scripts", "c:\test")
    Set objFolder = objFSO.CreateFolder(strNewFolder)
Next

Yes, we know: this is a scary-looking script, isn’t it? That’s because there’s no simple, straightforward method for getting at nested subfolders; that is, folders inside of folders inside of folders. (Or at least not in VBScript; in Windows PowerShell there is a simple, straightforward method for getting at nested subfolders.) Therefore, we need to use a recursive subroutine: a subroutine that can call itself, and as many times as it needs to. Recursive subroutines aren’t exactly intuitive, which explains why the code looks as scary as it does.

Our advice? If you’d like to know more about recursion, check out this section of the Microsoft Windows 2000 Scripting Guide. Otherwise, don’t worry too much about it: just use today’s script as-is, changing the names of the folders as needed.

What we’re going to do today is grab the name (or, more properly, the path) of every subfolder in C:\Scripts and stash that information in an array; after that, we’ll loop through the values in the array in order to copy that folder structure to C:\Test. With that in mind, the first thing we do is initialize a dynamic array named arrFolders, then assign the value 0 to a counter variable named intSize. That’s what’s going on here:

Dim arrFolders()
intSize = 0

After we connect to the WMI service on the local computer we assign the path of the parent folder (C:\Scripts) to a variable named strFolderName. What we’re going to do (in a minute) is execute a chunk of code that returns a collection of all the topmost subfolders for our parent folder. What do we mean by “topmost” subfolders? Well, suppose we have a folder structure that looks like this:

· C:\Scripts

· C:\Scripts\Folder 1

· C:\Scripts\Folder 1\Folder A

· C:\Scripts\Folder 2

· C:\Scripts\Folder 2\Folder B

If C:\Scripts is our parent folder (and it is), then the topmost subfolders are Folder 1 and Folder 2; these are the subfolders that are one level “down” from the parent folder. What about Folder A and Folder B, folders nested two levels from the parent? Well, that’s the problem: deeply-nested subfolders like this are difficult to get at. In turn, that’s why we need the recursive subroutine.

Got all that? Good. Now let’s have some fun:

GetSubFolders strFolderName

OK, so maybe “fun” wasn’t the right word; how about “Now let’s get to work retrieving all the folder and subfolder names”? What we’re doing here is calling our recursive subroutine (a subroutine named GetSubFolders), passing the path to the parent folder as the sole subroutine parameter. In case you’ve forgotten, the GetSubFolders subroutine looks like this:

Sub GetSubFolders(strFolderName)
    Set colSubfolders = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder in colSubfolders
        strFolderName = objFolder.Name
        ReDim Preserve arrFolders(intSize)
        arrFolders(intSize) = strFolderName
        intSize = intSize + 1
        GetSubFolders strFolderName
    Next
End Sub

Yes, this is where things start to get a little weird. Inside the subroutine the first thing we do is execute an Associators Of query that returns a collection of all the topmost subfolders of C:\Scripts. How do we know this query returns the subfolders of the folder C:\Scripts? Well, for one thing, we’re getting back instances of the Win32_Subdirectory class; instances of that class just happen to be subfolders. For another, we know that the parent folder must be C:\Scripts because that’s the value of the variable strFolderName.

That’s how we know that this query returns the subfolders of the folder C:\Scripts.

As we noted, the Associators Of query returns a collection of all the topmost subfolders in the folder C:\Scripts (for example, C:\Scripts\Folder 1 and C:\Scripts\Folder 2). Our next step is to set up a For Each loop that walks through this collection of subfolders. Inside that loop, we use this line of code to grab the folder Name (that is, the folder path) and assign that value to the variable strFolderName:

strFolderName = objFolder.Name

After that, we use the following block of code to resize the array arrFolders, taking care to maintain any existing data in that array. We assign the path to the first subfolder (e.g., C:\Scripts\Folder 1) to the first item (item 0) in the array, then increment the value of our counter variable intSize by 1:

ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1

Why do we increment the value of the counter variable? That’s easy. So far we’ve added one folder to the array, making that single item array item 0. When the time comes to add another folder to the array, that subfolder needs to be added as item 1 in the array; that means that the value of our counter variable intSize also needs to be equal to 1.

That brings us to this line of code:

GetSubFolders strFolderName

Yes, you have seen that line of code before: once again we’re calling the subroutine GetSubFolders, this time passing the value C:\Scripts\Folder 1 as the subroutine parameter. And yes, we’re doing that even though we’re already inside the subroutine GetSubFolders; such is the nature of a recursive function. What this is going to do is run the subroutine against the folder C:\Scripts\Folder 1, pulling out the topmost subfolders of that folder. And then the subroutine will run against those folders, looking for their subfolders. This recursive process will continue until we run out of folders in the folder tree, by which time the paths to all of these folders will have been added to the array arrFolders.

We know: we get a headache just thinking about that, too. Fortunately, however, we don’t have to think about it too much; instead, VBScript does all the hard work (like keeping track of where it is in the folder structure and what it needs to do next) for us.

So what happens when we do finish adding all the folder paths to the array arrFolders? Well, for starters, we create an instance of the Scripting.FileSystemObject object, the object used to create new folders. We then set up a For Each loop to walk us through each of the folder paths in arrFolders, folder paths similar to this:

· C:\Scripts\Folder 1

· C:\Scripts\Folder 1\Folder A

· C:\Scripts\Folder 2

· C:\Scripts\Folder 2\Folder B

Needless to say, those are the paths to the existing folders; what we need to do now is copy all those folders to C:\Test. Admittedly, there are several different ways we can do this; we decided the easiest approach was to start out by using the Replace function to replace each instance of C:\Scripts with C:\Test. For the first item in the array that’s going to give us the following folder path:

C:\Test\Folder 1

And guess what? That just happens to be one of the paths we need in order to copy the folder structure of C:\Scripts to C:\Test. That means that all we have to do now is call the CreateFolder method (passing along this new folder name as the method parameter). Just like that, we’ve copied part of the folder structure to C:\Test, but without copying any files. And then we simply loop around and repeat the process with the next folder in the array.

When all is said and done C:\Test will contain the following subfolders:

· C:\Test\Folder 1

· C:\Test\Folder 1\Folder A

· C:\Test\Folder 2

· C:\Test\Folder 2\Folder B

Which are the very subfolders C:\Test should contain.

As we noted earlier, today the Scripting Family heads to Venice. No doubt all of you are familiar with Venice, the city built smack-dab in the middle of the Venetian Lagoon. (Most of the buildings actually rest on wood pilings driven into the sea floor.) For much of the 20th century Venice was slowly sinking into the lagoon; this was due to artesian wells that were sunk along the outskirts of the town. A ban on artesian wells enacted in the 1960s has slowed the sinking process; however, experts remain divided on whether this has stopped the sinking process.

All of this, of course, is one reason why the Scripting Guy who writes this column has such an affinity for the city of Venice. Something that is slowly sinking away into nothingness? If that isn’t a perfect description of his career path at Microsoft, well, we don’t know what is.


How Can I Create a Compressed Folder and Add Files to It?

Hey, Scripting Guy! How can I create a compressed folder and add files to it?

-- SL

Hey, SL. You know, one thing that the Scripting Guys have learned over the years is that when people ask a question they often expect to get an answer to a different question. The classic example, of course, is when the Scripting Spouse says, “Do these jeans make me look fat?” To be honest, we’re not always sure what question the Scripting Spouse is asking here, but we know one thing: whatever that question is, it has nothing to do with that pair of jeans. (Don’t believe us? Then try answering this question by saying something on the lines of “Those jeans? Oh, yeah, those jeans make you look fat.”)

Note. At the risk of sounding sexist, if a Scripting Husband should ask a question like that then it’s perfectly fine to say that the jeans make him look fat. (Editor’s Note: Uh, no, that hasn’t been the experience of the Scripting Editor.) After all, most husbands will simply shrug and say, “OK. I guess I might as well have another doughnut then.” (Well, okay, that part is true.)

Of course, that’s also what most husbands would say if you told them that the jeans didn’t make them look fat. Or if you told them the lawn needed mowing or that the house was on fire.

Besides, fat looks good, at least on men.

Or so the male Scripting Guys keep telling themselves. (The female Scripting Guy wouldn’t dream of destroying their illusions.)

So did we have a reason for mentioning all this? Of course we did; after all, SL, in your question you asked about creating a compressed folder. If you’re talking about using the folder/file compression technology built into the NTFS file system that’s great: in that case we have an answer for you. However, if you’re talking about creating a “zipped” folder (a technology introduced in Windows XP), well, that’s a different story: unfortunately, you can’t create zipped folders programmatically. (Although it might be worth your while to check out this unofficial and unsupported script in the Community-Submitted Scripts Center.) We can compress folders for you (equivalent to right-clicking a folder, clicking Properties, clicking Advanced, and then selecting Compress contents to save disk space). However, we can’t zip folders for you.

In other words, if you were hoping to get an answer to a different question, well, unfortunately we don’t have an answer for you. At least we don’t have an answer other than, “You can’t.”

But as long as it’s compression you want then it’s compression you’ll get. For example, here’s a script that compresses the folder C:\Scripts and all its contents (files and subfolders alike):

strComputer = "."

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

For Each objFolder in colFolders
    errResults = objFolder.Compress
Next

As you can see, there isn’t much to this. We begin by connecting to the WMI service on the local computer (although, as usual, the script works equally well against remote computers). We then use this line of code to retrieve a collection of all the folders that have the path C:\Scripts:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

Two things to note about this query. To begin with, even though we need to specify the full path to the folder, you might note that we use the property Name in our Where clause. Why? One reason and one reason only: in WMI the Name property is equivalent to the file path. (If that’s the case then shouldn’t they have called this property Path rather than Name? Probably, but there’s not much we can do about that at the moment.)

Second, notice how we specify the folder Name (path): C:\\Scripts. The extra \ is important here; in fact, without it the script will fail. That’s because the \ is a reserved character in WMI; any time that character is used in a Where clause it must be “escaped” by prefacing it with another \.

Yes, we agree. But that’s yet another thing we can’t do much about at the moment.

As we noted, this query returns a collection of all the folders with the Name C:\Scripts (and, seeing as how folder names must be unique, that means our collection will have, at most, a single item). All we have to do then is set up a For Each loop to loop through all the items in the collection. Inside that loop we do just one thing: we call the Compress method to compress the folder and everything in it. All of that requires just three little lines of code:

For Each objFolder in colFolders
    errResults = objFolder.Compress
Next

Just like that, the folder - and everything in it - will be compressed.

Of course, yours was actually a two-part question, wasn’t it? Not only did you want to know how to compress a folder, but you also wanted to know how to copy files into that folder. Turns out that this is even easier. You don’t have to do anything special to add files to this compressed folder; after all, compressed or not, it’s still just a folder in the file system. For example, to copy the file D:\Test.ppt to your newly-compressed folder you could use a script similar to this:

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

Set colFiles = objWMIService.ExecQuery("Select * from CIM_DataFile Where Name = 'D:\\Test.ppt'")

For Each objFile in colFiles
   strCopy = "C:\Scripts\" & objFile.FileName & "." & objFile.Extension
   objFile.Copy(strCopy)
Next

We won’t take the time today to explain how this script works; for more information, see the Microsoft Windows 2000 Scripting Guide. For now we’ll simply note that not only does the file get copied to C:\Scripts, but that copy will be compressed as well; in fact, anything copied to or created in C:\Scripts will automatically be compressed. And this compression is pretty good, too: for example, this particular PowerPoint presentation has a compressed size of 152 KB compared to an uncompressed size of 439 KB. All in all, not too bad.

And speaking of sizes, we now have a question for you: does today’s column make us look fat? Go ahead, take as much time as you need to answer; we’ll just have a doughnut or two while we wait.


How Can I Create a Folder for Each User in My Domain?

Hey, Scripting Guy! How can I create a folder for each user in my domain? I’d like the folder name to consist of the user’s first initial and their last name (for example, KMyer).

-- GN

Hey, GN. You know, this has been an interesting week for the Scripting Guys. Why? Because absolutely nothing interesting has happened to any of the Scripting Guys this entire week. Well, OK, maybe Scripting Guy Dean Tsaltas has had an interesting week. But Dean is currently off in the wilds of Nova Scotia, and no one has heard from him in the past few weeks. If his week is going anything like our week, however, he’s no doubt sitting in the wilds of Nova Scotia thinking, “Man, this is boring. I wonder if anything interesting has happened to the other Scripting Guys. And why exactly am I sitting in the wilds of Nova Scotia?”

Fortunately, today is Friday (although you don’t actually get to read this particular column until the following Monday), and no doubt all sorts of exciting things will happen over the weekend to help snap the Scripting Guys out of their lethargy. For example, the Scripting Guy who writes this column is going to pull weeds; that should get the old juices flowing again! Scripting Guy Jean Ross is seriously considering packing a few things into boxes, although she’s afraid that this might be too much excitement for a single weekend. (Maybe this weekend she’ll get the boxes out, then save the actual packing for next week.) As for Scripting Guy Peter Costantini – well, to tell you the truth, the other Scripting Guys never ask too many questions about what Peter does over the weekend. There are some things we’re just better off not knowing about.

Of course, the bottom line is this: we have no amusing anecdotes or witty repartee we can use to spice up today’s column. The truth is, we have nothing at all for today’s column. Nothing.

Well, except a script that can create a folder for each user in your domain:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT givenName, sn FROM 'LDAP://DC=fabrikam,DC=com' WHERE objectCategory='user'"

Set objRecordSet = objCommand.Execute

Set objFSO = CreateObject("Scripting.FileSystemObject")

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    strInitial = Left(objRecordSet.Fields("givenName").Value, 1)
    strFolderName = "C:\Public\" & strInitial & objRecordSet.Fields("sn").Value
    Set objFolder = objFSO.CreateFolder(strFolderName)
    objRecordSet.MoveNext
Loop

As usual, we aren’t going to spend much time discussing the ins and outs of Active Directory search scripts: we’re far too lazy – uh, we simply don’t have the space in the daily column to do that. But have no fear; after all, we still have a two-part Tales from the Script series that tells you everything you need to know about searching Active Directory. And you are welcome to read that any time you want.

Note. Yes, that includes explaining everything from the Connection object to the Page Size property to the Searchscope property. We Scripting Guys may be boring, but at least we’re thorough.

Heck we might as well be thorough: we don’t have anything else to do.

About all we will do with the search portion of our script is take a quick peek at the SQL query that returns a collection of user information:

objCommand.CommandText = _
    "SELECT givenName, sn FROM 'LDAP://DC=fabrikam,DC=com' WHERE objectCategory='user'"

As you can see, all we’re doing here is selecting two attributes (givenName and sn) for all the objects in the fabrikam.com domain that meet a specific criterion. (Criterion, of course, being the singular form of criteria. Apparently we Scripting Guys aren’t just thorough, we’re also grammatical.) And just what is that criterion? It’s this: returned objects must have an objectCategory equal to user. That’s how we can return a collection of all the user accounts in our domain. (Well, along with the fact that we started our search in the domain root.) What if we wanted to return a list of all our computer accounts? Then we’d use the following WHERE clause:

WHERE objectCategory='computer'

And so on.

And why givenName and sn? That’s easy: those are the two Active Directory attributes that correspond to, respectively, the user’s first name and last name. Wouldn’t firstName have been a better attribute name than givenName? Probably. But, like it or not, we’re stuck with givenName and sn.

Note. OK, so then what other user account attributes are available from Active Directory? Hard as this might be to believe, we don’t know, at least not off the tops of our heads. But that’s OK, because we can always look this information up in the Microsoft Windows 2000 Scripting Guide or in the ADSI SDK. And so can you.

After setting up our query we then call the Execute method to invoke the command and return a recordset consisting of all the user accounts in the domain fabrikam.com.

True Microsoft story. Although the method really is named Execute Microsoft editors always try to change the name; they’re afraid that readers will think Microsoft is ordering up firing squads and other forms of execution. Which is silly; despite the name, no one from Microsoft is ordering executions. You have our word on that.

Well, OK, technically the Scripting Guys have ordered executions; however, we’ve never been able to find any one to carry out these executions for us. And we’re too chicken to do them ourselves.

Fortunately for us, the Scripting Editor isn’t really a Microsoft editor; she’s a good editor instead. Among other things, that means she’ll let us use the word execute, although she won’t actually execute anyone, either. Which, come to think of it, is probably fortunate for us as well ….

OK, where were we? Oh, right. After calling the Execute method (heh-heh; take that Microsoft editors!) we get back a recordset consisting of the first and last names of all the users in our domain. Because of that, our next step is to set up a Do Until loop that enables us to loop through the entire recordset (or, to make it sound as though we know what we’re talking about, until the recordset’s EOF – end-of-file – property is True):

Do Until objRecordSet.EOF

Oh, right: before we did that we also created an instance of the Scripting.FileSystemObject, the object needed to create our folders. Our apologies to Scripting.FileSystemObject for neglecting to mention this.

Inside the Do Until loop we use this line of code to grab the first letter of the user’s first name (i.e., their first initial):

strInitial = Left(objRecordSet.Fields("givenName").Value, 1)

As you can see, that’s pretty straightforward: we just use the Left function to grab the first character in the user’s givenName. And yes, the syntax for referring to recordset fields is a bit cumbersome: objRecordSet.Fields("givenName").Value. But don’t worry too much about that: just copy and paste the string and leave it at that.

Once we have the user’s first initial we can then construct the name of our first folder:

strFolderName = "C:\Public\" & strInitial & objRecordSet.Fields("sn").Value

Here we’re simply assigning a value to a variable named strFolderName. And what is that value? Well, assuming that the first user in our recordset is named Ken Myer that value will be C:\Public\KMyer. In other words,

C:\Public\ plus K (first initial of first name) plus Myer (sn, or last name) = C:\Public\KMyer.

At that point we call the CreateFolder method to create a new folder, passing the variable strFolderName as the sole method parameter:

Set objFolder = objFSO.CreateFolder(strFolderName)

From there we call the MoveNext method to move on to the next record in the recordset. Etc., etc.

You know, that’s a good question: if this has been an especially boring week what kind of exciting things usually happen to the Scripting Guys? Wow; where do we begin with a question like that? Let’s see, the Scripting Guy who writes this column usually .. uh, writes this column. Scripting Guy Jean Ross usually – well, never mind. She didn’t actually do that; she just saw it on TV. Scripting Guy Peter Costantini – OK, we’re not sure about that: what exactly do you do with a package of Roman candles, a Speedo swimsuit, and two cartons of grated cheddar cheese? (That’s OK; we’d rather not know the answer to that question.)

You know what? We’ll have to get back to you on this.


How Can I Create a Folder on a Remote Computer?

Hey, Scripting Guy! Can I create a folder on a remote computer using a script?

-- AN

Hey, AN. Life is full of mysteries. To name one, what possessed somebody to think that the world was clamoring for Clamato juice, an unholy concoction combining tomato juice and clam juice? (As long as we’re on the subject: clam juice?) To name another, why is it that neither the FileSystemObject nor WMI provides a straightforward way to create a folder on a remote computer?

To be honest, we don’t know the answer to either of these questions. However, we do know the answer to the question “Can I create a folder on a remote computer using a script?” The answer is yes, although you have to settle for using a bit of a workaround to accomplish that feat.

As we noted, neither the FileSystemObject nor WMI provides a way to create a folder on a remote computer. The FileSystemObject has a method for creating folders, but this object is pretty much restricted to working on the local computer. WMI has methods for copying folders, for moving folders, for renaming folders, and for doing just about everything else you can think of with folders … except, of course, for creating folders. So what can you do about that?

Well, what you can do is take advantage of WMI’s ability to create a process on a remote computer. Using WMI, you can connect to a remote machine (assuming you have local administrator rights, of course) and then run any piece of software that’s installed on that machine. For example, suppose you have a burning desire to run Calculator on the remote computer atl-ws-01. This code will do the trick:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")
errReturn = objWMIService.Create _
    ("calc.exe", Null, Null, intProcessID)

If the remote machine is running Windows 2000, you’ll see Calculator pop up onscreen. If the remote machine is running Windows XP or Windows Server 2003, Calculator will still run, but you won’t actually see anything onscreen; for security reasons, on XP and 2003 processes started remotely run in a hidden window. But if you call up the Task Manager, you’ll see that Calculator is alive and well.

So how does that help you? Well, as you probably know there’s an easy way to create a folder on the local computer; just type something like this at the command prompt:

md c:\newfolder

That command will create a new folder (md being short for make directory) named C:\Newfolder. So how do we create a new folder on a remote computer? One way is to simply run the md command on the remote computer. We can do that by:

· Using WMI to connect to the Win32_Process class on the remote computer.

· Using the Create method to create a process that runs the command md c:\newfolder.

Can we really do that? You bet; in fact, here’s a script that creates a new folder on a remote computer named atl-ws-01:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")
errReturn = objWMIService.Create _
    ("cmd.exe /c md c:\newfolder", Null, Null, intProcessID)

Note the four parameters passed to the Create method. These are:

Parameter | Description
cmd.exe /c md c:\newfolder | The command-line command for creating a new folder. Note that we have to include the cmd.exe in the command. That’s because md part of cmd.exe; it’s not a standalone executable file. Basically what we’re doing here is telling WMI to start cmd.exe and then, after the shell; is running, execute the md command.
Null | Used to set the working folder for the process. Because that’s irrelevant to our needs, we just pass a Null parameter
Null | Represents startup configuration for the process. Again, we don’t care about startup configuration information for this script, so we send another Null parameter
intProcessID | An “out” parameter that stores the Process ID for the process we create. Because the md command takes only a second or so to complete we don’t have much use for the Process ID. However, suppose we were performing a task that would take several minutes to complete. By knowing the Process ID, we could do things like monitor the progress of the task, and thus be notified when the process actually completed.


How Can I Create a Folder With a Name Based on the Monday and Friday of the Coming Week?

Hey, Scripting Guy! Every Sunday I need to create a folder with a name similar to this: C:\Test\7-2-2007 to 7-6-2007, with the dates representing the Monday and the Friday of the upcoming week. How can I write a script that will create this folder for me?

-- R

Hey, R. You, from time-to-time people write to the Scripting Guys and say, “You guys are so crazy; where does all that craziness come from?” Well, here’s one place. As it turns out, later this summer the Scripting Guy who writes this column will be accompanying selected family members on a trip to Italy.

Note. Were these family members selected because they were somehow better than the other family members? No. They were selected simply because they weren’t a bunch of cheapskates, like the other family members.

At any rate, this past weekend the tourists-to-be were sitting around discussing the forthcoming trip. “At least this time I won’t be worried about the food,” said the Scripting Mom, with memories of Parisian croissants filled with snails still fresh in her mind. “I’ll just have spaghetti or some other kind of pasta at every meal.”

“I don’t know, Mom,” said the Scripting Sister. “You have to be careful in Italy, because spaghetti doesn’t necessarily mean spaghetti and meatballs. What if you order spaghetti and it comes with clams or something?”

“That won’t be a problem,” said the Scripting Mom. “I’ll just make sure I order spaghetti with marijuana sauce each time.”

Now, to the Scripting Mom’s credit she immediately corrected herself and said, “Marinara sauce. I meant marinara sauce.” Of course, by then the damage had already been done. “I don’t think you can get spaghetti with marijuana sauce in Rome,” noted one of the Scripting Brothers. “I think you’ll have to go to Amsterdam to get that.”

Official disclaimer. We should note that neither Microsoft nor the Scripting Guys endorse the use of illicit drugs such as marijuana.

And no, not even on spaghetti.

Anyway, if you’re wondering why the Scripting Guys are so crazy, well, at least some of that appears to be genetic. Fortunately, though, the Scripting Guys don’t spend all their time dining on spaghetti with marijuana sauce. Every now and then they pause for a moment and write scripts, scripts that do things like create a folder whose name includes the dates of the Monday and Friday of the coming week:

intDay = Weekday(Date)

If intDay = 1 Then
    intAdder = 1
Else
    intAdder = 9 - intDay
End If

dtmMonday = Date + intAdder
dtmFriday = dtmMonday + 4

strFolderName = "C:\Test\" & dtmMonday & " to " & dtmFriday
strFolderName = Replace(strFolderName, "/", "-")

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.CreateFolder(strFolderName)

Whoa, man: the colors, look at the colors ….

Sorry; the Scripting Mom packed a lunch for us this morning. For some reason, today’s lunch seems to pack more of a wallop than the usual bologna sandwich and Twinkie.

As for the script, as best we can recall, that starts out by using the Weekday function to assign a value representing the current day of the week to a variable name intDay. In case you’re wondering, the Weekday function returns one of the following values, depending on the day of the week:

Sunday | 1
Monday | 2
Tuesday | 3
Wednesday | 4
Thursday | 5
Friday | 6
Saturday | 7

Why do we do care about the current day of the week? Well, we thought it would be useful to create a script that can be run at any time; as created, you can run this script on any day of the week, not just on Sunday. That seemed better, and a little more generic, than hard-coding in values that would cause unexpected results of the script ran on any day but Sunday. Of course, to be truly generic we could modify the script so that it asked the user to input a date, then created a folder for the week immediately following which date was entered. But, then again, the Scripting Guys never want to be truly generic; we just want to be sort of generic.

However, for those of you who don’t mind being truly generic, just replace the first line of the script with the following block of code:

dtmDate = InputBox("Please enter a date: ", "Enter Date")

dtmDate = CDate(dtmDate)
intDay = Weekday(dtmDate)

This block of code asks the user to enter a date, uses the CDate function to convert that value to a date-time value, then uses the Weekday function to determine the day of the week corresponding to the entered date. Pretty slick, huh?

Now, back to the question at hand: why do we care about knowing the current day of the week? That turns out to be an easy question to answer: our folder name has to start with the Monday of the following week, and we can’t determine the date of the next Monday without knowing whether today happens to be a Tuesday, a Wednesday, or a whatever. Let’s say that today is a Saturday; in that case, we’re going to have to add 2 days to the current date to get the date of the next Monday. What if today is a Monday? In that case, we’ll have to add 7 days to the current date to get the date of the next Monday. That’s what this bit of code is for:

If intDay = 1 Then
    intAdder = 1
Else
    intAdder = 9 - intDay
End If

This code might not make a lot of sense at first, but it works. (Trust us.) What we’re doing here is checking to see if intDay is equal to 1, meaning that we’re running the script on a Sunday; if it is, then we assign 1 to a new variable named intAdder. As the name at least kind of implies, we’re going to use intAdder to add the requisite number of days to the current date, eventually ending up with the date of the next Monday. If the script is run on a Sunday, then we have just 1 day until the next Monday. Consequently, we assign the value 1 to intAdder.

If we run the script on any day but a Sunday, however, we execute this line of code instead:

intAdder = 9 - intDay

Had we wanted to, we could have set up a bunch of If Then statements, one for each day of the week. (For example, if intDay equals 2, then add 7 days to the current date.) We wanted to find a way to bypass writing all those If Then statements, however, so we used this simple little algorithm instead: subtract the integer value of the current day from 9 and, lo and behold, we’ll get the back the number of days we need to add. For example, suppose today is a Tuesday, which has an integer value of 3. Subtract 3 from 9 and we get 6. Amazingly enough, 6 also happens to be number of days we need to add to a Tuesday in order to get the value of the very next Monday.

And no, you haven’t been slipped some of the Scripting Mom’s spaghetti. Play with this a little bit and you’ll see how it works.

Now we’re ready to start calculating dates and creating folder names. To begin with, we determine the date of the next Monday by taking the current date and adding the value of intAdder:

dtmMonday = Date + intAdder

If today happens to be Monday, July 2, 2007 then intAdder will be 7; adding 7 to July 2, 2007 makes the variable dtmMonday equal to this: July 9, 2007.

If you thought that calculating the value of the next Monday was easy, wait until you see how easy it is to calculate the value of the next Friday; all you have to do is add 4 days to the date of the next Monday:

dtmFriday = dtmMonday + 4

As soon as we have our two dates we can then start putting together our folder name. Step 1 in that process is to execute this line of code:

strFolderName = "C:\Test\" & dtmMonday & " to " & dtmFriday

One small word of caution here: depending on your language and regional settings you might have to modify the code used to create the folder name. On the Scripting Guys’ test computer, dates are displayed in this format: Month/Day/Year. Thus the preceding line of code concatenates the following values:

· C:\Test, the root folder where the new folder will be created.

· 7/7/2007, the date of the next Monday.

· to , the word “ to ” (with a blank space before and after the word). As if you couldn’t see that for yourself.

· 7/6/2006, the date of the next Friday.

That’s going to result in the following folder path:

C:\Test\7/7/2007 to 7/6/2007

That’s close to what we want, except for all those / characters. Those aren’t allowed in folder names; therefore, we use our next line of code the replace each / with a dash (-):

strFolderName = Replace(strFolderName, "/", "-")

That gives us a folder path that looks like this:

C:\Test\7-7-2007 to 7-6-2007

That also means we’re now ready to create an instance of the Scripting.FileSystemObject object and then use the CreateFolder method to create our new folder:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.CreateFolder(strFolderName)

At that point, we’re done. Which can only mean one thing: it must be time for more spaghetti with …marinara… sauce. If you need us, we’ll be in the lunchroom.

For the rest of the day.


How Can I Create A New Folder Using a Name Based on the Current Date?

Hey, Scripting Guy! How can I create a new folder each night, using a folder name based on the current date?

-- DM

Hey, DM. In answer to your unasked question, no, the Scripting Guy who writes this column did not get selected to sit on a jury; as a result, he’s back to work, and back to writing this column. And, no, we don’t know why he wasn’t selected, although we suspect that his answer to the question, “How would you go about deciding this case?” might have been a factor. In retrospect, it might not have been a good idea for him to say, “Just shoot them all and let God sort it out.”

Editor’s Note: Having been rejected by the courts for jury duty, the Scripting Guy who writes this column is now apparently working on being rejected by Microsoft for his job. And while that would make the Scripting Editor’s life much simpler, she still does her best to keep him from saying things like “just shoot them” in his articles. Sometimes, however, there’s only so much one Scripting Editor can do.

Hey, we’re just kidding; he didn’t really say that (although his actual response might not have been much better). In truth, he was released from jury duty thanks to the enormous hue and cry raised by devoted readers of this column, readers who were concerned that they would not get their daily dose of Hey, Scripting Guy! during the three weeks while the trial took place.

Well, OK, there really wasn’t much of a hue, let alone a cry, although we assume that’s because none of you had the address of the King County Superior Court. To be really honest about it, the Scripting Guy who writes this column was released solely due to chance: when all was said and done, the juror number he was randomly assigned mathematically eliminated him from consideration.

Which only seems fair: after all, as near as he can tell, it was also due solely to random chance that he got his job as a Scripting Guy in the first place. Now he and universe are even!

At any rate, the Scripting Guy who writes this column is back. And what better way to celebrate his return than by taking a look at a script that creates a new folder each night, using a folder name based on the current date:

strMonth = Month(Date)

If Len(strMonth) = 1 Then
    strMonth = "0" & strMonth
End If

strDay = Day(Date)

If Len(strDay) = 1 Then
    strDay = "0" & strDay
End If

strYear = Year(Date)

strFolderName = "C:\Scripts\Tammy_" & strMonth & "-" & strDay & "-" & strYear

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.CreateFolder(strFolderName)

As you can see, there’s not a whole lot to this script, and what little code we do have is primarily involved in coming up with the correct folder name. DM is looking for folder names similar to this: C:\Scripts\Tammy_01-22-2007. And whatever DM wants, DM gets.

Note. Um, no, sorry, DM; you can’t have that. Or that. Or …. And yes, we know what we said. But that was just a figure of speech.

First things first: let’s figure out how to create a name based on the current date (a task that we know a lot of you are interested in doing). To begin with, we assign the current month (using the Month function) to a variable named strMonth. That’s going to assign strMonth the numeric value for the month; if the month is January, then strMonth will be equal to 1.

Which, for a lot of you, is a problem: after all, you’ve allotted two spaces for the month, meaning that you’d really like strMonth to be equal to 01. That’s OK; that’s what this block of code is for:

If Len(strMonth) = 1 Then
    strMonth = "0" & strMonth
End If

Here we’re using the Len function to determine the number of characters in the variable strMonth. If the number of characters (length) is equal to 1, as it will be in the month of January, we simply use this line of code to add a leading 0 to the value:

strMonth = "0" & strMonth

What if the number of characters is not equal to 1 (meaning that it must be equal to 2)? In that case we leave well enough alone and simply move on.

Speaking of which, we next go ahead and repeat this same process to determine the day of month, this time using the Day function to grab the value and a similar block of code to add a leading 0 to that value (if necessary):

strDay = Day(Date)

If Len(strDay) = 1 Then
    strDay = "0" & strDay
End If

Finally, we use the Year function to store the current year in the variable strYear:

strYear = Year(Date)

In this case, we don’t need to worry about leading zeroes; that’s because we’ve allotted four spaces for the year, and, by default, the Year function returns a value (such as 2007) four digits longs.

Once we’ve determined the values for the month, day, and year we then use this line of code to construct the complete path to the new folder:

strFolderName = "C:\Scripts\Tammy_" & strMonth & "-" & strDay & "-" & strYear

Here we’re simply concatenating the following values (assuming the date really is January 22, 2007), and storing the resulting string in a variable named strFolderName:

C:\Scripts\Tammy_
+                   01
+                    -
+                   22
+                    -
+                 2007

The net result is the desired folder path: C:\Scripts\Tammy_01-22-2007.

From here all we have to do is create an instance of the Scripting.FileSystemObject, then use the CreateFolder method to create a new folder:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.CreateFolder(strFolderName)

The one drawback to this script is that it works on only the local computer. What if you want to create this same folder on a remote computer? For one suggestion, see this previous Hey, Scripting Guy! column.

OK. Now that we have our script how do we make sure that it runs each and every night? Well, the best way to do that is to simply configure the script as a scheduled task. When you do so, use Cscript.exe as the executable program and assign the full path to your script file as a command-line argument for Cscript.exe.

So was the Scripting Guy who writes this column disappointed that he didn’t actually get to serve on a jury? Well, in some ways; his case actually sounded pretty interesting. On the other hand, the trial was expected to last at least three weeks, and it’s hard to believe that anything that doesn’t involve a ball of some kind could hold this Scripting Guy’s attention for three hours, let alone three weeks. Besides, he needed to come back to work: the other Scripting Guys were simply lost without him! Poor, helpless little things ….


How Can I Create a Sequential Series of Folders?

Hey, Scripting Guy! How can I prompt a user to enter a starting number and ending number, then create a sequential series of folders based on those two numbers?

-- PM

Hey, PM. We had some time to kill this afternoon, so we thought we’d see what we could do about answering your question. In fact, if we didn’t try answering this question the Scripting Guy who writes this column would just be sitting around waiting until it was time to leave work and go to the movie No Reservations starring Catherine Zeta-Jones, a master chef who “…lives her life like she runs her kitchen at a trendy Manhattan eatery—with a no-nonsense intensity that both captivates and intimidates everyone around her.” As you might expect, Catherine’s “… perfectionist nature is put to the test when she ‘inherits’ her 9-year-old niece Zoe, while contending with a brash new sous-chef who joins her staff.”

Whew!

In case you’re wondering, no, this isn’t the kind of movie the Scripting Guy who writes this column would normally go to; as it is, the kind of movie the Scripting Guy who writes this column would normally go to is no movie whatsoever, seeing as how he maybe makes it out to one or two movies a year. (Compared to, say, 7 University of Washington football games, 20+ University of Washington basketball games, a handful of Seattle Mariners games, and several thousand Scripting Son baseball games.) So does that mean the Scripting Guy who writes this column is being forced to see this movie against his will? Yes, as a matter of fact, it – um, we mean no, no, of course not. He doesn’t just want to go to this movie; in fact, he’s simply dying to see what happens when rivalry becomes romance, and Catherine Zeta-Jones has to learn to express herself beyond the realm of her kitchen. Well, assuming that she wants to connect with Zoe and find true happiness with high-spirited and free-wheeling sous chef Nick Palmer, that is.

OK, so maybe we are being a bit facetious here; the movie reviews have been pretty good, and we expect this will be a perfectly enjoyable evening. Still, the Scripting Guy who writes this column would be happier if up-to-the-minute baseball scores continually scrolled across the bottom of the screen, the same way they do on ESPN.

At any rate, show time isn’t for another 6 hours or so, which leaves plenty of time to explain how to write a script that can prompt a user to enter a starting number and ending number, then create a sequential series of folders based on those two numbers:

Set objFSO = CreateObject("Scripting.FileSystemObject")

intStartingFolder = InputBox("Please enter the starting number:")
intEndingFolder = InputBox("Please enter the ending number:")

For i = intStartingFolder to intEndingFolder
    strNumber = i
    Do While Len(strNumber) < 5
        strNumber = "0" & strNumber
    Loop

    strFolder = "C:\Scripts\2007-" & strNumber
    Set objFolder = objFSO.CreateFolder(strFolder)
Next

Note. At this point, no, we have no plans to turn this folder creation script into a movie starring Catherine Zeta-Jones as a master scripter who “…lives her life like she runs her IT department at a trendy Redmond software company—with a no-nonsense intensity that both captivates and intimidates everyone around her.” But we haven’t ruled anything out, either. Catherine, if you’re reading this (and, like most Hollywood movie stars, we assume you are), give us a call.

While we wait for Catherine’s people to call our people, and while we wait until we actually get some people, let’s talk about the script. (The folder creation script, not the movie script.) As you can see, we start out by creating an instance of the Scripting.FileSystemObject object. No big surprise plot twist there; that’s the object of choice for creating new folders. Next we use the InputBox function to prompt the user to enter the starting folder number, a value we store in the variable intStartingFolder:

intStartingFolder = InputBox("Please enter the starting number:")

Following that, and just to add to the tension and suspense (will we ever find true happiness with high-spirited and free-wheeling folder creator Nick Palmer?), we call the InputBox function a second time, this time prompting the user to enter the ending folder number and storing that value in the variable named intEndingFolder:

intEndingFolder = InputBox("Please enter the ending number:")

Before we go any further we should note that we didn’t include any error-handling in this script. Could that create a problem? You bet it could. For one thing, there’s no way to cancel the script; even if the user clicks the Cancel button in the input box the script will continue to run. (And, needless to say, will fail.) Fortunately, that’s something we can correct by modifying our InputBox calls so they look like this:

intStartingFolder = InputBox("Please enter the starting number:")

If intStartingFolder = "" Then
    Wscript.Quit
End If

In other words, if the user clicks Cancel (meaning that the variable intStartingFolder gets assigned an empty string value) then we call the Quit method and terminate the script.

Much as someone should have done for the Michael Jordan “movie” Space Jam.

In addition, we should also include some code that verifies that the user actually entered a number and not a string value. For example, code similar to this:

If Not IsNumeric intStartingFolder Then
    Wscript.Echo "You must enter a number."
    Wscript.Quit
End If

Oh, and while we’re at it, we should also verify that the ending folder number is larger than the starting folder number:

If Not intEndingFolder <= intStartingFolder Then
    Wscript.Echo "The ending folder number must be larger than the starting folder number."
    Wscript.Quit
End If

However, that made the script a little too long and a little too complicated-looking so we left it all out. But don’t despair: the complete script will appear on the director’s cut DVD.

Once we have our starting and ending folder numbers our next task is to set up a For loop that runs from the starting number (say, 500) to the ending number (say, 519). That’s what this line of code is for:

For i = intStartingFolder to intEndingFolder

Inside this loop we assign the value of the counter variable i to the variable strNumber; in this example, that means that – the first time through the loop – strNumber will be equal to 500.

And now we’re going to take a slight detour. In PM’s original email the folders had names similar to this one, with five digits allocated for the folder number:

C:\Scripts\2007-00500

We’ve decided to adopt this same naming convention, which raises an important question: how can we ensure that our folders allocate five digits for the folder number? Well, to begin with, we use the Len function and the following line of code to determine whether the length of the value stored in strNumber (that is, the number of characters in the value) is less than 5:

Do While Len(strNumber) < 5

Let’s assume that it is, which is definitely true if strNumber equals 500. In that case, we use this line of code to set the value of strNumber to the existing value of strNumber plus a leading zero:

strNumber = "0" & strNumber

In our example, that means strNumber now equals 0500. We then loop around and check the length of the “new” strNumber variable. Because the length is 4 and 4 is less than 5, we add another leading zero; that makes strNumber equal to 00500. We then loop around and check the length again. Because the length is no longer less than 5 we break out of the Do While statement and get on with the show.

And what kind of excitement and adventure can we expect now that we’ve broken out of the Do While statement? Well, hang on to yours hats; for one thing, we’re going to use this line of code to construct a path for the new folder and store it in a variable named strFolder:

strFolder = "C:\Scripts\2007-" & strNumber

What’s strFolder going to be equal to? You got it: C:\Scripts\2007-00500, which just happens to be the path for one of the folders we need to create. With that in mind, we then call the CreateFolder method and create a new folder:

Set objFolder = objFSO.CreateFolder(strFolder)

At that point we loop around and the script automatically increments the value of our counter variable by 1 (making i equal to 501); we then we repeat the process. This continues until we’ve created the folder C:\Scripts\2007-00519, at which point we’ve connected with Zoe, found true happiness with Nick Palmer, and created a whole bunch of new folders to boot. If that doesn’t sound like a box office bonanza, well, we don’t know what does.

Anyway, we hope that answers your question, PM. It does? Oh. Well, that’s good, um, we were hoping that this would clear everything up. But listen, are you sure we answered your question; you know, did we completely answer this question for you? After all, if you have even the least bit of uncertainty about this issue then we’d be happy to explain it all over again, and as often as needed. In fact, we’re willing to do that even if we have to stay late, even if that means missing the movie that we’re so looking forward to seeing. Granted, that would be a considerable sacrifice on our part, but you know how it is for the Scripting Guys: business before pleasure. So just let us know if you have any questions, any questions at all. Any questions.

OK, not a single question, huh? All right; anyone else out there have a question about this script? Anyone? Hello? Hello?

Oh. OK. That’s … great ….


How Can I Delete a Folder and All Its Subfolders?

Hey, Scripting Guy! How can I delete a folder and all its subfolders?

-- FB

Hey, FB. You know, a lot of people think the Scripting Guys know everything there is to know about scripting. Let’s put it this way: we wish. In fact, it’s only when we begin to think that we do know something about scripting that we get ourselves into trouble.

Take (please!) this seemingly-simple little question about deleting a folder and all its subfolders. One of the Scripting Guys (Greg asked us not to mention his name, to save him from embarrassment) thought this was easy; in fact, he already had a script that could delete a folder and its subfolders:

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory where Name = 'c:\\Scripts'")

For Each objFolder in colFolders
    errResults = objFolder.Delete
Next

Our unnamed Scripting Guy grabbed the script, did a quick test on his Windows XP Service Pack 1 computer and on a Windows 2000 computer, and discovered that the script worked just fine. With yet another job well done, he then took the rest of the day off and began planning how he would spend the huge bonus he would undoubtedly receive for having successfully solved this problem.

It was only later that another Scripting Guy (and – alas – the newest member of the team at that) pointed out that his script – which worked perfectly well back in the Dark Ages of computing – no longer functioned in the modern world; in particular, it failed on any Windows XP computer running Service Pack 2. Why? Well, in Service Pack 1 you delete a folder and all its contents, including any subfolders, with no questions asked. In Service Pack 2, however, the behavior of the Win32_Directory class has changed. On a computer running SP2, a folder cannot be deleted if it contains even a single subfolder; instead, you generate a “Folder not empty” error. Ouch! In fact, using WMI the only way to delete a folder in SP2 is to first delete all its subfolders. And, of course, before you can delete a subfolder you have to delete all of its subfolders. And before you can delete any of those – well, it just goes on and on from there.

Of course a lesser person would have given up at that point, and just told you that, “There’s no way to delete a folder and all its contents.” And that’s exactly what our anonymous Scripting Guy was going to do. However, under intense pressure from his colleagues, he came up with this solution instead, one that creates a list of the entire folder tree and then methodically deletes folders from the bottom to the top. And don’t worry, we’ll tell you what that all means in a minute.

Here’s a script that ought to work on any version of Windows (or at least any version of Windows with WMI installed):

Dim arrFolders()
intSize = 0

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

strFolderName = "c:\scripts\archive"

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

Sub GetSubFolders(strFolderName)
    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name
        ReDim Preserve arrFolders(intSize)
        arrFolders(intSize) = strFolderName
        intSize = intSize + 1
        GetSubFolders strFolderName
    Next
End Sub

For i = Ubound(arrFolders) to 0 Step -1
    strFolder = arrFolders(i)
    strFolder = Replace(strFolder, "\", "\\")
    Set colFolders = objWMIService.ExecQuery _
        ("Select * from Win32_Directory where Name = '" & strFolder & "'")

    For Each objFolder in colFolders
        errResults = objFolder.Delete
    Next
Next

We won’t even attempt to walk through this code line-by-line; instead we’ll just give you a rough idea of what we’re doing, and why, and point you to some additional resources if you need more information.

To begin with, let’s say C:\Scripts\Archive looks like this:

We can’t delete the Archive folder right away; that’s because of Subfolder A1 and Subfolder A2. And we can’t delete Subfolder A1 right away; that’s because of Subfolder B1. To get rid of the Archive folder we need to:

· Delete Subfolder A2.

· Delete Subfolder B1, clearing out Subfolder A1.

· Delete Subfolder A1. With both Subfolder A1 and Subfolder A2 gone, we’ll then be free to delete the Archive folder.

· Delete the Archive folder.

The approach we’ve taken makes use of a recursive function; this function can identify all the subfolders of a folder, as well as all the subfolders of those subfolders. We won’t discuss this function in any detail today; for an explanation of how it works, check out this Hey, Scripting Guy! column.

By necessity, our recursive function starts with the first folder – Archive – and then works its way down the tree. Unfortunately, that’s a bit of a problem; after all, we need to start deleting folders from the bottom of the tree and work our way up. There are probably lots of ways to handle this problem, but what we did was elect not to try deleting folders right away; instead, all we do is stash the folder paths into an array named arrFolders. Each time we encounter a new folder or subfolder we add the folder path to the array. When the recursive function finishes, our array will look like this:

· C:\Scripts\Archive

· C:\Scripts\Archive\Subfolder A1

· C:\Scripts\Archive\Subfolder A1\Subfolder B1

· C:\Scripts\Archive\Subfolder A2

Note. We used a so-called “dynamic” array in this script; for more information about dynamic arrays – and about weird-looking commands like Redim Preserve – please see this portion of the Microsoft Windows 2000 Scripting Guide.

Trust us: we’re well aware that this was way easier in Windows 2000. But at least now we’re ready to start deleting folders. If you look at the items in our array, they happen to be exactly opposite of what we need; for example, the Archive folder is the first item in our array, but it has to be the last item that we delete. If we could invert the array – making the first last and the last first, to steal a phrase – we’d be in business. That’s where this block of code comes in:

For i = Ubound(arrFolders) to 0 Step -1
    strFolder = arrFolders(i)
    strFolder = Replace(strFolder, "\", "\\")
    Set colFolders = objWMIService.ExecQuery _
        ("Select * from Win32_Directory where Name = '" & strFolder & "'")

    For Each objFolder in colFolders
        errResults = objFolder.Delete
    Next
Next

What we’re doing here is reading our array from the bottom up. We’re creating a loop that starts with the very last item in the array; that’s what the Ubound (upper bound) function is for. We’re then going to work our way down to the first item in the array: item 0. (As you might recall, the first element in an array is always item 0, not item 1.) The Step -1 function just means that we step backwards rather than forward: instead of going 0-1-2-3, we’re going 3-2-1-0. This is how we can start deleting at the bottom of the tree.

Of course, before we can do that we need to adjust the folder paths; that’s what we do here:

strFolder = Replace(strFolder, "\", "\\")

Our folder paths are going to look like this: C:\Scripts\Archive\Subfolder A1\Subfolder B1. That’s fine, except that we need to include these paths in a WQL query. Consequently, we need to double up all the \’s, resulting in paths that look like this: C:\\Scripts\\Archive\\Subfolder A1\\Subfolder B1. We use the Replace function to replace each \ with \\.

You’re right: there’s nothing straightforward about this script, is there?

With our new \\ folder paths we can then use WMI to connect to the folder in question and – at long last – use the Delete method to actually delete the folder. That’s what happens here:

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory where Name = '" & strFolder & "'")

For Each objFolder in colFolders
    errResults = objFolder.Delete
Next

Having disposed of Subfolder B1, we can then start working our way up the tree, eventually deleting C:\Scripts\Archive.

Five million lines of code later, we’re done, and C:\Scripts\Archive has been deleted. We’re not saying it’s a particularly elegant solution, but it is a solution, and it will work on all versions of Windows. And because it uses WMI, it works equally well on remote machines as it does on the local computer.

So there you have it: deleting a folder and all its subfolders, a subject we’ve vowed never to deal with again. Well, at least not until the next service pack comes out ….


How Can I Delete a Folder That Has an Apostrophe in the Name?

Hey, Scripting Guy! How can I delete a folder that has an apostrophe in the name?

-- JH

Hey, JH. Ah, yes, the apostrophe: every script writer’s arch-nemesis. The apostrophe seems like such a simple little character, but don’t let its appearance fool you: the apostrophe (or the single quote) is probably the most deadly character to be found on the keyboard. Whether you’re working with Active Directory, a database, or the file system, a single apostrophe can wreak total havoc on your scripts. As Bart Simpson once said of Hershey®’s Milk Duds, the apostrophe is “sweet on the outside, but poison on the inside.”

Note. No, Milk Duds don’t really have poison on the inside.

The problem with the apostrophe is that it’s a “reserved” character, a character that VBScript uses for its own purposes. For example, suppose you want to delete the folder C:\Scripts. Here’s a script that will do just that:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * From Win32_Directory Where Name = 'C:\\Scripts'")

For Each objFolder in colFolders
    errResults = objFolder.Delete
Next

If you look closely at the Where clause, you’ll see that the apostrophe (or the single quote) is used to delineate a string value: ‘C:\\Scripts’. That’s why we run into problems with a folder that has an apostrophe in its name (say, Ken’s Scripts). What do you suppose happens when we try running a script like this one:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * From Win32_Directory Where Name = 'C:\\Ken's Scripts'")

For Each objFolder in colFolders
    errResults = objFolder.Delete
Next

We won’t keep you in suspense: the script will flat-out fail. Why? Well, look at the Where clause:

Where Name = 'c:\\Ken's Scripts'

Because the apostrophe marks the beginning and ending of a string in a Where clause, VBScript thinks the name of the folder is this: ‘C:\\Ken’. That would be fine, except then we have a bunch of other characters (s Scripts’) following the end of the string. VBScript has no idea what that gibberish is supposed to mean, and so it simply gives up without even trying.

Yes, we know. But that’s just the way VBScript works. Because the apostrophe is a reserved character used to (among other things) mark the beginning and end of strings in a Where clause, VBScript simply has no idea what we’re talking about.

And no, no jokes about how that’s like most of the people who’ve had the misfortune of talking to the Scripting Guys. That one’s too easy!

So is there a way to work around this problem? You bet there is. As you probably know, any time we use a backslash (\) in a Where clause (like in a file path) we need to “escape” that backslash by putting another \ in front of it; that’s why we have a file path like C:\\Scripts instead of C:\Scripts. We have to do that because the \ is another reserved character; the only way to tell VBScript to use the \ as-is is by escaping it (which just means prefacing it with another \).

Hey, wait a minute: if you can use a reserved character like the \ simply by prefacing it with another \, maybe you could use a reserved character like the single quote by prefacing it with a \.

You know, that’s just crazy enough to work:

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

Set colFolders = objWMIService. _
    ExecQuery("Select * from Win32_Directory Where Name = 'C:\\Ken\'s Scripts'")

For Each objFolder in colFolders
    errResults = objFolder.Delete
Next

And there’s your answer right there, JH. Notice how we put a \ right before our apostrophe; that gives us a construction that looks like this: ‘C:\\Ken\’s Scripts’. Put a \ before any apostrophes in the folder name and the script will work just fine. Granted, it looks a little odd, but it does the trick.

Just like a Milk Dud.


How Can I Delete the Oldest File From a Folder If and When That Folder Exceeds a Specified Size?

Hey, Scripting Guy! I need to check the size of a folder once every hour; if the folder has exceeded a specified size, I then need to delete the oldest file in that folder. How can I do that?

-- JC

Hey, JC. You know, a lot of you are thinking, “Gee, I wonder what was the very best, very coolest thing that the Scripting Guy who writes that column saw on his recent trip to Italy?” As it turns out, that’s an easy question to answer: the very best, very coolest thing that the Scripting Guy who writes this column saw on his recent trip to Italy was housed at the Vatican Museum and – What’s that? The Sistine Chapel? Oh, is that at the Vatican? Well, what do you know? No, the – the Raphael Rooms? Sorry; never heard of them. At any rate, without a doubt the very best, very coolest thing the Scripting Guy who writes this column saw on his recent tip to Italy was housed at the Vatican Museum: a Pepsi-Cola!

You heard right: a Pepsi-Cola. The Scripting Guy who writes this column had been to Europe a couple times prior to his recent vacation, and never once had he even seen a Pepsi, let alone have the opportunity to drink one. But after completing their tour of the Museum the Scripting Family stopped off at the Museum cafeteria to get something to drink. And there it was: a can of Pepsi.

Needless to say, it was a profoundly moving experience, at least for a Scripting Guy who doesn’t really like Coca-Cola all that much.

Scripting Dad and Scripting Son note. As we mentioned, the family stopped off in the cafeteria just to get something to drink. Eat lunch there? Oh, please, not in a museum cafeteria. But then, just as the Scripting Dad and Scripting Son were reaching for their drinks, a cafeteria worker came by and plopped down a huge tub of pasta, pasta completely smothered with broiled cheese. For some reason, eating lunch at a museum cafeteria no longer seemed like such a bad idea after all.

Of course, it goes without saying that drinking a Pepsi, in Europe, mind you, is an experience that will never be topped. So then what was the second-best and second-coolest thing the Scripting Guy who writes this column saw in Italy? That would have to be this vintage Italian script (made by artisans using the same materials and techniques Italian scripters have used for hundreds of years), a script that can check to see if a folder has exceeded a specified size and, if it has, delete the oldest file in that folder:

strOldestFile = ""
dtmOldestDate = Now

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder
= objFSO.GetFolder("C:\Scripts")

intFolderSize = Int((objFolder.Size / 1024) / 1024)

If intFolderSize
>= 25 Then
Set colFiles = objFolder.Files

For Each objFile in colFiles
strFile = objFile.Path
dtmFileDate = objFile.DateCreated
If dtmFileDate < dtmOldestDate
Then
dtmOldestDate = dtmFileDate
strOldestFile = strFile
End If
Next

objFSO.DeleteFile(strOldestFile)
End If

A couple quick notes before we begin. First, this script is designed to run only on the local computer. Why? Well, to run against a remote computer, the obvious technology of choice would be WMI. Unfortunately, though, there’s a bug in WMI’s Win32_Directory class: any time you retrieve the size of a folder the size comes back as 0 bytes. (If only we all had bathroom scales that worked in similar fashion!) That makes WMI a somewhat less-than-ideal technology for any task involving folder sizes. Could we perform this task remotely, even if that involves using a work-around or two? Yes we can, and that’s something we’ll take up in a future column, if anyone’s interested.

Second, this script is designed to run once and then stop. But wait, you say, didn’t JC need the script to check the folder size every hour? That’s correct. However, rather than add in code that pauses the script for an hour we recommend that you set up a scheduled task and have that task run the script once an hour. That’s a much better, much easier, and much more foolproof approach to getting a script to run once every hour.

Note. Why foolproof? Well, suppose something happens and the script process terminates (e.g., the command window in which the script is running is closed). In that case, you’ve lost your ability to monitor the folder, at least until you restart the script. With a scheduled task, you don’t ever have to worry about things like processes terminating prematurely and needing to be restarted.

OK, so much for all that; let’s see if we can figure out how the script actually works. To begin with, we assign values to a pair of variables, strOldestFile and dtmOldestDate:

strOldestFile = ""
dtmOldestDate = Now

We’re going to use the variable strOldestFile to keep track of the path to the oldest file in the folder C:\Scripts. Of course, at the beginning of the script we don’t know the path to the oldest file in the folder, so we simply assign this variable an empty string (“”).

Now, how are we going to determine which file is the oldest file in the folder? Well, we could figure that out by using some sort of complicated WMI query and sorting algorithm. However, we opted to do something a little easier: we’re simply going to assign a beginning value to the variable dtmOldestDate (in this case, that value being the current date and time, something we can calculate by using the VBScript Now function). After that, we’ll just loop through the collection of files found in the folder C:\Scripts. For each file, we’ll check to see if the file creation date is older than the value stored in dtmOldestDate. If it is, then we’ll assign the file path of that file to strOldestFile and the file creation date to dtmOldestDate. If it isn’t, well, then we’ll simply move on and repeat the process with the next file in the collection.

But more on that in a minute.

After initializing our variables we create an instance of the Scripting.FileSystemObject, then use the GetFolder method to bind us to the folder C:\Scripts; that’s what we do with this block of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Scripts")

Once we’ve created an object reference (objFolder) to C:\Scripts we then use this line of code to store the size of that folder in the variable intFolderSize:

intFolderSize = Int((objFolder.Size / 1024) / 1024)

You know, you’re right: that is a somewhat-complicated looking bit of code, isn’t it? And, to be honest, there’s no need for it to be that complicated; instead, we could have simply assigned the value of the Size property to the variable intFolderSize, like so:

intFolderSize = objFolder.Size

So then why didn’t we just do that? Well, as it turns out, folder sizes are returned in bytes; that means a 25-megabyte folder will have a size equal to this:

26214400

There’s nothing wrong with that, but very few people (or, at least, very few Scripting Guys) can glance at the number 26214400 and immediately think, “Oh, 25 megabytes, eh?” Therefore, we did a little bit of math: we took the value of the Size property and divided it by 1024, giving us the size in kilobytes. We then took that value and divided it by 1024 once more, thus giving us a size (e.g., 25.1637182137) in megabytes. Finally, and because we really don’t care about the .1637182137, we used the Int function to strip off the decimal places.

The net result? We end up assigning a nice, clean value (25) to the variable intFolderSize. Because, as we all know, cleanliness is next to godliness.

Scripting Guys trivia. Contrary to popular belief, the phrase “Cleanliness is next to godliness” does not appear in the Bible. Instead, the quote comes from a sermon by John Wesley, founder of the Methodist church: “Let it be observed, that slovenliness is no part of religion; that neither this, nor any text of Scripture, condemns neatness of apparel. Certainly this is a duty, not a sin. Cleanliness is, indeed, next to godliness."

If that information doesn’t win you a bunch of bar room bets, well, you’re obviously hanging out in the wrong bar rooms.

OK, back to work. (After all, idle hands are the Devil’s plaything.) As soon as we know the folder size we can use code like this to determine if that size is greater than or equal to a specified value (in this case, 25 megabytes):

If intFolderSize >= 25 Then

If the size is less than 25 megabytes, then we’re done; because the size is within the allowed limits we don’t have to delete anything. If the size is greater than (or equal to) 25 megabytes, however, our next step is to use this line of code to retrieve a collection of all the files in the folder:

Set colFiles = objFolder.Files

Once we have the collection in hand we set up a For Each loop to loop through each and every file in the folder. Inside the loop, the first thing we do is assign the file Path to a variable named strFile, then assign the file creation date and time (the DateCreated property) to a variable named dtmFileDate:

strFile = objFile.Path
dtmFileDate = objFile.DateCreated

So far so good, right? We then compare the value of dtmFileDate to the variable dtmOldestDate:

If dtmFileDate < dtmOldestDate Then

If the value of dtmFileDate (for example, 1/1/2007) is less than the value of dtmOldestDate (for example, 2/2/2007) that can mean only one thing: this particular file is – so far anyway – the oldest file in the folder. Therefore, we assign the file’s creation date to dtmOldestDate, and assign the file path to a variable named strOldestFile:

dtmOldestDate = dtmFileDate
sttrOldestFile = strFile

See what we’re doing there? That’s right: we’re just using these two variables to keep track of the oldest file in the folder, replacing the values of the two variables each and every time we run into a file older than any other file we’ve looked at.

After that we loop around and repeat the process with the next file in the collection. When we’re done, the path to the oldest file in the collection will be stored in the variable strOldestFile. In turn, that means we can delete the oldest file in the folder simply by passing that variable to the DeleteFile method:

objFSO.DeleteFile(strOldestFile)

And that should do it.

Incidentally, it should be obvious which side the Scripting Guy who writes this column takes in the great Pepsi vs. Coke debate. However, even though he doesn’t like Coke very much, the Scripting Guy who writes this column would never disparage anyone who does prefer Coke over Pepsi. The truth is, he refuses to say anything bad about Coke at all, even though the fact that Pepsi is sold at the Vatican Museum does suggest that Pepsi is the official soft drink of heaven.

Not that Coke drinkers have to worry much about making it to heaven, mind you.

Legal and theological disclaimer. The preceding statements do not represent the opinions of Microsoft, Pepsi-Cola, Coca-Cola, the Vatican Museum, or heaven. Although the Scripting Editor does seem to think that the Scripting Guy who writes this column is really on to something.

Oops; never mind. Turns out that that the Scripting Editor thinks that the Scripting Guy who writes this column is on something. That’s a little different.


How Can I Determine if a Folder Exists on a Computer?

Hey, Scripting Guy! Is there any way to determine whether or not a specific folder exists on a computer?

-- RP, Umatilla, OR

Hey, RP. There are a couple ways of doing this, depending on whether you are looking for the folder on the local computer or on a remote computer, and depending on whether or not you know the exact path to the folder. Let’s start with the easiest possible situation: you’re looking for the folder C:\Scripts on the local computer.

Why is this so easy? Because the FileSystemObject has a FolderExists method designed to tell you whether or not a folder exists. All you have to do is run the method, passing it the path C:\Scripts. If the method returns True, then the folder was found; if it returns False, then the folder was not found.

Here’s what the code looks like:

Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FolderExists("C:\Scripts") Then
    Wscript.Echo "The folder exists."
Else
    Wscript.Echo "The folder does not exist."
End If

OK, but what if you’re looking for a folder on a remote computer; after all, the FileSystemObject is designed to run locally. Well, in that case, just use the WMI class Win32_Directory and look for a folder with the Name C:\\Scripts.

Note. That’s not a misprint. When searching for files and folders using WMI, you must double any \’s found in your query. If you were looking for C:\Scripts\MyScripts\AdminScripts your query would look like this:

"Select * From Win32_Directory Where Name = " & _
    "'C:\\Scripts\\MyScripts\\AdminScripts'"

This sample script checks for the existence of the folder C:\Scripts on the remote computer atl-ws-01. How do we know if the folder was found? Well, the script reports back the Count (the number of items found). If Count = 0, then C:\Scripts doesn’t exist; if Count = 1, then C:\Scripts does exist.

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where " & _
        "Name = 'C:\\Scripts'")
Wscript.Echo colFolders.Count

Ok, now for the hard one: what if you’re looking for a folder named Scripts, but you have no idea whether it’s on drive C or drive D or somewhere else; in other words, you can’t search a specific path like C:\Scripts. That’s all right; you can use WMI to search for a folder with the FileName Scripts (the FileName property is equivalent to what we would call the folder name). Because the entire file system has to be searched this script might take a minute or so to complete (depending on how many folders you have on your machine), but it will do the trick:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where " & _
        "FileName = 'Scripts'")
Wscript.Echo colFolders.Count

This script, by the way, can be used both locally and remotely. Note, too that it’s possible for this script to return more than one item; that’s because all these folders have the FileName Scripts, even though their paths differ:

C:\Documents and Settings\All Users\Documents\Corporate\Scripts
C:\Scripts
D:\Administrative Tools\Scripts
E:\WMI\Scripts


How Can I Determine if a Folder Exists on a Computer?

Hey, Scripting Guy! How can I determine if the C:\Documents and Settings\%username%\Application Data\Microsoft\Templates folder exists on a computer?

-- JM

Hey, JM. The FileSystemObject includes a FolderExists method that makes it very easy to check for the existence of a folder. Just pass FolderExists the complete path to the folder and you’ll quickly get back True if the Folder exists and False if the folder does not exist.

The only catch here is that you don’t necessarily know the complete path to the folder; after all, each user who logs on is going to have a different path to the Templates folder; that’s because %username% is different for each user.

But that’s OK: we can easily figure out the value of %username%, and then use that value to construct the path to the Templates folder. Let’s show you a script that checks for the existence of the C:\Documents and Settings\%username%\Application Data\Microsoft\Templates folder, and then we’ll explain how it works:

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName

strPath = "C:\Documents and Settings\" & strUser & "\Application Data\Microsoft\Templates"

Set objFSO = CreateObject("Scripting.FileSystemObject")

If objFSO.FolderExists(strPath) Then
    Wscript.Echo "The folder exists."
Else
    Wscript.Echo "The folder does not exist."
End If

We begin by creating an instance of the Wscript.Network object, and then storing the value of the UserName property in a variable named strUser. Because UserName happens to have the same value as the %username% environment variable we can now construct the exact path to the Templates folder. That’s what we do in this line of code, which simply adds C:\Documents and Settings\ plus the value of strUser plus \Application Data\Microsoft\Templates:

strPath = "C:\Documents and Settings\" & strUser & "\Application Data\Microsoft\Templates"

If our user happens to be named kmyer, we’ll now have a path that looks like this:

C:\Documents and Settings\kmyer\Application Data\Microsoft\Templates

The rest is easy. We create an instance of the FileSystemObject and use the FolderExists method to determine whether the folder C:\Documents and Settings\kmyer\Application Data\Microsoft\Templates exists or not. We echo back the answer – yes, the folder exists or no, the folder does not exist – and we’re done.


How Can I Determine If the Number of Files in a Series of Folders Exceeds a Specified Value?

Hey, Scripting Guy! How can I determine if the number of files in a series of folders exceeds a specified value?

-- DP

Hey, DP. Buongiorno. Le come sta? Tutto bene?

What does all that mean? Io non so; Io non parlo Italiano. In other words: I don’t know; I don’t speak Italian.

No, really: “Io non so; Io non parlo Italiano” actually does mean “I don’t know; I don’t speak Italian.” As for the first sentence, that can be translated (at least by us) as this: “Good day. How are you? Everything OK?”

So why the sudden interest in Italian? Well, even as you’re reading this the Scripting Guy who writes this column is getting ready to wing his way towards Italy, off for a 10-day vacation in Rome and Venice. A couple of years ago the Scripting Guy who writes this column escorted several family members to London and Paris. He had a great time, but found it absolutely exhausting trying to get assorted and diverse family members to agree on where to go, what to do, when to eat, etc. “That was fun,” he said upon returning home. “But I’m never doing that again.”

And now, needless to say, he’s doing that again. And, along the way, he’s also trying to learn a few words of Italian, the better not to look like a typical clueless American tourist. (Obviously he is a typical clueless American tourist. He just doesn’t want to look like one.)

Originally, the Scripting Guy who writes this column hoped to write today’s entire column in Italian; as it turns out, however, the teach-yourself-Italian book he picked up at the bookstore doesn’t include the Italian equivalent for phrases like “Use an Associators Of query to retrieve a collection of all the subfolders in a specified folder.” But maybe that’s just as well; after all, this column can be difficult enough to understand when he writes it in English, let alone if he tried writing it in Italian.

So, in English, here’s a script that should solve your problem, DP:

strComputer = "."

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

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='C:\Absentee Reports'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

For Each objFolder in colSubfolders
    Set colFiles = objWMIService.ExecQuery _
        ("ASSOCIATORS OF {Win32_Directory.Name='" & objFolder.Name & "'} Where " _
            & "ResultClass = CIM_DataFile")

    If colFiles.Count => 4 Then
        Select Case colFiles.Count
            Case 4
                Wscript.Echo objFolder.Name & " has 4 files in it."
            Case 5
                Wscript.Echo objFolder.Name & " has 5 files in it."
            Case 6
                Wscript.Echo objFolder.Name & " has 6 files in it."
            Case 7
                Wscript.Echo objFolder.Name & " has 7 files in it."
            Case 8
                Wscript.Echo objFolder.Name & " has 8 files in it."
        End Select
    End If
Next

As you can see, we start out by connecting to the WMI service on the local computer. But have no fear: we can just as easily run this script against a remote computer. To do so, all we have to do is assign the name of the remote computer the variable strComputer, like this:

strComputer = "atl-fs-01"

According to DP, one of their servers houses a folder (in our example, C:\Absentee Reports) that features a subfolder for each employee in the organization. In other words, a folder structure similar to this one:

C:\Absentee Reports
C:\Absentee Reports\Gail Erickson
C:\Absentee Reports\Jonathan Hass
C:\Absentee Reports\Ken Myer
C:\Absentee Reports\Pilar Ackerman

Each time an employee is absent from the job a new document detailing the absence is added to the appropriate subfolder; for example, if Ken Myer is absent a new document will be added to the subfolder C:\Absentee Reports\Ken Myer. What we need to do is look inside each of these subfolders, count the number of documents in each one, and then take action based on that number. And that’s a good question: how in the world are we going to do all that?

Well, for starters, we’re going to use this line of code to return a collection of all the subfolders found in C:\Absentee Reports:

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='C:\Absentee Reports'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

And no, that’s not Italian; Italian – even the Scripting Guy who writes this column’s Italian – would make more sense. Instead, this crazy-looking code is an Associators Of query and, for better or worse, it’s supposed to look like that. But you don’t have to worry about the details; just note that we’re retrieving a collection of subfolders (instances of the Win32_Subdirectory class) for the parent folder C:\Absentee Reports. Note, too that this returns only the “top-level” subfolders; if you have nested subfolders (subfolders inside of subfolders) then you’ll need to use a different approach to get at all the subfolders and sub-subfolders.

Once we issue our query and get back a collection of all the subfolders in C:\Absentee Reports, our next step is to set up a For Each loop to walk through all the folders in that collection. The first thing we do inside that loop is issue yet another query, this one returning a collection consisting of all the files found in the first subfolder:

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

Yes, another crazy (pazzesco) Associators Of query. But let’s assume that the first subfolder in our collection is a folder named Gail Erickson; that means that the folder’s Name property (something we more commonly refer to as the file path) is equal to C:\Absentee Reports\Gail Erickson. That means that our Associators Of query is asking for all the instances of the CIM_DataFile class that are associated with (stored in) the folder C:\Absentee Reports\Gail Erickson (represented by the variable objFolder.Name). In other words, connect to the C:\Absentee Reports\Gail Erickson folder and give us back a collection of all the files (files being instances of the CIM_DataFile class) that you find there.

The rest is easy (facile). All WMI collections have a property (Count) that tells us the number of items in the collection. We want to take a different action based on whether a folder has 4, 5, 6, 7, or 8 files in it; if a folder has less than 4 files in it then we don’t want to do anything at all. Consequently, as soon as we get back the collection we examine the value of the Count property to see if that value is equal to or greater than 4:

If colFiles.Count => 4 Then

If it is, we then set up a Select Case statement based on the value of the Count property:

Select Case colFiles.Count
    Case 4
        Wscript.Echo objFolder.Name & " has 4 files in it."
    Case 5
        Wscript.Echo objFolder.Name & " has 5 files in it."
    Case 6
        Wscript.Echo objFolder.Name & " has 6 files in it."
    Case 7
        Wscript.Echo objFolder.Name & " has 7 files in it."
    Case 8
        Wscript.Echo objFolder.Name & " has 8 files in it."
    Case Else
        Wscript.Echo objFolder.Name & " has more than 8 files in it."
End Select

As you can see, we aren’t doing anything very fancy here. If, for example, the Count is equal to 4 we simply echo back a message stating that the folder has 4 files in it. That’s not very exciting, but that’s not the point; the point is to show you how you can determine the number of files in a folder and then take specific action based on the number of files. Needless to say, to do something a little fancier (and a little more meaningful), all you have to do is replace our sample code with something a little fancier (and a little more meaningful).

From there we simply loop around and repeat the process with the next subfolder in the collection. When all is said and done we’ll get back a report similar to this:

C:\Scripts\Ken Myer has 6 files in it.
C:\Scripts\Pilar Ackerman has 6 files in it.

At that point we’re done (fatto).

Or at least we think that fatto means “done”. No doubt the Scripting Son, who fancies himself as being a bit of a comedian, would consider fatto to be a description of the Scripting Dad’s waistline instead.

Right, Scripting Son. Ha-ha.

We hope that answers your question, DP. We’d actually love to stay and discuss this in more detail, but it’s time for the Scripting Guy who writes this column to head to the airport (aeroporto). But don’t worry: he’ll make sure that there’s a new Hey, Scripting Guy! each and every day while he’s gone. Why is he doing all that work even though he’s on vacation? That’s easy: it’s because he – hmmm .…

You know, now that we think about it, there’s really only one answer to that question: because the Scripting Guy who writes this column isn’t particularly astuto (smart).

As the Scripting Son (not to mention the Scripting Editor) could have told you years ago.

Oh, well; que sera sera. Arrivederci, everyone!


How Can I Get a List of All the Subfolders in a Folder and Then Put That List Into an Array?

Hey, Scripting Guy! How can I get a list of all the subfolders in a folder and then put that list into an array?

-- PW

Hey, PW. We know how anxious you’ve all been, waiting to hear whether the Scripting Guys were successfully moved from Building 43 to Building 42 (yes, directly across from their old building). Well, about the best we can say is that we were moved; whether we were moved successfully is another story. For example, prior to the big move they had us fill out a “Move Map” which included these two questions:

· How many rolling filing cabinets do you want?

· How many bookshelves do you want?

The Scripting Guy who writes this column requested one bookshelf and no filing cabinets. So what did he get? Ah, you must have been moved by Microsoft before: that’s right, no bookshelves and one filing cabinet, exactly the opposite of what he requested. There was also a note on his desk that read “Your computer equipment and/or peripherals were not connected in your old office, therefore they were not reconnected.” Not that hooking up a couple computers is a particularly big deal (especially for a Scripting Guy!) but, technically, his computer equipment and/or peripherals were connected in his old office; he’s learned, the hard way, that computer equipment works best when it’s hooked up and running.

Unless, of course, he just doesn’t know what it means to have connected computer equipment.

Oh, and his phone doesn’t work, either. Or so he thought. However, the guy going around working on phones checked his list and said, “It’s not on the list, so it must be working.” Oh, OK. Sorry to bother you.

Oh, well. At any rate, the Scripting Guys are back in business. And if you don’t believe us, here’s a script that grabs a list of all the subfolders in a folder and then stashes that list into an array:

On Error Resume Next

Dim arrFolders()
intSize = 0

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

strFolderName = "c:\scripts"

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

Sub GetSubFolders(strFolderName)
    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name
        ReDim Preserve arrFolders(intSize)
        arrFolders(intSize) = strFolderName
        intSize = intSize + 1
        GetSubFolders strFolderName
    Next
End Sub

For Each strFolder in arrFolders
    Wscript.Echo strFolder
Next

You’re right: this is a complicated script. That’s because we need to use a recursive subroutine in order to get at all the subfolders in a folder. We won’t you kid you: a recursive subroutine (a subroutine that can call itself) is a difficult concept to grasp, and we won’t take the time to explain all the ins and outs today. For more information, take a look at this portion of the Microsoft Windows 2000 Scripting Guide.

In the meantime, though, we’ll at least try to explain the principle behind this script. We start out by using the following line of code to declare a dynamic array named arrFolders; as you might expect, this is the array where we’ll store the list of folders:

Dim arrFolders()

We then assign the value 0 to a variable named intSize; this is a “counter variable” we’ll use to keep track (and periodically increase the size) of our dynamic array.

After connecting to the WMI service on the local computer (although this script works equally well against remote computers) we then use this block of code to bind to the folder C:\Scripts:

strFolderName = "c:\scripts"

Set colSubfolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

And now we’re ready for action. At the moment we have a dynamic array named arrFolders; however, that array is empty. Consequently, the first thing we need to do is get our root folder – C:\Scripts – into the array. That’s what we do here:

ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1

As you can see, we use Redim Preserve to redimension the size of our array. And what size do we make the array? Why, we make it size 0, using the variable intSize. (Yes, we know. But remember, an array of size 0 means that the array can hold 1 item. Likewise, an array of size 1 means that the array can hold 2 items, and so forth.) We next add the folder path (stored in the variable strFolderName) to the array, then increment the variable intSize by 1. None of that should be too terribly complicated.

That brings us to these three lines of code:

For Each objFolder in colSubfolders
    GetSubFolders strFolderName
Next

What we’re doing here is calling our recursive subroutine, GetSubFolders. We need to do this because WMI, by itself, returns only the “top-level” subfolders in a folder; for example, it returns C:\Scripts\ADSI but not C:\Scripts\ADSI\Users or C:\Scripts\ADSI\Groups. Because of that we need to connect to each top-level subfolder and then repeat the process for each of those sub-subfolders; in other words, get a collection of subfolders and then check to see if those subfolders have any sub-subfolders. Without going into excruciating detail, that’s what the recursive subroutine is for. It will dutifully check each subfolder, and each subfolder of those subfolders, and each subfolder of those sub-subfolders and – well, you get the idea. The net result? We’ll get back the complete folder tree: all of the folders and subfolders found in C:\Scripts.

This, by the way, is the actual recursive subroutine:

Sub GetSubFolders(strFolderName)
    Set colSubfolders2 = objWMIService.ExecQuery _
        ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
            & "Where AssocClass = Win32_Subdirectory " _
                & "ResultRole = PartComponent")

    For Each objFolder2 in colSubfolders2
        strFolderName = objFolder2.Name
        ReDim Preserve arrFolders(intSize)
        arrFolders(intSize) = strFolderName
        intSize = intSize + 1
        GetSubFolders strFolderName
    Next
End Sub

Like we said, we won’t discuss how this works. However, notice that we’re using the same Associators Of query we used to get the collection of top-level subfolders found in C:\Scripts. Assuming this query returns data we then set up a For Each loop to loop through the collection. Inside the loop we add the folder paths to the array (for example, there’s the Redim Preserve command again) and we then call the GetSubFolders subroutine. Why? To determine whether or not the subfolder has any sub-subfolders.

Yes, it’s very confusing, at least for us. Fortunately, though, VBScript has no problem keeping track of everything. In that respect, this might be one time that you simply let VBScript handle matters and not worry too much about it.

When all is said and done we then use this block of code to echo back all the folder paths added to the array arrFolders:

For Each strFolder in arrFolders
    Wscript.Echo strFolder
Next

Give it a try and see what happens.

We hope that helps, PW. Incidentally, the Scripting Guy who writes this column may have discovered the real reason his computer equipment and/or peripherals were not reconnected: his computer (or at least his desktop machine) is currently in four pieces, and one of those pieces might very well be bent beyond repair. The Scripting Guy who writes this column doesn’t know a lot about hardware, but past experience suggests that computers work best when all the pieces are there and they all actually fit together. Maybe the movers should have just carried the computer over instead of dribbling it.

Or maybe not. After all, they are the experts.

Editor’s Note: Someone must have tipped off the movers as to who they were moving. All of the Scripting Editor’s equipment made it to the new office unscathed and in perfect working condition, with the correct number of book shelves and filing cabinets (two of each), and a working telephone. The Scripting Editor has no idea how everything went wrong for the Scripting Guy who writes this column. Really.


How Can I List All the Empty Folders on a Specified Drive?

Hey, Scripting Guy! How can I list all the empty folders (that is, folders with a size of 0 bytes) on a specified drive?

-- DD

Hey, DD. You know, physicist Edwin Schrodinger once proposed a thought experiment in which a cat is placed in a sealed box, along with a canister of poison gas. The experiment is designed in such a way that there is exactly a 50-50 chance that the canister will release the gas, killing the cat instantly. According to quantum mechanics, until you open the box and check for yourself, the cat is somehow both dead and alive. One of the great paradoxes of modern life.

Note. So why is this just a thought experiment and not a real experiment? We don’t know for sure, but imagine this: you place a cat in a sealed box, along with a canister of poison gas. Now suppose that the canister doesn’t release the gas. Would you want to be the poor researcher who had to open the box and deal with what would likely be one crazy-mad cat? Obviously Schrodinger had no desire to be that person, either. (Editor’s Note: There’s also the small detail that you might actually kill a cat.)

Another famous paradox is the Grandfather Paradox. Suppose you go back in time and murder your own grandfather. Obviously that would mean that you would never be born. But if you were never born how were you able to go back in time and kill your own grandfather? And if you were able to go back in time, why the heck did you go back and kill your grandfather instead of bringing back a bunch of Willy Wonka Scrumdidlyuptious Bars, quite possibly the best candy bar ever made?

At the very least, go back and kill the grandfather of the person who decided to stop making the Scrumdidlyuptious Bar.

So is any of this relevant to retrieving a list of all the empty folders on a specified drive? Kind of; after all, it turns out that getting a list of empty folders is both harder than you might expect and, at the same time, easier than you might expect. Call it the Empty Folders Paradox. (We were going to call it the Grandfather Paradox, but that name was already taken.)

Let’s start off by talking about why it’s harder than you might expect. At first glance this sounds pretty easy; after all, you can use WMI’s Win32_Directory class to return a collection of all the folders on a drive. Can’t you just write a query that asks for all the folders that have a FileSize equal to 0?

Sure, except that you’ll be disappointed in the results. That’s because, as far as WMI is concerned, all folders have the same FileSize: no size. Because the FileSize property isn’t fully-implemented on the Win32_Directory class, FileSize always come back as Null.

Always.

That’s a problem: not only does it eliminate your ability to use WMI to retrieve a list of empty folders, but it also makes it a bit tricky to perform this task against remote computers. And yet, paradoxically, although the FileSize problem makes this task harder than you’d expect, the final solution (which uses the FileSystemObject) is easier than you might expect. In fact, that final solution requires no more code than this:

On Error Resume Next

Set FSO = CreateObject("Scripting.FileSystemObject")
ShowSubFolders FSO.GetFolder("C:\")

Sub ShowSubFolders(Folder)
    For Each Subfolder in Folder.SubFolders
        If Subfolder.Size = 0 Then
            Wscript.Echo Subfolder.Path
        End If
        ShowSubFolders Subfolder
    Next
End Sub

As you can see, we start out by implementing the On Error Resume Next statement (just in case we run into any problems). We then create an instance of the Scripting.FileSystemObject. That brings us to this line of code:

ShowSubFolders FSO.GetFolder("C:\")

ShowSubFolders is a “recursive subroutine” that we’ll use - repeatedly - in order to get at all the subfolders in C:\, including all the subfolders in those subfolders, and the subfolders in those sub-subfolders, etc. We won’t discuss recursion in any detail today; that goes a bit beyond the scope of this column. (If you thought the Grandfather Paradox was difficult to conceptualize, wait till you try tackling recursion!) However, you can find a brief introduction to recursion in the Microsoft Windows 2000 Scripting Guide.

Note. No, you don’t have to go back in time to the year 2000 in order to read the Scripting Guide; just access it online. If you do go back in time, though, say hi to your grandfather for us.

Inside our subroutine we use this line of code to retrieve a collection of all the top-level subfolders found in C:\:

For Each Subfolder in Folder.SubFolders

Good question: what do we mean by “top-level” subfolders? That means we’re going to get back a subfolder like C:\Scripts; however, we will not get back any sub-subfolders like C:\Scripts\WMI or C:\Scripts\ADSI. The SubFolders property works on only one level of subfolders at a time. But we’ll show you how to get around that issue in just a minute.

Once we have a collection of subfolders we use this line of code to determine whether the folder is empty (that is, whether or not it has a Size equal to 0):

If Subfolder.Size = 0 Then

If the Size is equal to 0 we echo back the folder Path. If the Size is not equal to 0, well, in that case we don’t do anything at all.

This is where things get a little paradoxical:

ShowSubFolders SubFolder

You’re right: we’re already inside the ShowSubFolders subroutine and yet now we’re calling that same very same subroutine. That sounds crazy (and it is), but such is the nature of recursion. The basic idea is this: we start out in C:\ and get a collection of folders. Let’s say the first folder we get back is C:\Scripts. We check the file size, then call the ShowSubFolders subroutine to get back a new collection, one consisting of all the subfolders found in C:\Scripts. Let’s assume there’s just one such subfolder: C:\Scripts\WMI. We check the file size and then call ShowSubFolders again, this time to see if C:\Scripts\WMI has any subfolders. This process continues until we run out of subfolders, in which case we go back to C:\ and check the next subfolder in that collection.

That’s OK; it gives us a headache, too. But it works and, fortunately, you don’t have to keep track of which subfolders you’ve looked at and which subfolders you haven’t looked at; VBScript takes care of that for you.

Note. Originally VBScript didn’t keep track of that stuff for you. But we sent Peter back in time and had him change that. He was supposed to bring us some candy bars, too, but he said he got hungry on the trip back.

Believe it or not, that’s all we have to do. Run the script and you should get back a list of empty folders:

C:\Documents and Settings\Test\Local Settings\Temp
C:\Documents and Settings\Test\NetHood
C:\Documents and Settings\Test\PrintHood
C:\drvrtmp
C:\Inetpub\wwwroot\aspnet_client\system_web\2_0_50727
C:\Program Files\Accessories
C:\Program Files\Accessories\Imagevue
C:\Program Files\CA\eTrust Antivirus\ArcTemp

Notice that we have C:\Program Files\Accessories and C:\Program Files\Accessories\Imagevue. That’s a sign that we’re retrieving all the subfolders, and not just the top-level ones.

As an added bonus, it turns out that you can even use this script against remote computers, provided that you use administrative shares like C$. If that’s the case, then the following script will return a list of all the empty folders found on drive C of the computer atl-fs-01:

On Error Resume Next

Set FSO = CreateObject("Scripting.FileSystemObject")
ShowSubfolders FSO.GetFolder("\\atl-fs-01\C$")

Sub ShowSubFolders(Folder)
    For Each Subfolder in Folder.SubFolders
    If Subfolder.Size = 0 Then
        Wscript.Echo Subfolder.Path
    End If
        ShowSubFolders Subfolder
    Next
End Sub

So is this the end of today’s column? Well, we’re not sure. After all, to paraphrase Zeno’s Paradox, before you can reach the end of the column you have to reach the halfway point of the column. But before you can reach the halfway point of the column you have to reach the halfway point of the halfway point. But before you can reach - well, the gist of the paradox is that you never, ever reach the end of the column.

Which is probably the case for a lot of our befuddled readers.


How Can I Locate All the Folders That Have \Archive\ASN\ in the Path?

Hey, Scripting Guy! How can I locate all the folders on a computer that have \Archive\ASN\ somewhere in the path?

-- AB

Hey, AB. We hope this message finds you well. Perhaps you are surprised to hear from us like this; a mutual friend provided us with your name and assured us that you were a responsible and trustworthy person.

Recently we Scripting Guys discovered that we are all the illegitimate children of the late Ken Myer, former defense minister for the country of Freedonia. As defense minister our father was permitted to retain a small portion of each defense contract as payment for the services he rendered. Being a simple and honest man, he managed to save 50 million American dollars, all deposited in the Second National Bank of Freedonia.

Of course, as happens so often to defense ministers in Freedonia, our father was recently assassinated. Before he died he managed to contact all of us and told us about the money; unfortunately, he went to his reward before he was able to give us the PIN number. That is why we are turning to you, AB, to ask for your help in transferring this money to an American bank account.

What’s that? How much will you get for doing this? Well, to tell you the truth, we didn’t think about that; it never occurred to us that someone might actually fall for this. Tell you what, AB: if you can figure out a way to smuggle $50 million out of Freedonia we’ll let you keep $43 million; all we ask is that the rest be divided equally among the Scripting Guys:

· $1 million for Greg

· $1 million for Jean

· $1 million for Greg

· $1 million for Dean

· $1 million for Greg

· $1 million for Peter

· $1 million for Greg

Seems fair enough.

Originally we intended to provide you with a $10,000 advance, just so you would know we were serious about this. Unfortunately, though, the current political situation in Freedonia makes it impossible for us to return home and withdraw the money. However, we do have something which many people would say is worth far more than $10,000: a script that can locate all the folders on a computer that have \Archive\ASN\ somewhere in the path.

In other words, a script like this one:

strComputer = "."

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Path Like '%\\Archive\\ASN\\%'")

For Each objFolder in colFolders
    Wscript.Echo "Name: " & objFolder.Name
Next

Before we go any further we have something to confess: this script only works on Windows XP or Windows Server 2003. That’s because it uses WMI’s Like operator, an operator available only on those two operating systems. However, before we sign off for the day, we’ll show you a “brute force” script that will work on other versions of Windows.

And don’t worry; we won’t forget. After all, if you can’t trust the illegitimate children of the former defense minister of Freedonia who can you trust?

As for the Windows XP/Windows Server 2003 script, we start out by connecting to the WMI service on the local computer (although we could just as easily retrieve this same information from a remote computer). We then issue the following query:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Path Like '%\\Archive\\ASN\\%'")

As you can see, what we’re doing here is querying the Win32_Directory class, asking for a list of all the folders that have \\Archive\\ASN\\ somewhere in the path. (Don’t worry; we’ll explain that in a second.) Notice the syntax we use in this query. To begin with, we use the Like operator rather than, say, an equals sign. In addition, we surrounding the target string with percent signs (%). In the wild and wacky world of WMI queries, the percent sign is a wildcard character meaning “any character or set of characters;” that means it serves the same function as the asterisk does in a command like this:

dir *.*

By putting percent signs before and after the target string we’re essentially saying, “Find any folder that has \\Archive\\ASN\\ somewhere in its path. We don’t care what characters, if any, come before the target string, and we don’t care what characters, if any, come after the target string. Just make sure that \\Archive\\ASN\\ shows up somewhere.”

Now, why \\Archive\\ASN\\ rather than \Archive\ASN\? Well, as it turns out, the \ is a reserved character in WMI. Among other things, that means this character must be “escaped” any time it’s used in a WMI query. How do you escape a character in WMI? Oddly enough, you put a \ in front of it. Therefore, if we have a path like \Archive\ASN\ that means we need to escape each and every \; in turn, that gives us a path that looks like this:

\\Archive\\ASN\\

All that’s left to do now is set up a For Each loop to walk through the collection and echo back the Name of each folder that has \Archive\ASN\ somewhere in its path:

For Each objFolder in colFolders
    Wscript.Echo "Name: " & objFolder.Name
Next

As we noted, if you’re running a version of Windows that shipped prior to Windows XP this script won’t do you any good; that’s because the version of WMI that came with those operating systems doesn’t support the Like operator. However, you can achieve the same net effect by using the following script, which returns a collection of all the folders on the computer, and then uses the InStr function to see if the string \Archive\ASN\ can be found anywhere within the folder path:

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory")

For Each objFolder in colFolders
    If InStr(objFolder.Path, "\Archive\ASN\") Then
        Wscript.Echo "Name: " & objFolder.Name
    End If
Next

And you’re right: here we used the string \Archive\ASN\ instead of \\Archive\\ASN\\. That’s because we only need to escape the \ character when that character appears in a WQL query.

That should do the trick, AB; if you have any questions, just let us know. And yes, now that you mention it, the Scripting Editor still is on vacation. But it’s just a coincidence that we waited until she left town before trying to con – um, trying to recruit people to help us retrieve our $50 million.


How Can I Open a Window to a UNC Location?

Hey, Scripting Guy! How can I open a window to a UNC location? You've given many examples that involve opening a web page. I have been unable to get a similar result trying to get a script to open \\server\share.

-- CY

Hey, CY. You know, during the heyday of the Cold War the U.S. had analysts whose sole duty was to pore over photographs taken in the Soviet Union and try to figure out what was really going on behind the Iron Curtain. Who was standing next to whom? Who had been relegated to the background? Who had been airbrushed out of the picture altogether? Just what was going on over there?

Today those same analysts spend their time poring over Hey, Scripting Guy! columns. Would the Scripting Guys “tell you porkies?” What was Frank “Home Run” Baker known for besides leading major league baseball in home runs four times? Can you really make scrambled eggs just by shaking a chicken really hard right before she lays her eggs? Just what is going on over there?

Needless to say, we can’t reveal all our secrets. But here’s a tip. Sometimes you’ll hear the Scripting Guys say something like, “You know, this is something we get asked quite a bit and, fortunately, the solution is quite simple.” They’ll then proceed to show everyone a two-line script. Does that mean that this is a question that the Scripting Guys really do get asked quite a bit? Well, maybe. What it mostly means, however, is that the Scripting Guys are feeling lazy that day and are trying to get away with writing two lousy lines of code and calling it a column. Don’t let them get away it! As loyal readers of Hey, Scripting Guy! you deserve better than that.

With that out of the way, let’s see if we can tackle today’s problem: opening a window to a UNC location. You know, this is something we get asked quite a bit and, fortunately, the solution is quite simple. In fact, this task can be carried out using just two lines of code:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "\\atl-fs-01\public"

As you can see, there isn’t much to this script. We start out by creating an instance of the Wscript.Shell object; once we have a handle to that object all we have to do is call the Run method, passing our UNC path as the sole method parameter. Will that really open \\atl-fs-01\public in a Windows Explorer window? Give it a try and see for yourself.

The only thing you need to watch out for are UNC paths that include blank spaces. For example, consider the following script, one that purports to open the folder \\atl-fs-01\public\ken myer:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "\\atl-fs-01\public\ken myer"

Is that going to work? Unfortunately, no; that’s because Windows is going to assume that the path ends at the first blank space, making the path \\atl-fs-01\public\ken. All you’re going to back is the following error message:

The system cannot find the file specified.

But that’s OK; this is easy to fix. Any time your UNC path includes a blank space (or blank spaces) you need to enclose the entire path in double quote marks. (Yes, in much the same way you need to use double quote marks when typing a command like cd “c:\documents and settings\ken myer” from the command prompt.) Of course, it looks as though we already have double quote marks. However, those double quotes simply indicate that we are dealing with a string value; they aren’t part of the value itself. This is crazy-looking, but the following syntax will take care of the blank space issue:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run """\\atl-fs-01\public\ken myer"""

Three double quote marks before the path and three double quote marks after the path. That’s the recipe for success, in life as well as in scripting.

Here’s a little bonus script, one that we won’t discuss in much detail. This script returns a collection of all the shared folders on a computer (in this case, a remote computer named atl-fs-01), taking care to eliminate all the administrative shares. (For our purposes, that means any shared folder that, like C$ or printers$, has a $ in the name.) After returning the collection the script constructs a UNC path for each shared folder and then opens that folder in its very own window:

strComputer = "atl-fs-01"

Set objShell = CreateObject("Wscript.Shell")

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_Share")

For Each objItem in colItems
    If InStr(objItem.Name, "$") = 0 Then
        strPath = "\\" & strComputer & "\" & objItem.Name
        objShell.Run strPath
    End If
Next

Pretty slick, huh?

And before we go, here’s another handy hint for you. Any time the Scripting Guys start handing out bonus scripts that can mean only one thing: they’re feeling a little guilty about not putting much effort into their work. Just something to watch out for.


How Can I Rename a Folder?

Hey, Scripting Guy! How can I add a suffix to a folder name? For example, I’d like to rename the folder C:\January to C:\January_2006.

-- NB

Hey, NB. You know, back in July the Scripting Guy who writes this column happened to wander through the living room while the Scripting Son was watching the world-famous Nathan’s Hot Dog-Eating Contest on ESPN. (And no, don’t worry: this isn’t the start of some long and pointless rant against eating contests being broadcast on a sports channel. After all, if ESPN is going to show spelling bees, poker games, and soccer, well, why not hot dog-eating contests, too?) The Scripting Guy who writes this column watched for a few minutes and, truth be told, he was a bit repulsed by what he saw. Good heavens, he thought, how could people sit there and continually, and mindlessly, stuff themselves full of food like that? What is wrong with those people?!?

Of course, that was this summer, long before the Thanksgiving holiday. Now, after four days of turkey, mashed potatoes and gravy, sweet potatoes, stuffing, rolls, stir-fried green beans and bacon, assorted Jellos and salads, pumpkin pie, apple-raspberry pie – well, let’s just say that the Scripting Guy who writes this column now has a lot more empathy for people who mindlessly stuff themselves full of food.

Note. What’s that? What was the Scripting Guy who writes this column thankful for this past holiday season? Mainly he was thankful that he has a job where he can just sit all day and not have to move very much.

But don’t worry: he’ll be up and ready to go come time for Christmas dinner.

Fortunately, the Scripting Guy who writes this column doesn’t have to move very much in order to answer your question, NB. You need a script that can add a suffix to a folder name? Here you go:

strComputer = "."

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\January'")

For Each objFolder in colFolders
    strNewName = objFolder.Name & "_2006"
    objFolder.Rename strNewName
Next

Yes, this is a very simple script; what else would you expect for a column being written shortly after Thanksgiving? As you can see, we begin by connecting to the WMI service on the local computer, although – as with nearly all WMI scripts – we could just as easily run this script against a remote machine. (Meaning, in other words, that you can add a suffix to a folder found on a remote computer.) After making this connection we then use this line of code to retrieve a collection of all the folders on the computer that have a Name (which, in WMI-speak, is equivalent to the file path) equal to C:\January:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\January'")

And no, we didn’t eat too much cranberry, walnut, and apple salad this weekend. Well, OK, actually we pretty much ate all of it, leaving only a spoonful or two for the rest of the family. However, that isn’t why we typed the name like this: C:\\January. Instead, the two \’s are actually required; that’s because the \ is a reserved character in WMI, and any time you use a reserved character in a WMI query you need to “escape” that character by prefacing it with a \. What if we wanted to rename the folder C:\January\Personnel\Reviews? Then we’d use a query like this one, taking care to escape each \ in the path:

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\January\\Personnel\\Reviews'")

After executing the query we’ll get back a collection consisting of all the folders on the computer that have the Name C:\January. (And, because folder Names – or paths – must be unique, we know the collection will contain, at most, one item.) With the collection in hand we next set up a For Each loop to loop through each of the folders in that collection. Inside that loop, we construct a new name for the folder by taking the existing folder Name and adding the string value _2006, storing the new path (C:\January_2006) in a variable named strNewName:

strNewName = objFolder.Name & "_2006"

Once we’ve constructed the new name we can then use the Rename method to rename C:\January, making sure that we pass the value strNewName as the method parameter:

objFolder.Rename strNewName

And yes, in order to rename a folder you must pass the entire path for the new folder. Passing just the folder name (January_2006) won’t do you any good.

Because we probably should try to get a little exercise we decided to toss in a bonus script. Suppose you have a folder – C:\Budget – that contains a bunch of subfolders (January, February, March, etc.). What if you wanted to tack a suffix onto each of those subfolders? No problem; this little script (which we won’t discuss in any detail today) should do the trick:

strComputer = "."

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

strFolderName = "C:\Budget"

Set colFolders = objWMIService.ExecQuery _
    ("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
        & "Where AssocClass = Win32_Subdirectory " _
            & "ResultRole = PartComponent")

For Each objFolder in colFolders
    strNewName = objFolder.Name & "_2006"
    objFolder.Rename strNewName
Next

Wow; that was more tiring than we expected it to be.

Incidentally, we know that many of you are dying to know who won this year’s Terducken Bowl. Unfortunately, we only managed to get halfway to 100: the game is currently tied at 49-49, and will have to be completed at Christmas.

So why weren’t we able to get in a complete game? Well, to begin with, two of the players (the Scripting Nephews) were late because they had to help clean house. (We were shocked, too: on the day of last year’s Super Bowl did the Pittsburgh Steelers make Ben Roethlisberger and Jerome Bettis clean their houses before letting them leave for the game?) On top of that, the game was played in a hurricane, with winds gusting around 250,000 miles per hour. And no, that’s not an exaggeration: at one point one of the Scripting Nephews threw a pass directly down the middle of the field. A huge gust of wind came up and not only blew the ball off course, it blew the ball completely off the field, over the fence and on into the neighbor’s yard.

But that’s OK; if nothing else, this gives the Scripting Guy who writes this column something to look forward to come Christmas time. What’s that? What about the food? Oh, sure, he’ll probably eat something. But just to be polite; after all, he wouldn’t want to hurt the Scripting Mom’s feelings. Maybe just a little turkey, a little gravy, and a tiny sliver of pie.

OK, maybe two tiny slivers of pie.


How Can I Search for Folders That Have a Dot in Their Name?

Hey, Scripting Guy! How can I search for folders that have a dot in their name? For example, a folder like Administrator.name or Folder.old?

-- DU

Hey, DU. You know, if your intention was to trick the Scripting Guys (not that tricking the Scripting Guys would be anything to brag about), well, you almost succeeded. That’s because, upon first reading your question, we immediately starting planning an elaborate series of scripts that would use wildcard characters and the WMI LIKE operator to search for folders that had a dot somewhere in their name. Of course, the LIKE operator is supported only on Windows XP and Windows Server 2003, which meant we had to simultaneously develop an even more elaborate series of scripts to perform this same task on Windows 2000. This was going to be hard, but we knew that the end result would be the largest, most complicated, and most breathtaking script ever created.

As you might expect, after hundreds of hours of work and countless nights without sleep, we realized there was a much easier (albeit less breathtaking) way to do this:

strComputer = "."

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

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory Where Extension <> ''")

For Each objFolder in colFolders
    Wscript.Echo objFolder.Name
Next

You’re right: there’s not a wildcard character or a LIKE operator to be found in our script. Why not? Because it turns out that we don’t need them. That’s because any time there is a dot in a folder name WMI treats the value after the dot as a file extension. For example, with a folder named Administrator.old, Administrator is considered the file name and old is considered the file extension. How about a folder named Administrator.old folder? In that case, old folder is considered the file extension. Here’s another one: Administrator.old.folder. Turns out that, if you have two or more dots in the name, only the value that comes after the last dot – folder – is considered the file extension. Everything else (Administrator.old) makes up the file name.

Does that really matter to any of us? You bet it does. If a folder has a dot somewhere in its name (and, as we’ve seen, it doesn’t matter where that dot occurs) that means that the folder has a file extension. In turn, that means we can find all the folders that have a dot in their name simply by looking for all the folders that have a file extension.

And that’s exactly what we do in our script. We start off by connecting to the WMI service on the local computer, although you could also – um, that’s right, you could also run this script against a remote computer. Guess we must have mentioned that somewhere before, huh? At any rate, after we connect to the WMI service we then encounter this line of code:

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory Where Extension <> ''")

This is the heart and soul of our solution. What we’re doing here is asking WMI to return all the instances of the Win32_Directory class that have a file extension of some kind. To do that we simply request all the instances of Win32_Directory where the value of the Extension property is not a blank string (that is, it has some kind of value). Because there will be a file extension only if there’s a dot somewhere in the name we know that any folder we get back will – um, that’s right, have a dot somewhere in its name.

Do we keep repeating ourselves over and over again? That must be because the Scripting Guy who edits this column is beginning to show her age a bit. Poor thing: it must be hard to grow old. (Editor’s Note: The editor will have to ask the writer about that, since the writer is much older. And, if for some reason you believe what you heard on TechNet Radio , that would put the Scripting Guy who writes this column somewhere in his 70s.)

At any rate, our query returns a list of all the folders (directories) that have a file extension. All we have to do then is set up a For Each loop to walk through that list. Inside that loop we do just one thing: we echo back the value of the folder’s Name property:

Wscript.Echo objFolder.Name

And what will that give us? That will give us output similar to this:

c:\program files\microsoft sdks\windows\v1.0\samples\web\iis\isapi_6.0
c:\program files\microsoft visual studio .net 2003
c:\program files\microsoft visual studio 8\sdk\v2.0
c:\program files\microsoft.net

Pretty cool, huh?

Now, as it turns out – and stop us if you’ve heard this one before – if a folder has a dot in its name that means it also has a file extension ….


How Can I Tell if a Folder Has Any Files with a Specific File Extension?

Hey, Scripting Guy! How can I verify whether or not any files with a specific file extension exist in a folder?

-- GM

Hey, GM. This is actually a pretty easy thing to do. All we have to do is write a WMI query that includes the path of the folder we want to check as well as the file extension we’re checking for. For example, this script retrieves a collection of all the .txt files found in the C:\Scripts folder:

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

Set colFiles = objWMIService. _
    ExecQuery("SELECT * FROM CIM_DataFile WHERE Path = '\\Scripts\\' " & _
        "AND Drive = 'C:' AND Extension = 'txt'")

Wscript.Echo "Number of .txt files found: " & colFiles.Count

If you look closely at this script (and you guys do look closely at all this stuff, don’t you?) you might notice a couple things. First, we didn’t specify the path as C:\Scripts; instead, we separated the drive (C:) from the folder (Scripts). Why did we do that? Well, because – for better or worse – that’s how the CIM_DataFile class works; what we think of as the file path is divvied up between the drive and the folders. The Drive parameter is actually optional here; we could leave it out unless we also have a folder named D:\Scripts. If that was the case, then the Drive parameter becomes mandatory; leave it out, and the query returns all the files found in a folder with the path Scripts. That would include D:\Scripts (and E:\Scripts and F:\Scripts and …) as well as C:\Scripts.

Second, you might note that we used double \\’s to surround the Path; thus we have \\Scripts\\ rather than \Scripts\ (i.e., C:\Scripts). Why? Again, that’s just the way WMI works; any time you include a file path in a WHERE clause you need to use two \\’s. What if we wanted to check the folder C:\Documents and Settings\Ken Myer\Desktop\Work Files to see if it had any .txt files in it? In that case, our WHERE clause would look like this:

WHERE Path = '\\Documents and Settings\\Ken Myer\\Desktop\\Work Files\\

Note, too the two \\’s at the end of the Path. Those are important; don’t leave them out. (Well, unless you don’t care whether or not the script actually works.) And as long as we’re busy noting things, note that we don’t include the period when specifying the file extension; it’s just txt, not .txt.

After we get our collection back, all we have to do is echo the value of the collection’s Count property. This will tell us how many .txt files were found. If the Count is 0, we didn’t find any; if the Count is 7, we found 7. If – well, you can figure out the rest yourself.

See, we told you this was easy. Hey, every once in a while, we actually do know what we’re talking about!


How Can I Use a Wildcard Character to Delete Folders?

Hey, Scripting Guy! How can I delete folders based on a wildcard character? For example, how can I delete all the folders whose name starts with December?

-- RR

Hey, RR. Well, that depends. If you’re running Windows XP or Windows Server 2003, you can actually use a wildcard character to identify and then delete all the folders whose name starts with December. If you’re running Windows 2000, you can achieve the same net result: you can identify and delete all the folders whose name starts with December. It’s just that you’ll have to do it in a more clumsy, brute-force fashion.

Let’s start by taking a look at an XP/2003 script that identifies (but doesn’t yet delete) all the folders whose name starts with December:

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

Set colFolders = objWMIService.ExecQuery _
    ("SELECT * FROM Win32_Directory WHERE FileName LIKE 'December%'")

For Each objFolder in colFolders
    Wscript.Echo objFolder.Name
Next

The key to this script lies in the WQL query:

SELECT * FROM Win32_Directory WHERE FileName LIKE 'December%'

What we’re doing here is selecting all the folders that have a FileName like December. FileName is the WMI property corresponding to what we would call the folder name. (And, yes, there’s sometimes a difference between the name we use for something and the name WMI uses for that same thing.) For example, if we have a folder with the path C:\Scripts\Reports\December, the FileName is December.

LIKE ‘December%’ simply means, “Where the FileName starts with December.” The percent sign (%) is the wildcard character (equivalent to the * used most other places as the wildcard character). In this case, the wildcard indicates that we want folder names that start with December, but we don’t care what – if anything – comes after the word December. What if wanted to look for folders whose names ended with the word December? In that case, we’d put the wildcard at the beginning of the string, like so:

SELECT * FROM Win32_Directory WHERE FileName LIKE '%December'

And what if we wanted folders whose name simply contained the word December (for example, Reports_December_2005)? In that case, we’d put a wildcard before and after the string:

SELECT * FROM Win32_Directory WHERE FileName LIKE '%December%'

The LIKE operator is available only on Windows XP and Windows 2003. If you want to perform this task on a Windows 2000 computer, you’ll have to retrieve the list of all the folders on the machine and then check to see if any of them have a folder name (FileName) starting with December. That’s what we do with this script:

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

Set colFolders = objWMIService.ExecQuery("SELECT * FROM Win32_Directory")

For Each objFolder in colFolders
    If Left(objFolder.FileName, 8) = "december" Then
        Wscript.Echo objFolder.Name
    End If
Next

With this script we get back a collection of all the folders on a computer, and then check each one to see if the first (leftmost) 8 characters happen to be december. If they are, we echo the folder name; if not, we move on to the next folder in the collection.

This works just fine, although it does take a bit longer to complete. On a test computer with a couple thousand folders, the XP/2003 version took around 15 seconds to complete; the Windows 2000 version took over a minute. But at least it does the job.

Now, what about deleting all the folders that you find? Well, to do that, just substitute the Delete method for the line of code where we echo the folder name. For example, here’s the XP/2003 version of a script that both finds and deletes all folders whose name starts with December:

On Error Resume Next

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

Set colFolders = objWMIService.ExecQuery _
    ("SELECT * FROM Win32_Directory WHERE FileName LIKE 'December%'")

For Each objFolder in colFolders
    objFolder.Delete
Next

Needless to say, be careful about running this particular script; after all, it will delete all the folders it finds whose name starts with December. In some cases, it might be a good idea to first retrieve a list of all these folders and make sure there aren’t any surprises in there (that is, folders you don’t really want to delete that just happen to have a name starting with December) before you start deleting things willy-nilly.


How Can I Use Information in an Excel Spreadsheet to Rename a Set of Folders?

Hey, Scripting Guy! How can I rename a bunch of folders using a script? The old folder names are in the first column of an Excel spreadsheet and the new folder names are in the second column of an Excel spreadsheet.

-- SF

Hey, SF. Oh, and welcome to Hey, Scripting Guy!, your daily source for scripting information. We’d like you to know that Hey, Scripting Guy!, unlike so many other daily scripting columns, is never outsourced; instead, the Scripting Guys create each column themselves, right here in the US of A. Hey, Scripting Guy!, a symbol of American pride and cratfmanshpi.

Uh, craftsmanship.

At any rate, thanks for the question, SF. Could you have found a more reputable and reliable source for scripting information? Probably. But seeing as how you didn’t find a more reputable and reliable source for scripting information we’ll see if we can help. Can we come up with a script that can take information from an Excel spreadsheet and then use that information to rename a bunch of folders? We’re about to find out.

Let’s start by taking a peek at our spreadsheet, a very simple affair in which – as you noted, SF – the old (existing) folder names are in column 1 and the corresponding new folder names are in column 2:

In other words, we currently have a folder named C:\Scripts\Test Folder 1; when we’re done, we want this folder to have a new name: C:\Scripts\Active Directory. How are we going to do that? Like this:

strComputer = "."

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

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
objExcel.Visible = True

i = 1

Do Until objExcel.Cells(i, 1).Value = ""
    strOldFolderName = objExcel.Cells(i, 1).Value
    strNewFolderName = objExcel.Cells(i, 2).Value
    strOldFolderName = Replace(strOldFolderName, "\", "\\")

    Set colFolders = objWMIService.ExecQuery _
        ("Select * from Win32_Directory where Name = '" & strOldFolderName & "'")

    For Each objFolder in colFolders
        errResults = objFolder.Rename(strNewFolderName)
    Next

    i = i + 1
Loop

As you can see, we start out by connecting to the WMI service on the local computer. What’s nice about this script (and nearly all WMI scripts, for that matter) is that we aren’t limited to working with the local machine. Want to rename folders on, say, the remote computer atl-fs-01? No problem; just assign the name of that remote computer to the variable strComputer, like so:

strComputer = "atl-fs-01"

After connecting to the WMI service we then use this block of code to create an instance of the Excel.Application object, open the spreadsheet C:\Scripts\Test.xls, and then make this instance of Excel visible onscreen:

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
objExcel.Visible = True

And no, Excel doesn’t have to be visible onscreen in order for this script to work; we make the application visible just so you can see what’s going on. If you’d just as soon have Excel run in a hidden window then remove the line of code that sets the Visible property to True. Oh, and be sure and add code like this at the end of the script to make sure Excel quits when the script is finished:

objExcel.Quit

With Excel up and running we next set the value of a counter variable named i to 1; we’ll use this variable to keep track of the current row in the spreadsheet. That brings us to the following Do Until loop:

Do Until objExcel.Cells(i, 1).Value = ""

As you can see, with this loop we’re checking the value of a cell in the spreadsheet. Which cell? Well, for starters, we’re checking the cell in row 1 (remember, the counter variable i is equal to 1), column 1. And what we’re doing is checking to see whether or not the cell is empty (that is, whether or not the Value is equal to “”). If the cell is empty that means we’ve run out of folder names. In that case we exit the loop and the script terminates.

Note. Needless to say, if you use this approach you need to make sure there are no blank rows in your spreadsheet. If there are blank rows, the first time the script encounters such a row it will assume that all the folders have been dealt with and exit the loop.

If the cell isn’t empty we assume that’s because there’s a folder name listed there. With that in mind we then execute the following two lines of code. The first line stores the current folder name in the variable strOldFolderName; the second line looks at cell i, 2 (row 1, column 2) to get the new folder name, a value that is stashed in the variable strNewFolderName:

strOldFolderName = objExcel.Cells(i, 1).Value
strNewFolderName = objExcel.Cells(i, 2).Value

Among other things, that means strOldFolderName is now equal to C:\Scripts\Test Folder 1. Believe it or not, that’s a problem. Why? Well, in a minute or two we’re going to use strOldFolderName in a WMI query; that’s a problem because the \ is a reserved character in WMI, and any \’s that show up in a query will trigger an error. Therefore, we need to use the following line of code to “escape” each \, a process which simply means prefacing each \ with a second \:

strOldFolderName = Replace(strOldFolderName, "\", "\\")

That makes strOldFolderName equal to C:\\Scripts\\Test Folder 1. That looks weird, but it can now be used in a WMI query.

Note. We don’t have to do anything to strNewFolderName; we can leave the value at C:\Scripts\Active Directory. This variable won’t be used in a query, which means we don’t have to escape all instances of the \ character.

Speaking of WMI queries, our next line of code just happens to be a WMI query, one that asks for a collection of all the folders on the computer that have a Name (path) equal to strOldFolderName:

Set colFolders = objWMIService.ExecQuery _
    ("Select * from Win32_Directory where Name = '" & strOldFolderName & "'")

All we have to do now is set up a For Each loop to loop through all the folders in the collection (and, because folder paths must be unique on a computer, the collection will contain, at most, a single item). Inside that loop we use this line of code to take the existing folder and rename it to the folder path specified by the variable strNewFolderName:

errResults = objFolder.Rename(strNewFolderName)

After calling the Rename method we increment the value of our counter variable by 1, then loop around and repeat the process with the new row in the spreadsheet. When we’re all done each of the 5 folders listed in the spreadsheet will have shiny new names. Just like we wanted them to.

Incidentally, it is true that the Scripting Guys write this column themselves; unlike other high-tech entrepreneurs we never outsource our work to developing countries. Not that we didn’t try, mind you; we just couldn’t find any developing country desperate enough (or silly enough) to want to write a daily scripting column.


How Can I Delete the C$ Share from a Computer?

Hey, Scripting Guy! How can I delete the C$ share from a computer?

-- RB

Hey, RB. One quick clarification here: we can tell you how to temporarily delete the C$ share from a computer. However, the share will automatically be recreated any time the Server service is stopped and then restarted, or any time the computer is rebooted. Why? That’s just the way these so-called “administrative shares” work. They’re like dandelions or stray cats: once you have them, you can’t really get rid of them.

That said, here’s a script that will at least temporarily delete the C$ share:

strComputer = "."

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

Set colShares = objWMIService.ExecQuery _
    ("Select * from Win32_Share Where Name = 'C$'")

For Each objShare in colShares
    objShare.Delete
Next

This script starts off by connecting to the WMI service on the local computer. We then use this line of code to retrieve a collection of all the shares that have the Name C$:

Set colShares = objWMIService.ExecQuery _
    ("Select * from Win32_Share Where Name = 'C$'")

Because share names must be unique on a computer our collection will have – at most – just one item in it. Because it is a collection, however, we still need to set up a For Each loop to cycle through all the members in the collection. Inside that loop we call the Delete method to stop sharing the folder.

Note. Despite the name, the Delete method doesn’t actually delete anything; it simply stops sharing the folder. The Delete method will not remove the share or any of its contents; it just prevents the data from being shared over the network.

Like we said, this effect is temporary: if the Server service is stopped and restarted or if the computer is rebooted then the C$ share will magically appear. Because of that you might want to put this code in a logon or computer startup script; that way the share will be removed even if the computer gets rebooted.

And, yes, you can delete the C$ share (or any other share) on a remote computer; all you have to do is assign the name of that computer to the variable strComputer. For example, here’s a revised script that deletes the C$ share on the remote computer atl-ws-01:

strComputer = "."

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

Set colShares = objWMIService.ExecQuery _
    ("Select * from Win32_Share Where Name = 'C$'")

For Each objShare in colShares
    objShare.Delete
Next

Hasta la vista, C$.


How Can I Determine the Local Path to a Shared Folder?

Hey, Scripting Guy! If I have a UNC path like \\server1\test, can I use a script to determine the local path to that shared folder (like C:\Scripts\Test Folder)?

-- JVK

Hey, JVK. Yes, you are correct: the Kirkland Fire did open their season last night, rallying for 4 runs in the last two innings to defeat Risan 4-2. And, yes, everyone in the league seems to think that Risan will be the team to beat this season. Mission accomplished.

Anyway, thanks for writing. Bye.

Oh, right; we almost forgot. Can you use a script to determine the local path to a shared folder? Of course you can (although we don’t know why anyone would bother to do that during baseball season). But if that’s what you want to do, well, here’s how you can do it:

strPath = "\\atl-fs-01\public"

strPath = Replace(strPath, "\\", "")

arrPath = Split(strPath, "\")

strComputer = arrPath(0)
strShare = arrPath(1)

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Share Where Name = '" & strShare & "'")

For Each objItem in colItems
    Wscript.Echo objItem.Path
Next

As you can see, we start off with a typical UNC path: \\atl-fs-01\public. We store that path, incidentally, in a variable named strPath. With the path name in hand we have to do two things. First, we need to separate the computer name (atl-fs-01) from the share name (public). Once that’s done we then need to connect to the computer atl-fs-01 and determine the local path for the public folder. How hard is that going to be? Not hard at all.

Especially for those of us who just managed to beat their “unbeatable” rivals. Did we mention that yet? The final score was 4-2, even though we didn’t play all that well. Must be really good coaching ….

But back to our regularly-scheduled program. To begin with, we need to get rid of the \\ that prefaces the UNC path. Although there are a couple different ways we could do that, we simply used the Replace function to replace each instance of \\ with nothing:

strPath = Replace(strPath, "\\", "")

After that line of code executes, the variable strPath will be equal to atl-fs-01\public. In turn, that means we can use the Split function to split this value into an array:

arrPath = Split(strPath, "\")

By splitting on the \, we end up with an array consisting of two items:

· atl-fs-01

· public

Needless to say, those are the two pieces of information we’re looking for: the name of the computer and the name of the shared folder. With that in mind, we then assign the first item in the array (item 0) to a variable named strComputer, and the second item in the array (item 1) to a variable named strShare:

strComputer = arrPath(0)
strShare = arrPath(1)

So now do we get to the hard part? Believe it or not, that was the hard part; from here on we just use a standard WMI script. We start out by connecting to the WMI service on the remote computer. (Which remote computer? Atl-fs-01, the computer whose name is stored in the variable strComputer.) We then use the ExecQuery method to retrieve a collection of all the shared folders on that computer:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Share Where Name = '" & strShare & "'")

Good point: we aren’t getting all the shared folders, are we? Instead, we’re getting only those shared folders that have a Name equal to the value stored in the variable strShare. (If you dozed off for a moment, strShare is equal to public, the name of the shared folder we’re interested in.) All that’s left now is to set up a For Each loop to walk through the collection (because shared folder names must be unique on a computer, there will be only one item in the collection) and echo back the value of the Path property. As you probably guessed, the Path property tells us the local path to the folder on atl-fs-01:

D:\Scripts\Public

That’s all you have to do to take a UNC path and determine the local folder path.

And now for the most important part. There we were, runners on first and second, down 2-0, when the third base coach daringly called for a double steal that put us in position to tie the score. Pretty gutsy call, huh? Wonder who that fearless baseball genius could have been ….


How Can I Share a Folder on a Remote Computer?

Hey, Scripting Guy! How can I share a folder on a remote computer?

-- RS

Hey, RS. One of the great things about WMI is the fact that – with one or two fairly obscure exceptions – anything you can do on the local computer you can also do on a remote computer. This is the big advantage scripting has over command line tools; a number of command line tools (including tools like net share) work only on the local computer. If you want to do something remotely, a WMI script is often the only way to go.

So how do you share a folder on a remote computer? Here’s one way:

Const FILE_SHARE = 0
Const MAXIMUM_CONNECTIONS = 25

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")

Set objNewShare = objWMIService.Get("Win32_Share")

errReturn = objNewShare.Create _
    ("C:\Public", "PublicShare", FILE_SHARE, _
        MAXIMUM_CONNECTIONS, "Public share for Fabrikam employees.")

The preceding script shares out the folder C:\Public on a computer name atl-ws-01. The script starts by setting a pair of constants. First we assign the value 0 to the constant FILE_SHARE (which indicates the type of shared resource we are creating); if we set the value of this constant to 2147483648 we could create an Administrative share rather than a standard file share. We also assign the value 25 to the constant MAXIMUM_CONNECTIONS, which sets the maximum number of simultaneous connections. If we wanted to allow an unlimited number of simultaneous connections we wouldn’t use this constant at all, and then leave this parameter blank when we create the share.

After connecting to the WMI service on the remote computer we then connect to the Win32_Share class. At that point, all we have to do is call the Create method, passing the following five parameters:

Parameter | Description
“C:\Public” | The local path to the folder being shared.
“PublicShare” | Share name for the shared folder.
FILE_SHARE | The type of share being created.
MAXIMUM_CONNECTIONS | The maximum number of simultaneous users that can connect to the share.
“Public share for Fabrikam employees.” | Optional description of the shared folder.

That’s about all there is to it; run the script, and the folder C:\Public will be shared out as PublicShare. Note that the folder C:\Public must already exist on the computer atl-ws-01; the Win32_Share Create method won’t create the folder for you. If you aren’t sure if the folder C:\Public exists, here’s a quick way to check that:

strComputer = "atl-ws-01"
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")

Set colFolders = objWMIService.ExecQuery _
    ("Select * From Win32_Directory Where Name = 'C:\\Public'")
Wscript.Echo colFolders.Count

This script echoes back the number of folders named C:\Public (note that in the query itself this must be specified as C:\\Public) found on the computer atl-ws-01. If the number of folders equals 0, then C:\Public doesn’t exist. If the number of folders equals 1, then it does exist.

By the way, we know a lot of you are going to ask about setting permissions on a shared folder. This can actually be done in a script, but the process is a bit complicated and requires more detailed discussion than we can provide in this column. But we hope to post a considerable amount of material explaining how to work with security descriptors very soon.


How Can I Determine if a Computer is a Laptop or a Desktop Machine?

Hey, Scripting Guy! Is there any way to use WMI to determine whether a computer is a laptop or a desktop machine?

-- CB

Hey, CB. You bet there is, although if you didn’t know that we have no idea how you’d figure it out on your own. It turns out there’s a goofy little WMI class called Win32_SystemEnclosure, and the ChassisTypes property can tell you whether a computer is a laptop, a desktop, or some other kind of top. Uh, type. Here a script that tells you what type of chassis you’re dealing with:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colChassis = objWMIService.ExecQuery _
    ("Select * from Win32_SystemEnclosure")
For Each objChassis in colChassis
    For  Each strChassisType in objChassis.ChassisTypes
        Wscript.Echo strChassisType
    Next
Next

Ok, so the preceding script only kind of tells you what type of chassis you’re dealing with. The ChassisType property actually returns a number, which means when we run the script on the computer sitting here in front of us we get back a 10. What’s a 10? According to the WMI SDK, that means this is a notebook computer. And guess what? It is!

But you don’t have to refer to the SDK each time you check the chassis type. Here’s an expanded version of the script that uses a Select Case statement to convert these integers to something a bit more meaningful:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colChassis = objWMIService.ExecQuery _
    ("Select * from Win32_SystemEnclosure")
For Each objChassis in colChassis
    For  Each strChassisType in objChassis.ChassisTypes
        Select Case strChassisType
            Case 1
                Wscript.Echo "Other"
            Case 2
                Wscript.Echo "Unknown"
            Case 3
                Wscript.Echo "Desktop"
            Case 4
                Wscript.Echo "Low Profile Desktop"
            Case 5
                Wscript.Echo "Pizza Box"
            Case 6
                Wscript.Echo "Mini Tower"
            Case 7
                Wscript.Echo "Tower"
            Case 8
                Wscript.Echo "Portable"
            Case 9
                Wscript.Echo "Laptop"
            Case 10
                Wscript.Echo "Notebook"
            Case 11
                Wscript.Echo "Handheld"
            Case 12
                Wscript.Echo "Docking Station"
            Case 13
                Wscript.Echo "All-in-One"
            Case 14
                Wscript.Echo "Sub-Notebook"
            Case 15
                Wscript.Echo "Space Saving"
            Case 16
                Wscript.Echo "Lunch Box"
            Case 17
                Wscript.Echo "Main System Chassis"
            Case 18
                Wscript.Echo "Expansion Chassis"
            Case 19
                Wscript.Echo "Sub-Chassis"
            Case 20
                Wscript.Echo "Bus Expansion Chassis"
            Case 21
                Wscript.Echo "Peripheral Chassis"
            Case 22
                Wscript.Echo "Storage Chassis"
            Case 23
                Wscript.Echo "Rack Mount Chassis"
            Case 24
                Wscript.Echo "Sealed-Case PC"
            Case Else
                Wscript.Echo "Unknown"
            End Select
    Next
Next

Before you ask, no, we don’t really know what most of these chassis types are, nor do we know what the difference is between a portable, a laptop, and a notebook. We also don’t know why the ChassisTypes property is stored as an array. Does that mean a computer could have more than one chassis type? Don’t ask us; we don’t know. But give us a break: after all, we’re the Microsoft Scripting Guys, not the Microsoft Hardware Guys!


How Can I Determine Whether a Computer Has Any USB 2

How Can I Determine Whether a Computer Has Any USB 2.0 Ports?

Hey, Scripting Guy! Using a script, is there any way to determine whether a computer has any USB 2.0 ports?

-- RD

Hey, RD. Are you familiar with the movie Freaky Friday, where the mom and the daughter switch bodies (and roles)? Well, we have sort of a Freaky Friday kind of thing going on with this question. After all, in this column the Scripting Guys are supposed to help you. But this question actually helped one of the Scripting Guys. He had been trying – unsuccessfully – to test a USB device, and had pretty much written the thing off as a piece of junk. Upon reading your question, though, he thought to himself, “Oh, yeah: USB 1.1 and USB 2.0” As soon as he tried the device in a USB 1.1 slot – as opposed to a 2.0 slot – it worked just fine.

Hey, no one ever said this particular Scripting Guy was a genius. (Editor’s Note: I’ll vouch for that.)

So we owe you one, RD; how can we ever repay you? Good point: maybe we could start by answering your question. OK, here’s a script that will probably return the number of USB 2.0 ports on a computer. Let’s show you the code, then we’ll explain why we said it will “probably” return the number of ports:

strComputer = "."
i = 0

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colControllers = objWMIService.ExecQuery _
    ("Select * From Win32_USBController")

For Each objController in colControllers
    If Instr(objController.Name, "Enhanced") Then
        i = i + 1
    End If
Next

Wscript.Echo "No. of USB 2.0 Ports: " & i

This script starts out by assigning values to two variables. The variable strComputer is assigned the value dot (.), which – in WMI-speak – represents the local computer. Meanwhile, the counter variable i is assigned the value 0; we’ll be using this variable to keep a running tally of all the USB 2.0 ports we find.

After connecting to the WMI service we then use this query to return all the instances of the Win32_USBController class:

Set colControllers = objWMIService.ExecQuery _
    ("Select * From Win32_USBController")

From there we set up a For Each loop to walk through each item in the collection (the collection, of course, represents all the USB ports on the computer). Unfortunately, the Win32_USBController class doesn’t have a property (something like, say, Version) that can tell us the USB version; in fact, there’s no obvious way to look at the properties of a USB slot and know whether it’s version 1.1 or version 2.0. However, most 2.0 ports include the word Enhanced somewhere in their name. We can’t guarantee that all of them do, which is why we say that this script will probably return the number of USB 2.0 ports on a computer. In a few random tests around the office, however, this approach seemed to work just fine; any time we found a 2.0 slot it had a name similar to this:

VIA USB Enhanced Host Controller

So how does that help us? Well, that means we can use the InStr function to see whether the word Enhanced appears anywhere in the Name of each slot:

If Instr(objController.Name, "Enhanced") Then

If InStr returns the value True (technically, if it returns a value greater than 0), that means the word Enhanced was found somewhere in the name. In that case we increment the value of i by 1:

i = i + 1

And what if InStr returns a 0, meaning that the word Enhanced could not be found? Well, in that case we simply loop around and try the next item in the collection, leaving the value of i as-is.

We continue along these lines, incrementing i each time we find a USB slot with the word Enhanced in its name. When we’re all done we simply echo back the value of i, which tells us the number of USB 2.0 ports on the computer.

It’s not necessarily a foolproof approach, but it seems to work. And besides, it’s not like you ever did anything for us, you know.

Oh, right; we forgot about that. Um, you like chocolate by any chance?


How Can I Determine Which USB Devices are Connected to a Computer?

Hey, Scripting Guy! How can I determine which USB devices are connected to a computer?

-- WM

Hey, WM. If you watch a lot of TV – um, not that the Scripting Guys do; we spend our spare time reading the works of Shakespeare, solving differential equations, and otherwise tending to our many intellectual pursuits. As we’ve been told, however, any time there’s a major crisis on a TV show one of the main characters grabs a bullhorn and tries to calm the crowd. “Stay calm!” he or she tells everyone. “There’s no reason to be alarmed. Everything’s going to be just fine.”

There are times when we Scripting Guys wish we had a bullhorn, and this is one of them. Can you determine which USB devices are connected to a computer? Yes, although you might start to panic a bit when you see how you have to do this. Stay calm. There’s no reason to be alarmed.

You’re right: we should have our own TV show, shouldn’t we?

The problem we have here is that there isn’t a Dynamic class devoted to USB devices: you can’t return a collection of USB devices the same way you can use Win32_Services to return a collection of all the services on a computer. Instead, you have to use a WMI Association class (Win32_USBControllerDevice) that associates a USB controller with a USB device. With that information you can identify the dependent entity in the association (which happens to be the device associated with a USB controller) and can then query the Win32_PNPEntity to get information about the device itself. (Unfortunately, Win32_USBControllerDevice returns nothing about the device except the somewhat-cryptic device ID.)

Confused? Don’t feel bad; we’re not 100% sure we understand it. So let’s make a deal: why don’t we show you the script, and then you can decide for yourself if you want to tackle the explanation of how the script works:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colDevices = objWMIService.ExecQuery _
    ("Select * From Win32_USBControllerDevice")

For Each objDevice in colDevices
    strDeviceName = objDevice.Dependent
    strQuotes = Chr(34)
    strDeviceName = Replace(strDeviceName, strQuotes, "")
    arrDeviceNames = Split(strDeviceName, "=")
    strDeviceName = arrDeviceNames(1)
    Set colUSBDevices = objWMIService.ExecQuery _
        ("Select * From Win32_PnPEntity Where DeviceID = '" & strDeviceName & "'")
    For Each objUSBDevice in colUSBDevices
        Wscript.Echo objUSBDevice.Description
    Next
Next

Oh, good: you’re still with us. As you can see, the script starts off by querying the Win32_USBControllerDevice; this returns a collection of all the USB controllers and the devices currently associated with them. That’s the good news. The bad news is this: we need to isolate the dependent entity for each item in the collection, an entity that represents a USB device. Why is that bad news? Well, it’s bad news because the Dependent property value is going to look something like this:

\\TOMSERVO\root\cimv2:Win32_PnPEntity.DeviceID="USB\\VID_045E&PID_0029\\5&236EE205&0&2"

Yuck. What we need to do is somehow isolate the actual device ID; in this case, USB\\VID_045E&PID_0029\\5&236EE205&0&2. When we have that value, we can then query the Win32_PNPEntity class for information about the device.

So how do we isolate the device ID? Well, there are probably a million different ways to do this; we chose one we thought was reasonably easy to follow. (Which ought to give you some indication of what the other methods were like.)

We begin by storing the value of the Dependent property in a variable named strDeviceName. We then use the VBScript Replace function to remove the double quotes – which we indicate by using Chr(34) – from the string. That leaves us with a value that looks like this:

\\TOMSERVO\root\cimv2:Win32_PnPEntity.DeviceID=USB\\VID_045E&PID_0029\\5&236EE205&0&2

Believe it or not, removing those double quotes puts us on the home stretch. If you examine the remaining value closely, you’ll notice that it’s basically divided into two parts, with the two parts separated by an equal sign. On the left of the equal sign we have the WMI path to the class and property; on the right of the equal sign we have the device ID. Why does that matter to us? Because now we can use the VBScript Split function to split the value on the equal sign, and create an array consisting of two items. Item 1 will be the WMI path, and item 2 will be – bingo! – the device ID. These two lines of code create the array, and then assign the value of item 2 in our array (the device ID) to the variable strDeviceName:

arrDeviceNames = Split(strDeviceName, "=")
strDeviceName = arrDeviceNames(1)

We now have the device ID for the first USB device connected to the computer. With the device ID in hand we can query the Win32_PNPEntity class for information about the USB device that’s been assigned that device ID. That’s what we do here:

Set colUSBDevices = objWMIService.ExecQuery _
    ("Select * From Win32_PnPEntity Where DeviceID = '" & strDeviceName & "'")

This query will return information about the actual device itself. In this script we do nothing more than echo back the value of the Description property for each device, although we could just as easily echo back values for any of the properties found in the Win32_PNPEntity class. We then loop around and repeat this process for each item in the Win32_USBControllerDevice collection.

When we run the script we’ll get back information similar to this (depending, of course, on the devices currently connected to the computer):

USB Root Hub
Microsoft USB IntelliMouse Web
Microsoft USB IntelliMouse Web
USB Mass Storage Device
Disk drive
Generic volume
USB Root Hub
USB Root Hub

It’s not perfect; USB Mass Storage Device, Disk drive, and Generic volume all refer to the same USB keychain drive. But at least it gives you some idea of what devices are (or are not) attached to a computer.

Just like we said: No reason to be alarmed. Everything is just fine.


How Can I Get a List of Installed Device Drivers?

Hey, Scripting Guy! How can I get a list of installed device drivers, their version number, and their date?

-- SH

Hey, SH. You know, as we’re writing this column the first round of the NCAA men’s basketball tournament is in full swing. But don’t worry: despite the fact that this year’s opening round games are all being broadcast live over the Internet you can rest assured that the Scripting Guys are hard at work, giving your question their undivided attention.

Oh, come on, ref: you’ve got a whistle, you might as well use it!

Sorry. The, uh, person across the hall seems to have their CD player turned up a bit loud. Say, could you keep it down a little? Some of us are trying to work here.

Can you believe the nerve of some people? Watching college basketball while at work; for shame.

Now, let’s see, we were talking about the 28 points Brandon Roy (the real player of the year this season) scored last night as the Washington Huskies defeated Utah State. And as Brandon noted in his post-game press conference, yes, SH, you can use a script to get a list of installed device drivers, their version number, and their date. In fact, you can use a script very much like this one to get back that information:

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

Set colItems = objWMIService.ExecQuery("Select * from Win32_PnPSignedDriver")

For Each objItem in colItems
    Wscript.Echo "Device ID: " & objItem.DeviceID
    Wscript.Echo "Device Name: " & objItem.DeviceName
    dtmWMIDate = objItem.DriverDate
    strReturn = WMIDateStringToDate(dtmWMIDate)
    Wscript.Echo "Driver Date: " & strReturn
    Wscript.Echo "Driver Version: " & objItem.DriverVersion
    Wscript.Echo "Is Signed: " & objItem.IsSigned
    Wscript.Echo
Next

Function WMIDateStringToDate(dtmWMIDate)
    If Not IsNull(dtmWMIDate) Then
        WMIDateStringToDate = CDate(Mid(dtmWMIDate, 5, 2) & "/" & _
            Mid(dtmWMIDate, 7, 2) & "/" & Left(dtmWMIDate, 4) _
                & " " & Mid (dtmWMIDate, 9, 2) & ":" & _
                    Mid(dtmWMIDate, 11, 2) & ":" & Mid(dtmWMIDate,13, 2))
    End If
End Function

Don’t let the code deter you here. This script looks a little frightening (especially towards the end), but that’s because of the goofy little function we have to use to convert WMI’s UTC date-time values (like 20010701000000.******+***) to somewhat more-readable date-time values (like 7/1/2001). We aren’t going to describe the workings of this function in any detail today; if you’d like to know more about what we’re doing here (and how we go about doing it) take a look at this section of the Microsoft Windows 2000 Scripting Guide. We should also note that this function is required if you’re running Windows 2000 or an earlier version of Windows. If you’re running Windows XP or Windows Server 2003, there’s a slightly easier way to convert UTC dates to real dates. We opted to use the goofy function because it works on any version of Windows.

Oh, my gosh! Come on, you have got to make that shot!

Um, we’ll close the door so that CD player won’t bother us anymore.

And, yes, that is a kind of unusual song he’s listening to. Must be some kind of rap/hip-hop thing.

Other than the goofy date-time converter the remainder of the script is pretty straightforward. We begin by connecting to the WMI service on the local computer; of course – and let’s all say it together – we could also run this script against a remote machine. (How? Just assign the name of the remote computer to the variable strComputer.) We then use this line of code to retrieve a collection of all the plug-and-play device drivers installed on the computer:

Set colItems = objWMIService.ExecQuery("Select * from Win32_PnPSignedDriver")

From there we simply set up a For Each loop to walk through the collection of device drivers, echoing back the values of properties such as DeviceName and DriverVersion. The one tricky part (which really isn’t all that tricky) occurs when we encounter the DriverDate property. If we just echoed back the DriverDate we’d get one of those ugly UTC date-time values. Therefore, instead of echoing back the value of DriverDate we do this:

dtmWMIDate = objItem.DriverDate
strReturn = WMIDateStringToDate(dtmWMIDate)
Wscript.Echo "Driver Date: " & strReturn

As you can see, we grab the value of DriverDate and store it in a variable named dtmWMIDate. We then call the function WMIDateStringToDate (cute name, huh?), passing the variable dtmWMIDate as the function parameter. The function converts the date for us and then stores the converted value in a variable named strReturn. And then it’s the value of the variable (rather than the actual property value) that we end up echoing back.

Whoa: he traveled on that play. Come on: doesn’t it look like he’s traveling every time he gets the ball?

By which we mean, gee, doesn’t it look like this is a very easy way to get back device driver information?

At any rate, we have to go now; it’s time to watch Arizona get beat by Wisconsin.

By which we mean, gee, it must be time to do even more work than we usually do ….


How Can I Tell if a Computer Has a DVD Drive Installed?

Hey, Scripting Guy! How can I tell if a computer has a DVD drive installed?

-- GC

Hey, GC. No doubt you’re hoping we’ll say something like, “Why, you just use the Win32_DVD class, of course.” Unfortunately, though, we can’t say that; that’s because there is no Win32_DVD class, or any class devoted to DVD drives. There is, of course, the Win32_CDROMDrive class, but it doesn’t have any properties like, say, DVDDrive. Looks bleak, doesn’t it?

But don’t worry, it – well, OK, in some cases it is bleak. But in other cases there is a workaround. If you have a DVD drive, the acronym DVD will often appear in the Caption property of the Win32_CDROMDrive class. For example, the Caption property might have a value similar to this:

QSI CDRW/DVD SBW242U

See the letters DVD? That indicates that this is a DVD drive. Like we said, not 100% foolproof, but most DVD drives will include the letters DVD in the Caption property.

You might also note the letters CDRW in the caption. As you might guess, those letters stand for CD Read/Write; turns out this particular drive not only can play DVDs, it can also read and write CDs. This is another question we get asked quite a bit: how can you tell whether or not a CD drive can write CDs? Checking for the letters RW within the Caption property is probably your best bet for that as well. Supposedly this can also be done by checking for the value 4 in the Capabilities property, but, to be honest, we have yet to see the value 4 returned from a drive capable of writing CDs. You’re better off using the Caption property.

Oh, yeah; we almost forgot. Here’s a sample script that returns the drive letter and caption for all the CD/DVD drives installed on a computer:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_CDROMDrive")
For Each objItem in colItems
    Wscript.Echo "Drive Letter: " & objItem.Drive
    Wscript.Echo "Caption: " & objItem.Caption
Next


How Can I Get a List of the Devices That Aren't Working?

Hey, Scripting Guy! In Device Manager, any time a device isn’t working that device is marked with a yellow exclamation mark. How can I use a script to get a list of the devices that aren’t working?

-- OL

Hey, OL. Sometimes in baseball a batter absolutely hammers one, but the ball takes a lucky bounce and lands right in your glove. That’s often referred to as a “candy hop.” Turns out that this question is the scripting equivalent of a candy hop: it looks like it might be a tough one to answer, but it turns out to be pretty easy.

WMI actually has a class – Win32_PNPEntity – that returns information about the devices found in Device Manager. Meanwhile, Win32_PNPEntity has a property – ConfigManagerErrorCode – that tells you whether or not the device is working properly. If the value of ConfigManagerErrorCode is 0, then the device appears to be working. If ConfigManagerErrorCode is anything but 0, then something must be wrong. And here’s the good part: any device with a ConfigManagerErrorCode value other than 0 will also have a little yellow exclamation mark next to it in Device Manager.

In other words, to write a script that returns a list of the devices that aren’t working properly we just need to connect to the Win32_PNPEntity class and query for all devices with a ConfigManagerErrrorCode other than 0:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_PNPEntity Where ConfigManagerErrorCode <> 0")

For Each objItem in colItems
    Wscript.Echo "Name: " & objItem.Name
Next

And what if you’d like more detailed information about all the devices installed on a computer? Well, because this question turned out to be an easy one to answer, we’re feeling generous: here’s a bonus script that returns all sorts of data about installed devices:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_PnPEntity")
For Each objItem in colItems
    Wscript.Echo "Class GUID: " & objItem.ClassGuid
    Wscript.Echo "Device is Working: " & objItem.ConfigManagerErrorCode
    Wscript.Echo "Description: " & objItem.Description
    Wscript.Echo "Device ID: " & objItem.DeviceID
    Wscript.Echo "Manufacturer: " & objItem.Manufacturer
    Wscript.Echo "Name: " & objItem.Name
    Wscript.Echo "PNP Device ID: " & objItem.PNPDeviceID
    Wscript.Echo "Service: " & objItem.Service
Next

No, no charge for the bonus script. That one is on us!


How Can I Tell Whether a Laptop Computer is Running Off Batteries?

Hey, Scripting Guy! How can I tell whether a laptop computer is running off batteries?

-- TJ

Hey, TJ. Ah, yes: batteries. Back in December the Scripting House lost power for six days. Was the Scripting Guy who writes this column concerned? Of course not. After all, he was fully prepared for an emergency like that: he had four (count ‘em: four) flashlights, each one fully-stocked with batteries. Granted, he had no way to heat the house, and after a day without power all the food (or at least the food in the refrigerator) had to be thrown out. But he did have four flashlights, fully-stocked with batteries.

About the only thing he wasn’t prepared for was the fact that batteries don’t maintain their charge forever. (Hey, who knew?) Within a day two of his four flashlights had already died; shortly thereafter the other two began to sputter and cough. At that point the Scripting Guy who writes this column sat down on the floor, disassembled all the flashlights, and started mixing and matching batteries in the hope of finding some combination that would enable at least one of the flashlights to continue working.

What’s that? Just walk down to the store and buy some new batteries? Actually the Scripting Family did walk down to the store one day, mainly because there wasn’t anything to do at home. Although most businesses were closed (they didn’t have power, either) one of the grocery stores was sort of open; as you entered the store they gave you a lantern so you could make your way through the darkened aisles, just in case someone overlooked a box of crackers or something. The Scripting Guy who writes this column wandered into the store just to see if they happened to have any batteries. Not only was every single battery of every single size long-since gone, but the battery area looked like a war zone: apparently people had gone into the store, torn the batteries from the package, and then (we assume) took them without even paying for them.

But who can blame them? After all, two days without a cappuccino will drive anyone insane.

At any rate, trying to find some combination of batteries that would make a flashlight work was an interesting exercise, an exercise very similar to your question, TJ: trying to determine whether a laptop computer is running off of batteries. As it turns out, there are millions of WMI classes in the cimv2 namespace that say they will return information about batteries; however, those classes proved to be a bit disappointing, to say the least. For example, on the Scripting Test Machine the Win32_PortableBattery class returned no information whatsoever. By contrast, the Win32_Battery class did return information … sort of. To note two instances, the BatteryStatus and Availability classes seemed to randomly return one of two values: Unknown or Other.

Note. What’s the difference between Unknown and Other? Good question. As near as we can tell, Unknown means, “We have no idea what the battery status is.” By contrast, Other apparently means, “We know what the battery status is. But we aren’t going to tell you.” Either way the information wasn’t particularly helpful.

It was at this point that the Scripting Guy who writes this column started poking through the root\wmi namespace. Poking through the root\wmi namespace is a lot like going to a second-hand store: most of what you find is of no use to you whatsoever. (The majority of classes stashed in root\wmi are valid classes, but they don’t return any data.) Every now and then, however, you stumble upon something a little more valuable. At a second-hand store this might be a long-lost copy of the Declaration of Independence; in the root\wmi namespace, this might be the BatteryStatus class:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From BatteryStatus Where Voltage > 0")

For Each objItem in colItems
    Wscript.Echo "Battery: " & objItem.InstanceName
    Wscript.Echo "On AC Power: " & objItem.PowerOnline
    Wscript.Echo "Battery is Discharging: " & objItem.Discharging
    Wscript.Echo "Battery is Charging: " & objItem.Charging
    Wscript.Echo "Remaining capacity: " & objItem.RemainingCapacity
Next

One caveat before we explain how this script works. As far as we know this particular class has never been documented; we searched everywhere for additional information on the BatteryStatus class and couldn’t find much of anything. However, as near as we can tell the script works just fine on laptops running either Windows XP or Windows Vista, so, with any luck, this will get you started, TJ.

Note. Does the script run on other versions of Windows? To tell you the truth, we don’t know; that’s because we don’t have any laptops running, say, Windows 2000 or Windows Server 2003.

As for the script itself, we begin by binding to the WMI service on the local computer. Fortunately this script also works remotely; after all, if you want to know if the local computer is running off batteries it’s probably quicker and easier just to see whether or not the power cord is plugged into the wall. So then how do you modify this script to run against a remote machine? That’s easy; just assign the name of the remote computer (e.g., atl-ws-01) to the variable strComputer:

strComputer = "atl-ws-01"

After making the connection (note that we bind to the root\wmi namespace) we then use this line of code to retrieve information about all the batteries with a Voltage greater than 0:

Set colItems = objWMIService.ExecQuery("Select * From BatteryStatus Where Voltage > 0")

Why did we add the stipulation about batteries with a voltage greater than 0? Well, to tell you the truth, we aren’t sure if that’s required or not. In our case the Scripting Test Machine has two separate battery bays; being a cheapskate, however, the Scripting Guy who writes this column only has one battery installed in the computer. Nevertheless the empty bay gets reported back as a battery, albeit a battery with a voltage of 0. That seemed silly to us, so we added the Where clause to filter out non-existent batteries.

Note. Yes, when trying to determine which flashlight batteries worked and which ones didn’t the Scripting Guy who writes this column should have done something similar, carefully setting aside batteries that he knew did not work. The one thing he shouldn’t have done is test a battery and then simply toss it back on the pile, mixing it in with the batteries that he hadn’t tested. So is this what he did? Well, you know, that was a long time ago, and it’s hard to remember what he did or didn’t do ….

After executing our query we then set up a For Each loop to report back information for each battery in the collection. We chose to echo back values for the following properties:

· InstanceName. Identifying information for the battery.

· PowerOnline. The key to this entire script. If PowerOnline is True that means that the AC adapter is plugged in and the computer is not running off the battery. If PowerOnline is False that means that the computer is running off the battery.

· Discharging. Indicates whether the battery is in use and therefore discharging.

· Charging. Indicates whether the battery is currently charging.

· RemainingCapacity. Lets you know the estimated battery life. We’re not 100% sure what this number means. We found some information that suggests the value is reported back as milliwatt-hours, but we don’t know that for sure; for that matter, we don’t even know what a millwatt-hour is. However, we do know this: on our test computer the Windows Power Meter would report an estimated battery life like 2 hours and 54 minutes (that is, 174 minutes). Meanwhile, the value of the RemainingCapacity property would come back as 43734. If we divided 43734 by 174 minutes, we got back a value of 252. For us 252 seemed to be the magic number: any time we divided RemainingCapacity by 252 we got back a value, in minutes, that matched up quite nicely with the estimated battery life reported back by the Power Meter. Again, we can’t guarantee that 252 is the magic number but, again, it gives you a place to start.

So what happens when we unplug the test machine (causing it to run off the battery) and then fire up the script? Why, we get back information similar to this:

Battery: ACPI\PNP0C0A\1_0
On AC Power: False
Battery is Discharging: True
Battery is Charging: False
Remaining capacity: 41769

And if we then plug the computer in and try the script again we get back information similar to this:

Battery: ACPI\PNP0C0A\1_0
On AC Power: True
Battery is Discharging: False
Battery is Charging: True
Remaining capacity: 42273

In other words, and unlike the typical Scripting Guy flashlight, the script appears to work just fine.

Speaking of flashlights, once the stores were reopened and the shelves restocked did the Scripting Guy who writes this column go out and replenish his supply of batteries? Let’s put it this way: remember the fable about the grasshopper and the ant, the story where the ant worked hard to store up food for the winter while the grasshopper spent his time singing and dancing? To be honest, we don’t know much about this particular fable; we were too busy singing and dancing to finish reading it. But we’re sure that, somehow, the grasshopper magically ended up with all the food and batteries he needed to get him through the winter.

At least we hope he did.


How Can I Add a Domain Group to the Local Administrators Group?

Hey, Scripting Guy! How can I add a domain group to the local Administrators group?

-- SS

Hey, SS. You know, people think of the Scripting Guys as striding powerfully through the world of system administration scripting, confident, almost-arrogant in the knowledge that they can meet any challenge thrown at them. We like to think of ourselves as being that way, too, even though we know it isn’t true. Confident, almost-arrogant in the knowledge that we can meet any challenge thrown at us? Well ….

Take this question for example. Awhile back we answered a similar question, explaining how a domain user could be added to the local Administrators group on a computer. We expected that, if anything, we'd get a few emails from people saying, “Thanks; that answered my question.” Instead, we got a flood of emails from people saying, “OK, sure. But how do you add a domain group to the local Administrators group?” In turn, that got us a little worried. Does that mean you have to use a different approach to add a domain group to the local Administrators group? Oh my gosh: how do you add a domain group to the local Administrators group?

Faced with a challenge thrown at us, we did what we usually do in that situation: we acted as though the problem never existed. In the past couple of weeks, however, we’ve gotten several more emails on the subject, and we decided that it was time to meet the challenge head-on.

And guess what? It turns out that to add a domain group to the local Administrators group you use the exact same approach you use when adding a domain user to the local Administrators group:

strComputer = "atl-ws-01"

Set objAdmins = GetObject("WinNT://" & strComputer & "/Administrators")
Set objGroup = GetObject("WinNT://fabrikam/accounting")

objAdmins.Add(objGroup.ADsPath)

This script begins by assigning the name of the local computer (in this case, atl-ws-01) to a variable named strComputer. We then use this line of code to bind to the local Administrators group on atl-ws-01:

Set objAdmins = GetObject("WinNT://" & strComputer & "/Administrators")

Now comes the one tricky part. We need to create a second object reference by binding to the domain group. When you bind to a group in Active Directory you typically use an ADsPath similar to this:

Set objGroup = GetObject("LDAP://cn=accounting, ou=finance, dc=fabrikam, dc=com")

However, we can’t use a binding string like that in this script. Why not? Well, because we're working with a local account (the local Administrators group), so we need to use the WinNT provider; for better or worse, the WinNT provider has no idea what something like cn=accounting, ou=finance, dc=fabrikam, dc=com means. Therefore, we have to use an old-fashioned, Windows NT 4.0 binding string, making a connection using the domain_name/logon_name format:

Set objGroup = GetObject("WinNT://fabrikam/accounting")

It’s something you’re not used to doing, but it works.

Note. OK, Scripting Guys, it looks like it's easier to use the WinNT provider to bind to Active Directory than to use the LDAP provider to bind to Active Directory; if that's the case, then why don't we always use the WinNT provider to bind to an Active Directory object? That's easy. If you bind to an Active Directory object using the LDAP provider you have access to all the Active Directory properties of that object; in the case of a user account, that's over 200 properties you can read and write. If you bind using the WinNT provider, however, you get access only to the Windows NT properties of that object; for a user account, that amounts to only 20 or so properties. To get to the Active Directory attributes of an object you have to use the LDAP provider. We can use the WinNT provider here because all we need is an object reference; we don’t need any of the properties associated with the account.

As soon as we have an object reference that the WinNT provider understands all we have to do is call the Add method to add the domain group to the local Administrators group:

objAdmins.Add(objGroup.ADsPath)

Just the way we always knew it would work.

Well, OK, just the well we always expected it would work.

OK, just the way we always hoped it would work.

Fine, just the way …. Well, the point is, it does work. And that's all that really matters.


How Can I Add a Domain User to a Local Administrators Group?

Hey, Scripting Guy! How can I add a domain user to the local Administrators group in a computer?

-- MB

Hey, MB. One reason we started doing this column was because we wanted to know more about what system administrators do (and script) on a regular basis. As it is, sitting here in our luxurious penthouse suites atop the Microsoft campus, we’re not always fully in tune with the way things are done in the real world. For example, in the Script Center we have a sample script that shows you how to add a user to the local Administrators account:

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
Set objUser = GetObject("WinNT://" & strComputer & "/kenmyer")
objGroup.Add(objUser.ADsPath)

So what’s wrong with this script? Nothing, except that this might not be the most practical example ever devised. After all, this script shows you how to add another local user to the Administrators group. That’s OK, but what most of you really want to know (as we can tell by the number of emails we’ve received to this effect) is how to add a domain user to the local Administrators group. Message received, loud and clear: Let’s show you how to add a domain user to the local Administrators group.

Incidentally, the script to do this is almost identical to the script for adding a local user to the Administrators group. The only difference, as we’ll see in a moment, occurs in line 3. In the preceding script, we bind to a local user account on a computer using this line of code:

Set objUser = GetObject("WinNT://" & strComputer & "/kenmyer")

We then pass the ADsPath of that user account to the Add method, which adds the user to the group:

objGroup.Add(objUser.ADsPath)

We want to do the same thing with our new script, only we don’t want to bind to a local user account, we want to bind to a domain user account. And so that’s what we’re going to do, substituting in a new line 3:

Set objUser = GetObject("WinNT://fabrikam/kenmyer")

Here we’re using the WinNT provider to bind to the fabrikam domain; more specifically, we’re using the WinNT provider to bind to the kenmyer user account in the fabrikam domain. Ah, we see some of you are upset by this. “Why are they using the WinNT provider?” you’re muttering. “Aren’t they supposed to use the LDAP provider when binding to Active Directory?”

The answer to that question is yes, most of the time. However, suppose we used the LDAP provider to retrieve the ADsPath for kenmyer, using code like this:

Set objUser = GetObject(“LDAP://CN=kenmyer,OU=Finance,dc=fabrikam,dc=com”)

That looks OK, except we get back an ADsPath that looks like this:

LDAP://CN=kenmyer,OU=Finance,dc=fabrikam,dc=com

That’s OK, too … at least until you try passing that value to the local computer. Remember, the Security Account Manager on the local computer speaks WinNT, it doesn’t speak LDAP. If you try passing an LDAP path to the local computer it just won’t work.

Instead, we need to pass an ADsPath that looks like this:

WinNT://fabrikam/kenmyer

And guess what? If we bind to the fabrikam domain using the WinNT provider, that’s exactly the kind of ADsPath we’ll get back. If you’re working strictly with Active Directory then you should use the LDAP provider. But if you’re going to grab an account out of Active Directory and use that account in a local computer group you’ll have to use the WinNT provider.

We know: all this talk of providers and ADsPaths and what-not is making your head spin. But don’t fret too much about that. Instead, just use this script to add a domain user (a user named kenmyer, in the fabrikam domain) to the local Administrators group on the computer atl-ws-01:

strComputer = "atl-ws-01"
Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
Set objUser = GetObject("WinNT://fabrikam/kenmyer")
objGroup.Add(objUser.ADsPath)

And keep those cards and letters coming in!


How Can I Delete Everyone Except the Administrator and the Domain Admins Group from the Local Administrators Group?

Hey, Scripting Guy! How can I delete everyone except the Administrator and the Domain Admins group from the local Administrators group?

-- JS

Hey, JS. You know, when you become a doctor you have to take the Hippocratic Oath, which famously begins, “First, do no harm.” When you become a Scripting Guy you have to take the Scriptocratic Oath, an oath which, somewhat less-famously, begins, “First, give them a warning. After that it’s not your fault if anything bad happens.” So, JS, consider yourself warned.

Actually, the script we’re about to show you isn’t particularly dangerous. However, it could be a bit of a nuisance. Per your request, the script removes everyone except the Administrator and Domain Admin accounts from the local Administrators group. That’s fine, except in at least one scenario. At Microsoft, for example, users are typically local administrators on their computers. However, those users never log on as the local Administrator; instead, they log on using their domain account, which happens to be a member of the local Administrators group.

So what’s the problem? Well, the script we’re about to show you will remove that domain user account from the local Administrators group; as a result, those users will no longer be local Administrators. That might very well be what you want to happen. But forewarned is forearmed and all that.

True story. Not too long ago, one of the Scripting Guys had to temporarily remove their computer from the domain. They did so, a process which also removed their domain user account – and the Domain Admins account – from the local Administrators group. Of course, you could still log on as Administrator … provided you knew the local Administrators password, that is. As you might have guessed, this Scripting Guy had no idea what the local Administrators password was. Uh-oh ….

And, no, we can’t tell you which Scripting Guy did this: Jean would be terribly embarrassed if anyone ever found out.

In other words, don’t run with scissors, don’t stick your finger in a light socket, and don’t use this script if it’s going to lock you out of your own machine:

strComputer = "atl-ws-01"

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")

For Each objUser In objGroup.Members
    If objUser.Name <> "Administrator" AND objUser.Name <> "Domain Admins" Then
        objGroup.Remove(objUser.AdsPath)
    End If
Next

Yes, it does look harmless, doesn’t it? And it is: after all, a domain Administrator can still access the machine and add someone back to the local Administrators group. It could create a nuisance for you, but nothing that can’t be fixed.

As for the script itself, it begins by connecting to the Administrators group on a specified computer; in this sample script, that’s a computer named atl-ws-01. After making the connection the script sets up a For Each loop to loop through all the members of the group; said membership can be accessed via the Members property.

Inside the loop we use this line of code to ensure that the name of our group member is neither Administrator nor Domain Admins:

If objUser.Name <> "Administrator" AND objUser.Name <> "Domain Admins" Then

Notice that we use the AND operator here: the Name is not Administrator and the Name is not Domain Admins. A common mistake scripters make is to use the OR operator in a script like this: the Name is not Administrator or the Name is not Domain Admins. Don’t make that mistake.

Why not? That’s easy: because then every member of the group will fit the criteria. Take the Administrator account, for example. Granted, the name of the account is equal to Administrator; that would seem to disqualify it. However, the name is not equal to Domain Admins; consequently it does meet the criteria; after all, you qualify if the name is not equal to Administrator or the name is not equal to Domain Admins. That’s why we make sure that the name is not equal to Administrator and it is not equal to Domain Admins.

If you don’t see how this works try running this script, which simply reports back the names of the group members:

strComputer = "atl-ws-01"

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")

For Each objUser In objGroup.Members
    If objUser.Name <> "Administrator" AND objUser.Name <> "Domain Admins" Then
        Wscript.Echo objUser.Name
    End If
Next

Now replace the AND with OR and see what happens.

See? Every now and then we actually do know what we’re talking about!

So what happens if an account meets the criteria; for example, the account kenmyer is not equal to Administrator and it is not equal to Domain Admins. In that case, we simply call the Remove method, passing it the AdsPath of the account in question; that removes the account from the group:

objGroup.Remove(objUser.AdsPath)

We then repeat the process with the other group members. When we’re done the local Administrators group should have only two members: Administrator and Domain Admins.

Like we said, make sure this is what you want before you use this script. But how about this: be careful when you use this script and, in return, we’ll let you run with scissors. Deal?


How Can I Determine the Name of the Local Administrators Group?

Hey, Scripting Guy! How can I determine the name of the local Administrators group? This can vary depending on the operating system language; for example, in German the name of the group is Administratoren.

-- DS

Hey, DS. You know, the first thought that popped into our heads when we read your question was this: just call Scripting Guy Peter Costantini. After all, Peter speaks every language known to man (plus several others not known to man). If anyone can identify the local Administrators group for you Peter can.

Unfortunately, though, there were two problems with that idea. For one, we can never guarantee which dimension Peter will be in; he has a bad habit of upsetting the laws of time and space. More important, even speaking scores of languages might not be of much help if the local Administrators group has been renamed. In a case like that, even on a German version of Windows the local Administrators group wouldn’t be named Administratoren. Instead it would be, well, whatever someone renamed it.

With that in mind, we decided to try a different approach: use a script. Here’s a script that will return the name of the local Administrators group on a Windows XP or Windows Server 2003 computer (we’ll show you a Windows 2000 version momentarily):

strComputer = "."

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

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_Group Where LocalAccount = TRUE And SID = 'S-1-5-32-544'")

For Each objAccount in colAccounts
    Wscript.Echo objAccount.Name
Next

As you can see, this is a fairly simple – and fairly straightforward – WMI script. It begins by connecting to the WMI service on the local computer; to connect to the WMI service on a remote machine just assign the name of that computer to the variable strComputer. For example, this line of code causes the script to connect to the remote computer atl-fs-01:

strComputer = "atl-fs-01"

Next we have this interesting little snippet of code:

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_Group Where LocalAccount = TRUE And SID = 'S-1-5-32-544'")

What we’re doing here is querying WMI for all the groups on the computer that meet the following criteria:

· The value of the LocalAccount property is True. This is an important part of the query, because it limits WMI to examining local accounts. Leave this out and WMI will examine each and every group in Active Directory.

· The value of the SID property is S-1-5-32-544. The SID (Security Identifier) is a unique number that the operating system uses to identify an account. That’s one reason why you can change the name of the local Administrators account without worrying that the local admins will now lose access to everything. For the most part, the operating system ignores the account name and uses the SID instead. And that’s a value that never changes.

So is there some significance to the value S-1-5-32-544? As a matter of fact, there is: this happens to be a “well-known SID,” and it always references the local Administrators group. In other words, if you can find a local account with the SID S-1-5-32-544, then you’ve found your local Administrators group. It’s that easy.

The rest of the script simply sets up a For Each loop and walks through the collection of local groups with the SID S-1-5-32-544, echoing back the name of each group. Because SIDs must be unique on a computer and because S-1-5-32-544 has to refer to the local Administrators group, this collection will consist of one, and only one, item: the local Administrators group. This should work just fine, regardless of the operating system language or the actual name of the local admins group.

The one drawback to this script, as we noted, is that it runs only on Windows XP and Windows Server 2003. But don’t worry: we’d never forget you Windows 2000 users. Here’s a revised script that runs on Windows 2000, Windows XP, Windows Server 2003, and even Windows NT 4.0:

strComputer = "atl-fs-01"

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

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_Group Where Domain = '" & strComputer & "' AND SID = 'S-1-5-32-544'")

For Each objAccount in colAccounts
    Wscript.Echo objAccount.Name
Next

The reason our original script won’t run on Windows 2000 is because, prior to Windows XP, the Win32_Group class did not support the property LocalAccount. Therefore, we’ve made two minor modifications to this script:

· We bind to the local computer by name (instead of using the dot, WMI shorthand for the local computer).

· In our query, we don’t look for groups where the LocalAccount is set to True; after all, the LocalAccount property isn’t available to us. Instead, we look for groups where the value of the Domain property is equal to the name of the local computer. As you might have guessed, those will be local accounts.

If you find yourself traveling forward or backwards in time, you might run into Peter; if so, he can probably help you determine the name of the local Administrators group. If you don’t do much time-traveling, however, then the scripts we showed you today should make for a reasonably good Plan B.


How Can I Remove a Group from the Local Administrators Group?

Hey, Scripting Guy! How can I remove a group from the local Administrators group?

-- SB

Hey, SB. One of the Scripting Guys has very vague memories of an old TV show called Branded, in which the hero (played by Chuck Connors) was accused of cowardice and then dishonorably discharged from the US Cavalry. About all the Scripting Guy remembers from the show is the opening, where they tear the insignias off the guy’s uniform, break his sword in half, and then make him march out of the fort in shame.

Why do we bring that up? No real reason; we just thought it would be cool if similar ceremonies were held any time a user or group was removed from the local Administrators group. Until such time, however, you can remove a group (in this case, an Active Directory group) from the local Administrators group by using a script similar to this one:

strComputer = "atl-fs-01"

Set objAdmins = GetObject("WinNT://" & strComputer & "/Administrators")
Set objGroup = GetObject("WinNT://fabrikam/finance")

objAdmins.Remove(objGroup.ADsPath)

Yes, it’s very simple, isn’t it? The script begins by assigning the name of the computer (in this case, atl-fs-01) to a variable named strComputer. We then use this line of code to bind to the local Administrators group on that computer:

Set objAdmins = GetObject("WinNT://" & strComputer & "/Administrators")

Once we have an object reference to the Administrators group, our next task is to create a second object reference, this one to the group to be removed. That’s what we do here:

Set objGroup = GetObject("WinNT://fabrikam/finance")

Notice that we’re using the old-fashioned, Windows NT-style naming convention when referencing the group account: fabrikam/finance. Why? That’s easy: to work with local users and groups, we have to use the WinNT provider. The WinNT provider doesn’t understand Active Directory lingo; it can’t make heads-or-tails out of an object path like this:

cn=Finance Users, ou=Finance, dc=fabrikam, dc=com

Therefore, we have to fall back to the old school account name: domain name/logon name. But that’s OK: fortunately, Active Directory understands this naming convention, too. When we request the account fabrikam/finance, Active Directory knows exactly what we’re talking about.

Note. This question has come up before, but it’s worth repeating: yes, you can access objects in Active Directory using the WinNT provider. And, yes, it’s true that the object path is much simpler. But don’t be tempted: use the WinNT provider only when you absolutely have to. Why? Well, for example, when it comes to user accounts the LDAP provider typically used when working with Active Directory supports over 200 properties; the WinNT provider supports only about 20 or so. The LDAP provider is much more powerful and much more useful.

Once we have an object reference to the group all we have to do is call the Remove method to remove that group from the local Administrators group:

objAdmins.Remove(objGroup.ADsPath)

We still think it’d be cooler to tear someone’s pocket protector off their shirt and then break their stapler over our knee, but this will work.

Of course, the group you want to remove might not be an Active Directory group, it might be a local group. Is that going to be a problem? No; in fact, it’s a tiny bit easier. Just bind directly to the group account on the local machine and have at it:

strComputer = "atl-fs-01"

Set objAdmins = GetObject("WinNT://" & strComputer & "/Administrators")
Set objGroup = GetObject("WinNT://finance")

objAdmins.Remove(objGroup.ADsPath)

Incidentally, the process used to remove a group from another group is the exact same process used to remove a user from a group: you bind to the target group (in this case, the local Administrators group), you bind to the object to be removed (either a group or a user, it doesn’t matter), and then you call the Remove method, passing as the sole parameter the ADsPath of the account to be removed.


How Can I Remove a Specific Set of Users From the Local Administrators Group?

Hey, Scripting Guy! How can I delete all the users whose name starts with GER/ad_ from the local Administrators group on a computer?

-- ID

Hey, ID. It’s a Friday as the Scripting Guy who writes this column writes this column. Usually Fridays are a very sad day for this Scripting Guy: after all, when Friday comes to an end he faces two days of having to stay home rather than experience the joy of coming in to work. But this Friday is different; this time around the Scripting Guy who writes this column is in a good mood. Why? Because as he drove in to work today he happened to glance down at the odometer and noticed that it was displaying nothing but 6s!

We understand: that sounds too incredible to be true. But true it is. And while the Scripting Guy who writes this column didn’t have a camera with him he did enlist the services of a police artist to recreate exactly what he saw on the odometer:

66666
  6.6

With the top line (66666) representing the total number of miles on the car and the bottom line (6.6) representing the number of miles driven on this particular trip.

That’s what he said, too: wow.

Note. Being the culturally-literate guy that he is, the Scripting Guy who writes this column is well aware of the so-called Mark of the Beast and the alleged meaning of the number 666. So do all these 6s, combined with the fact that the Scripting Guy who writes this column works for Microsoft (of all places!) mean that the Scripting Guy who writes this column is the Evil One? Don’t be silly. And you don’t have to take his word for it; let’s ask the Scripting Editor if she thinks the Scripting Guy who writes this column is the Evil One. What do you say, Scripting Editor? Hello? Scripting Editor? You there?

Well, she seems to have stepped out for a moment. But we all know what she would have said had she been here.

At any rate, seeing as how the Scripting Guy who writes this column is in a good mood today, let’s try to tackle the problem of deleting a specific set of users from the local Administrators group. And this should put you in a good mood, ID; turns out that this isn’t a very hard problem to solve after all:

strComputer = "atl-fs-01"

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")

For Each objUser In objGroup.Members
    If InStr(objUser.ADsPath, "WinNT://GER/ad_") Then
        objGroup.Remove(objUser.AdsPath)
    End If
Next

Note: If you’re running Windows Vista, this script will only work with elevated privileges. That means that if you run from the command prompt, you need to open the command window by right-clicking and selecting Run As Administrator.

As you can see, we start out by assigning the name of the target computer to a variable called strComputer; we then use this line of code to connect to the local Administrators group on that machine:

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")

The important thing to note here: we use the WinNT provider, written exactly in that fashion (W-i-n-N-T). Type that in any other way (e.g., winnt or WINNT) and the script will fail.

Before we go much further we should note that, if we were working with Active Directory, we could do some sort of search to locate all the “GER/ad_” users. For better or worse, however, we can’t do a search for local accounts. Instead, we need to bind directly to the Administrators group; in addition, we need to set up a For Each loop that loops through the items in the group’s Members property:

For Each objUser In objGroup.Members

As you might have guessed, the items in the Members property correspond quite nicely to the list of group members. That means that all we have to do now is figure out which users meet our criteria (GER/ad_) and which ones don’t.

Admittedly, that’s a little bit tricky, if for no reason other than the fact that people typically work with the Name property when dealing with local user accounts. That doesn’t help us much, because the Name property doesn’t include the domain (the GER/ in our example). Instead you get back names similar to this:

kmyer
packerman
jhaas

That’s nice, but there’s no way to tell whether these are truly local user accounts or if they are domain accounts. And if they are domain accounts, there’s no way of identifying which domain the account came from.

Because of that, we need to use the account’s ADsPath property rather than the account Name. ADsPath is going to return information similar to this, with GER representing the domain name:

WinNT://GER/kmyer
WinNT://GER/packerman
WinNT://GER/jhaas

Now our task is much easier: if an ADsPath starts with WinNT://GER/ad_ then this is a user we need to remove from the local Administrators group.

That explains our next line of code, a line that checks to see if the string value WinNT://GER/ad_ can be found anywhere in the user’s ADsPath:

If InStr(objUser.ADsPath, "WinNT://GER/ad_") Then

If that string value can be found we then call the Remove method (passing the ADsPath as the sole method parameter) in order to remove that user from the Administrators group:

objGroup.Remove(objUser.AdsPath)

If that string value can’t be found then we don’t do anything at all; instead we simply loop around and repeat the process with the next user in the collection. Where we’re all done we will have removed all the GER/ad_ users from the local Admins group.

Not as fancy as a search, but, ultimately, every bit as effective.

In case you’re wondering, as excited as the Scripting Guy who writes this column was to see nothing but 6s on his odometer, that was not the most exciting thing that has happened to him lately, not by a longshot. For example, far more exciting was the day he stopped at the grocery store and the bill came to $20.00 exactly. That was cool. And then there was his recent trip to Orlando. While his peers were out partying all night long he and Scripting Guy Jean Ross stayed in their hotel rooms and spent the night creating and publishing Script Center pages. Dance the night away at the Groove or the Glo Lounge, or stay up till 3:00 AM working? For the Scripting Guys, that was an easy decision.

Which goes a long ways towards explaining why something like all 6s on the odometer gets them so excited.


How Can I Change the Local Administrator Password on All My Computers?

Hey, Scripting Guy! How can I change the local Administrator password on all the computers in my domain?

-- AO

Hey, AO. You know, according to the now-classic cartoon punch-line, “on the Internet, no one knows if you’re a dog.” To be perfectly honest, the Scripting Guy who writes this column doesn’t care if anyone thinks he’s a dog or not; in fact, that’s a step or two up from the kind of animal he’s usually associated with. Instead, his only concern is whether or not anyone knows if he’s actually working or simply making it look like he’s working. And thanks to the Internet, and to a little foresight on his part, as far as everyone knows he’s been hard at work since the middle of last week. In reality, however, he spent all that time in the city of Kennewick, WA, soaking in the sunshine and watching the Scripting Son play in the “Bandit Bash” baseball tournament.

Note. Pop quiz: Try to guess which pitcher got the win in the game that gave the Scripting Son’s team third-place in the tournament.

Oh, well, you’re right: the Scripting Son was the winning pitcher, giving up just one earned run in 5 innings. We didn’t think anyone would be able to guess that.

Unfortunately for the Scripting Guy who writes this column, however, the party’s over: the tournament is complete (although a new one starts in just two days); he no longer gets to sit around and let the Scripting Mom do all the cooking; and – to add insult to injury – he’s back at work and faced with the chore of answering another question about system administration scripting. To be perfectly honest, that’s a chore that seems way too daunting after four days of sunshine, baseball, and free food. Could this be the end of Hey, Scripting Guy! the daily column?

Wait, don’t jump: this isn’t the end of Hey, Scripting Guy! the daily column after all. Admittedly, the Scripting Guy who writes this column found it hard to get motivated this morning. But then he remembered the words his Scripting Dad told him back on his very first day of kindergarten:

“Son, someday you’re going to write a daily scripting column and you’re going to be too burned out from baseball, sunshine, and free food to face up to that responsibility. When that time comes, remember this: Active Directory search scripts are a cinch to write, and people love them. When in doubt, toss together an Active Directory search script and call it good. Everyone will be happy, and you’ll only have to put in 10, maybe 15 minutes worth of work.”

Thanks, Scripting Dad:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = _
    "Select Name From 'LDAP://DC=fabrikam,DC=com' Where objectClass='computer'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

Do Until objRecordSet.EOF
    strComputer = objRecordSet.Fields("Name").Value

    Set objUser = GetObject("WinNT://" & strComputer & "/Administrator")
    objUser.SetPassword "x%tY7iu8%4f"

    objRecordSet.MoveNext
Loop

Of course, some of you might be wondering, “Why would the Scripting Dad tell his son that Active Directory search scripts are easy to write? This actually looks a little complicated to me.” Well, you’re right: this script is a little complicated. (Although most of it’s boilerplate code that can be used as-is in almost any Active Directory search script.) However, per some additional advice given by the Scripting Dad many years ago, the Scripting Guys never bother to explain the detailed workings of an Active Directory search script; instead, they simply refer people to the two-part Tales From the Script series Dude, Where’s My Printer? That’s what makes Active Directory search scripts so easy to write. At least for the Scripting Guys.

Note. Isn’t that cheating a little bit, to write a daily column that simply points people to an article that was posted several years ago? Sure it is. But that’s OK: on the Internet, no one knows that you’re cheating.

Well, unless you’re dumb enough to admit that you’re cheating. But we doubt that anyone is that dumb.

In other words, we won’t really talk about the search portion of this script; about all we’ll do is take a peek at the SQL query that returns a collection of all the computers in the fabrikam.com domain:

objCommand.CommandText = _
    "Select Name From 'LDAP://DC=fabrikam,DC=com' Where objectClass='computer'"

This is actually a pretty straightforward query: we’re simply asking for the Name of each and every object in the domain, provided that the object in question has an objectClass equal to computer. As you might expect, the only objects that have an objectClass equal to computer will be computer objects.

Just like the Scripting Dad said.

After executing the query we get back a recordset consisting of all the computer accounts in the domain. In order to change the local Administrator password on each of those machines our next step is to set up a Do Until loop that runs until we reach the end of the recordset (or, if you want to show off your knowledge of scripting, until the recordset’s EOF – end-of-file—property is True):

Do Until objRecordSet.EOF

Inside the loop, we grab the value of the Name field (the property we requested in our SQL query) and assign it to a variable named strComputer; that’s what we do in this line of code:

strComputer = objRecordSet.Fields("Name").Value

Once we have the name of the first computer safely stashed away in strComputer we can then use this line of code to bind to the local Administrator account on that machine:

Set objUser = GetObject("WinNT://" & strComputer & "/Administrator")

Again, pretty simple and pretty straightforward. Just make sure that you type the WinNT provider name exactly as shown; if you type WinNT in any other manner (e.g., WINNT or winnt) the script will fail. After making the connection to the local Administrator account all you have to do is call the SetPassword method and assign the account a new password:

objUser.SetPassword "x%tY7iu8%4f"

Note. Yes, we know: we shouldn’t use the Scripting Son’s middle name as our password. But remember, this is just for demonstration purposes.

That’s all we have to do. After changing the password on the first computer we call the MoveNext method and move on to the next record in the recordset; we then repeat the process with the second computer in our collection. This continues until we’ve hit each and every computer in the domain.

Well, OK, in theory we’ll hit each and every computer in the domain. In reality, of course, you’re bound to have a few computers that are shut down, offline, or otherwise unavailable. In that case you might want to make a couple of modifications to the script. First, you might want to log each computer name and whether or not you were able to connect to that machine. One easy way to do that is to write this information to a text file; if you don’t know how to write information to a text file, then take a look at this section of the Microsoft Windows 2000 Scripting Guide.

Second, you might want to ping each computer before attempting to bind to the local Administrator account; that will enable the script to run much faster, because it won’t waste time trying to connect to a computer that isn’t even online. So how can you ping a computer before trying to connect to it? Take a look at our Multiple/Remote Computer Scripting Templates for samples.

In case you’re wondering, the Scripting Guy who writes this column grew up in Kennewick, WA and everyone in his immediate family still lives there; that’s how he managed to score free room and board during the baseball tournament. Kennewick is a nice enough town, although it’s far less famous as a town than it is as home of Kennewick Man, the name given to the remains of a prehistoric man found on the banks of the Columbia River. Kennewick Man has been a figure of considerable controversy, with numerous Native American tribes claiming the 9,300-year-old man as one of their ancestors. To date, however, courts have rejected all of these claims, based in large part – or so we’ve been told – on testimony from Peter Costantini, the oldest living Scripting Guy. Peter claims to have graduated from high school with Kennewick Man, and insists that Kennewick Man was not Native American. We’ll have to take Peter’s word on that one.


How Can I Configure an Expiration Date for a Local User Account?

Hey, Scripting Guy! How can I configure an expiration date for a local user account?

-- GW

Hey, GW. You know, it’s amazing how quickly things change in the fast-paced world of system administration scripting. For example, in Monday’s column we announced that, as time allowed and whenever possible, the Scripting Guys would include a Windows PowerShell solution in Hey, Scripting Guy! along with the VBScript solutions we typically provide people. Today, just a few days later, we’re making a new announcement: the Scripting Guys aren’t going to provide Windows PowerShell solutions after all.

Whoa, whoa settle down; let us finish. It’s true that the Scripting Guys aren’t going to provide Windows PowerShell solutions; instead we have something much better in mind: June Blender, PowerShell writer to the stars, is going to provide those solutions for us. That’s better for all of us, and for at least two reasons. First, June actually knows what she’s doing. Second, if June is doing the work then the Scripting Guy who writes this column doesn’t have to do anything at all. That part we really like.

Note. Good question: is June Blender really the PowerShell writer to the stars? To be honest, she denies that. But rumor has it that any time Tom Hanks or Cate Blanchett need a PowerShell script they contact June. And no, to the best of our knowledge, both Meryl Streep and Brad Pitt still write their own PowerShell scripts.

Admittedly, there is one drawback to this new arrangement: the fact that the Scripting Guy who writes this column typically writes it the day before it’s due to appear in the Script Center. As a matter of fact, it’s often a race to see if he can finish the column before it’s time for him to go home. (What’s that? Stay a little later so he can finish the column and then go home? You don’t know much about the Scripting Guy who writes this column, do you?) That means that the Windows PowerShell solutions will usually be added after the fact: we’ll publish a column on, say, Thursday, then add the PowerShell stuff on Friday. We’ll see if we can come up with a better way of syncing that information, but we can’t make any promises. However, we will promise to do something somewhere that will help you locate the articles that include both a VBScript solution and a Windows PowerShell solution.

In fact, June has already started adding her PowerShell solutions. Don’t believe us? Then take a peek at this column. Oh, and at this column, too. And this one. Incidentally, that last column doesn’t have any Windows PowerShell stuff in it; we just thought it was a good column.

Needless to say, the average daily scripting columnist would break June in gently, probably by choosing a question that can easily be addressed using Windows PowerShell. But the Scripting Guy who writes this column considers himself anything but average; therefore, we’re going to start off by showing you how to set the account expiration date for a local user account:

Set objUser = GetObject("WinNT://atl-ws-01/kenmyer")

objUser.AccountExpirationDate = #03/01/2008#
objUser.SetInfo

Note to Windows Vista users: This script works only if you Run As Administrator. When you open the Command Prompt, right-click and select Run As Administrator, then run the script from that command prompt window.

As you can see, there’s hardly anything to this script, at least not the VBScript version. (What about the Windows PowerShell version? Beats us; after all, that’s not our problem.) We start off by binding to the kenmyer user account on the computer atl-ws-01. Once we’ve made the connection we then set the value of the AccountExpirationDate attribute to March 1, 2008:

objUser.AccountExpirationDate = #03/01/2008#

Why the little pound signs (#) around the date? That’s actually optional; we could have enclosed the date in double quote marks instead. However, in VBScript the pound signs specify that the value in question is a date-time value. It’s a good idea (dare we say a “best practice?”) to enclose your date-time values in pound signs; that helps guard against VBScript mistakenly viewing the value as a string, or even as a mathematical equation (e.g., 3 divided by 1 divided by 2008).

After assigning a value to the AccountExpirationDate attribute we then call the SetInfo method and officially write the change to the System Account Manager (SAM) on atl-ws-01. For those of you unfamiliar with the method, SetInfo is roughly equivalent to the Save command in an application such as Microsoft Word. You can type as much stuff into a Word document as you want, but if you never save the document then all that effort is for naught. The same is true when you use ADSI to modify attribute values. We can assign any value we want to AccountExpirationDate (or any other attribute, for that matter), but those changes do not take effect until we call SaveInfo. If the script ends without us calling SaveInfo then those changes will never take effect.

And sure, you can get a little fancy here. For example, suppose you want to give Ken Myer a temporary, 30-day account on atl-ws-01. Here’s a script that sets the expiration date to the current date plus 30 days:

Set objUser = GetObject("WinNT://atl-ws-01/kenmyer")

objUser.AccountExpirationDate = Date + 30
objUser.SetInfo

Well, we did say a little fancy. But you get the idea.

Of course, if you’re going to go around changing account expiration dates that means that, sooner or later, you’ll need to verify those expiration dates for all the local accounts on atl-ws-01. Good thing we had this verify-account-expiration-dates script lying around, isn’t it:

On Error Resume Next

Set objAccounts = GetObject("WinNT://atl-ws-01")
objAccounts.Filter = Array("User")

For Each objUser In objAccounts
   If IsNull(objUser.AccountExpirationDate) Then
       Wscript.Echo objUser.Name, "Account has no expiration date."
   Else
       Wscript.Echo objUser.Name, objUser.AccountExpirationDate
   End If
Next

All we’re doing here is connecting to the SAM on atl-ws-01, then applying a Filter to ensure that the only items we get back are user accounts. (Otherwise we’d get back groups and printers and services and everything else known to the SAM.) After applying the filter we set up a For Each loop to loop through the entire collection of user accounts; inside that loop we use the IsNull function to identify accounts that don’t have an expiration date:

If IsNull(objUser.AccountExpirationDate) Then

If AccountExpirationDate is null we echo back a message to that effect; otherwise we echo back the user name and the account expiration date:

Wscript.Echo objUser.Name, objUser.AccountExpirationDate

The end result is something that looks like this:

Administrator Account has no expiration date.
ASPNET Account has no expiration date.
Guest Account has no expiration date.
HelpAssistant Account has no expiration date.
kenmyer 3/1/2008
pilarackerman 6/1/2007

So much for the VBScript version. As for the Windows PowerShell version, well, we’ll let June take care of that.

You know, we just thought of something: if we could find someone to write the VBScript version for us, well ….


How Can I Determine if a User is a Local Administrator?

Hey, Scripting Guy! Some of the things we do in our logon scripts require the user to be a local administrator. How can the script tell if the user is a local admin or not?

-- GM, Denver, CO

Hey, GM. The best way to do that is to check the membership of the local Administrator’s group and see if the user is in there. To do that, you need to first determine the name of the computer that the script is running on. That’s required so you can use ADSI and the WinNT provider to “bind” to the local Administrators group and retrieve the list of group members.

After that, you must then determine the logon name of the logged-on user. As soon as you know that, you can loop through the Administrators group and see if the logged-on user is a member.

Listen, don’t panic; all that takes only a few lines of code:

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName
strUser = objNetwork.UserName

Set objGroup = GetObject("WinNT://" & strComputer & "/Administrators")
For Each objUser in objGroup.Members
    If objUser.Name = strUser Then
        Wscript.Echo strUser & " is a local administrator."
    End If
Next

So what’s happening in this script? Well, first we create an instance of the Wscript Network object; using that object, we can figure out the names of both the computer and the logged on user.

With those names firmly in hand, we then use the WinNT provider to bind to the Administrators group on the computer in question. After binding to the group we set up a For Each loop to walk through the group membership (the Members property is stored as an array, which is why we need to use For Each). For each member found, we check to see whether the member’s logon name (objUser.Name) is equal to the name of the logged-on user (a value we stored in the variable strUser).

And what if the member name and the logged-user name match? Well, that means that the logged-on user must be a local administrator; otherwise he or she wouldn’t be a member of the Administrators group. In this sample script, we merely echo the fact that the user is a local administrator; in your actual logon script, you could go ahead and carry out any tasks that require admin privileges.

For more information about using the WinNT provider, see the ADSI SDK on MSDN.


How Can I Determine if the Local Administrator Account has been Renamed on a Computer?

Hey, Scripting Guy! How can I determine if the local administrator account has been renamed on a computer?

-- KF

Hey, KF. You know, our first thought was to tell you to use ADSI and try to bind to the local administrator account on the computer; if that failed, that would mean that there was no account named Administrator on that computer. In turn, that would mean that the account had either been renamed or deleted.

But then we thought about this a little more. (Yes, having two thoughts in a single day is a Scripting Guys record.) Suppose the local administrator account had been renamed. In that case, you’d probably want to know the new name for the account. So what we really want to know is the name of the local administrator account. Period.

But how can we know the name of the account if the account has been renamed? (This is the scripting equivalent of, “If a tree falls in the forest and no one is there to hear it, does it still make a sound?”) Why, through the magic of the “well-known SID,” of course. A SID, as you probably know, is a “security identifier,” a unique identifier assigned to each account on a computer. The computer actually uses the SID to keep track of each account: if you rename the administrator account the computer still knows which account is the administrator account. That’s because the SID – unlike the name – never changes.

Now that sounds pretty good except for one thing: if the SID is a unique identifier then how can we determine which SID represents the administrator account? That’s where the “well-known” part comes in. On a computer the SID for a local administrator will always begin with S-1-5- and end with -500. (That’s why the administrator SID—and other SIDS, such as SIDs for the Guest account—are considered well-known.) For example, you might have a SID that looks like this:

S-1-5-21-1559272821-92556266-1055285598-500

As you can see, our SID starts with S-1-5- and ends with -500. If we can find a SID that fits that pattern, then we’ve found our local administrator account.

So can we find a SID that fits that pattern? Yes, by using a script like this:

strComputer = "."

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

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where LocalAccount = TRUE")

For Each objAccount in colAccounts
    If Left (objAccount.SID, 6) = "S-1-5-" and Right(objAccount.SID, 4) = "-500" Then
        Wscript.Echo objAccount.Name
    End If
Next

Note. This particular script works only on Windows XP and Windows Server 2003. In a minute we’ll show you a way to perform this same task on Windows 2000 Server or Windows NT Server 4.0.

We begin by connecting to the WMI service on a computer, then use this query to retrieve a list of local computer accounts:

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where LocalAccount = TRUE")

Note the use of the Where clause Where LocalAccount = TRUE. That ensures that we get back only local accounts; we aren’t interested in weeding through domain accounts. This is also the reason why this particular script won’t run on Windows 2000 or Windows NT 4.0; the LocalAccount property wasn’t introduced until Windows XP.

After getting back a collection of local user accounts we set up a For Each loop and walk through the collection. For each account we use this line of code to see if the account fits the well-known SID pattern:

If Left (objAccount.SID, 6) = "S-1-5-" and Right(objAccount.SID, 4) = "-500" Then

As you can see, we simply check to see if the first six characters in the string equal S-1-5- and if the last four characters equal -500. If they do then we’ve found the local administrator account and we echo the account name. If they don’t, then we loop around and check the next account in the collection.

That should do the trick, provided that you’re running the script on Windows XP or Windows Server 2003. If you’re running on Windows 2000 Server or Windows NT Server 4.0 you can use this variation. (This version will also work on Windows XP and Windows Server 2003.) In this script we begin by assigning the name of the computer to the variable strComputer; note, too that we need to use the actual computer name and not the dot (a WMI shortcut method for indicating the local machine). We have to use the computer name because we’ll use this variable to look for accounts with a “domain” that matches the computer name. (It’s confusing, but in a technical sense a computer is a domain, though obviously not on the scale of something like Active Directory.) Although a dot might represent the local computer name in WMI, it does not represent a domain name. Therefore, we must be very explicit when specifying the computer name.

And, yes, when dealing with a local computer account the domain name is equivalent to the computer name. If you have a user named kmyer and a computer named atl-ws-01, the domain name is atl-ws-01 and the user name is kmyer.

We then use a modified query to return a collection of accounts where the Domain is equal to the name of the local machine; that will eliminate Active Directory accounts and return only local accounts.

Here’s the script:

strComputer = "atl-ws-01"

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

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where Domain = '" & strComputer & "'")

For Each objAccount in colAccounts
    If Left (objAccount.SID, 6) = "S-1-5-" and Right(objAccount.SID, 4) = "-500" Then
        Wscript.Echo objAccount.Name
    End If
Next

Why are we so concerned with limiting the returned collection to local accounts? Well, without that clause the Win32_UserAccount class will try to return a list of all the user accounts in Active Directory as well as all the local user accounts. If you have an hour or two (or three) to kill, well, what the heck: go ahead and return – and weed through – several thousand user accounts if you want to. Otherwise you’re better off limiting the returned data to local accounts only.


How Can I Determine Whether or Not My Local Users are Required to Have a Password?

Hey, Scripting Guy! How can I determine whether or not my local users are required to have a password?

-- GT

Hey, GT. You know, rather than beat around the bush we decided to come right out and admit it: yes, the rumors that have been circulating around the Internet are true. One of the Scripting Guys, his brothers, his son, and various Scripting Nephews have been playing a strange, customized version of Wiffle Ball in the Scripting Parents’ front yard for more than 20 years. To say that this one-on-one game leans towards the defensive side of things would be an understatement: after all, it’s not unusual to have a game that goes 15 innings before anyone scores. The unofficial record for most runs scored in a game is 5 and, in 20 years of continual, oft-times cut-throat playing, no one has ever hit back-to-back home runs in a game.

Until now. Yes, it’s true: on a recent visit home one of the Scripting Guys (Greg is far too modest to admit that it was he) smacked back-to-back home runs in the fourth inning, and off the Scripting Son, no less. It was, without a doubt, the happiest day of this Scripting Guy’s life. And probably the greatest sporting achievement of the 21st century. (OK, sure, Barry Bonds hit 73 home runs in one season. But home runs are a dime-a-dozen in the big leagues. We’d like to see Barry hit 73 homers in the Scripting Parents’ front yard.)

Disclaimer. In all fairness, we should note that in the fifth inning one of the balls disappeared forever into the neighbor’s yard while another split in half; consequently, the game ended a bit prematurely. The Scripting Son claims that, because the game did not go five innings, the two home runs do not count. The Scripting Son is wrong.

Of course, by now you’re probably wondering, “What the heck does this have to do with determining whether or not local users are required to have a password?” Well, to tell you the truth, GT, absolutely nothing. It’s just that, for the Scripting Guy in question, the high points in his life continue to get fewer and farther between. He has to take a moment to revel in whatever glory he can, whenever he can.

OK; that’s enough reveling. Here’s a script that can determine whether or not local users are required to have a password:

Const ADS_UF_PASSWD_NOTREQD = &H0020

Set colUsers = GetObject("WinNT://atl-fs-01")
colUsers.Filter = Array("User")

For Each objUser in colUsers
   If objUser.UserFlags AND ADS_UF_PASSWD_NOTREQD Then
        Wscript.Echo objUser.Name & ": Password not required."
    Else
        Wscript.Echo objUser.Name & ": Password required."
    End If
Next

You’re right: it’s nowhere near as exciting as back-to-back home runs, is it? (But, then again, not everyone can hit back-to-back homers, if you know what we mean.) However, while the script might not be very exciting, it is kind of useful. With that in mind, let’s sit down and see if we can figure out how it works.

As you can see, we start out by defining a constant named ADS_UF_PASSWD_NOTREQD and setting the value to &H0020; we’ll explain why we do that in just a moment. After that we use this line of code to connect to the System Account Management (SAM) database on the computer atl-fs-01:

Set colUsers = GetObject("WinNT://atl-fs-01")

By default, any time we connect to the SAM database we get back information for all sorts of things, including local users, local groups, installed services, installed printers, etc. Because we’re only interested in user accounts we then use the Filter property to filter out everything in the returned collection except those self-same user accounts:

colUsers.Filter = Array("User")

Got all that? Good. Now that we have a collection of all the user accounts (and only the user accounts) found on the computer atl-fs-01, our next step is to examine each of those accounts and determine whether or not a password is required. That’s what we do here:

If objUser.UserFlags AND ADS_UF_PASSWD_NOTREQD Then

Yes, that is kind of an odd-looking line of code, isn’t it? As it turns out, information about whether or not a user requires a password is stored in the UserFlags attribute. UserFlags is a “bitmask” attribute; that means it’s an attribute that actually contains information about a number of properties, including such things as whether or not a user password expires, whether or not the account itself expires, and whether the user is allowed to change his or her password. For a complete list of properties stored in the UserFlags attribute see the topic ADS_USER_FLAG_ENUM in the ADSI SDK.

Oh, what the heck: here are some of the other more useful properties (as well as their constants and values) that can be managed using UserFlags:

Property | Constant | Value
Logon script will be executed | ADS_UF_SCRIPT | &H0001
Account is disabled | ADS_UF_ACCOUNTDISABLE | &H0002
Account requires a home directory | ADS_UF_HOMEDIR_REQUIRED | &H0008
Account is locked out | ADS_UF_LOCKOUT | &H0010
Account does not require a password | ADS_UF_PASSWD_NOTREQD | &H0020
User cannot change password | ADS_UF_PASSWD_CANT_CHANGE | &H0040
Encrypted text password allowed | ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED | &H0080
Account password never expires | ADS_UF_DONT_EXPIRE_PASSWD | &H10000
Smartcard required for logon | ADS_UF_SMARTCARD_REQUIRED | &H40000
Password has expired | ADS_UF_PASSWORD_EXPIRED | &H800000

Explaining how bitmask values work lies a bit outside the scope of this column; for more details, see this Scripting Guys webcast. For now, we’ll simply note that a bitmask is like a control panel with a series of switches, switches that can either be on or off. When we execute a line of code like If objUser.UserFlags AND ADS_UF_PASSWD_NOTREQD Then what we’re really doing is checking to see if the switch labeled ADS_UF_PASSWD_NOTREQD (the switch that tells us whether or not the user is required to have a password) is on. If it is, that means that user does not have to have a password; in turn, we then echo back the user Name and the fact that no password is required:

script.Echo objUser.Name & ": Password not required.

Note. Be careful not to get mixed up here. Remember, the attribute is Password is not required, not Password is required. If the switch is on the user doesn’t have to have a password. (Because it’s true that no password is required.)

If ADS_UF_PASSWD_NOTREQD is off that means a password is required (again, that seems backwards, but think about it for a second). Thus we echo back the fact that this user requires a password:

Wscript.Echo objUser.Name & ": Password required.

When all is said and done we should get back information similar to this:

Administrator: Password required.
ASPNET: Password not required.
Ken Myer: Password required.
pilarackerman: Password required.
Guest: Password not required.
HelpAssistant: Password required.
Test: Password required.

Very nice.

Admittedly, working with bitmasks can be a bit confusing, especially when it comes time to modify these values. However, simply reading the values isn’t too bad: you just need to use the proper constant and value. For example, would you like to know whether or not a user’s password will ever expire? Well, to do that, define a constant named ADS_UF_DONT_EXPIRE_PASSWD, set the value to &H10000, and modify the output section of the script accordingly:

Const ADS_UF_DONT_EXPIRE_PASSWD = &H10000

Set colUsers = GetObject("WinNT://atl-fs-01")
colUsers.Filter = Array("User")

For Each objUser in colUsers
   If objUser.UserFlags AND ADS_UF_DONT_EXPIRE_PASSWD Then
        Wscript.Echo objUser.Name & ": Password does not expire."
    Else
        Wscript.Echo objUser.Name & ": Password expires."
    End If
Next

See? That’s not so bad.

And now for something really cool: after hitting back-to-back home runs, on the very next pitch the Scripting Guy lined a double off the birdbath. And then he followed that by - wait, we’re not done yet; we still have lots more to tell you. In fact, we … guys? Hello? Guys?

Hmmm; the network must have gone down again.


How Can I Disable the Guest Account on a Computer?

Hey, Scripting Guy! How can I determine whether or not the Guest account is enabled on a computer and, if it is, disable it?

-- PR

Hey, PR. At last: a question for which there is a very simple, very straightforward answer. This isn’t to minimize the importance of the task: in general, it’s highly recommended that you disable the Guest account on your computers. It’s just nice to have a question where we don’t have to come up with some crazy workaround.

Let’s start with determining whether or not the Guest account is enabled on a computer. To do that, we simply need to bind to the Guest account and then check the value of the AccountDisabled property. How hard is that? Well, it’s no harder than this:

strComputer = "atl-ws-01"
Set objUser = GetObject("WinNT://" & strComputer & "/Guest")

Wscript.Echo "Guest account disabled: " & objUser.AccountDisabled

That’s right, just three lines of code. We assign the name of the computer (atl-ws-01) to the variable strComputer, then use ADSI and the WinNT provider to bind to the Guest account on that computer. (Note: With the WinNT provider, case is important; don’t type something like winnt … unless, of course, you don’t want a script that actually works). Finally, we echo back the value of the AccountDisabled property. If the Guest account is disabled, then AccountDisabled will be True; if the account is enabled, then AccountDisabled will be False.

See why we liked this question so much?

Of course, you might be thinking, “Well, sure, determining whether the Guest account is enabled is easy, but what about disabling that account? How hard is that going to be?” Well, we have to be honest with you: this is definitely harder. After all, it takes four lines of code rather than just three:

strComputer = "atl-ws-01"
Set objUser = GetObject("WinNT://" & strComputer & "/Guest")

objUser.AccountDisabled = True
objUser.SetInfo

As you can see, we bind to the Guest account and set the value of AccountDisabled to True. We then call the SetInfo method to actually write this change to the account, disabling it.

That’s it. If you want to first check the value of the account and then disable it, you can simply combine the scripts, like so:

strComputer = "atl-ws-01"
Set objUser = GetObject("WinNT://" & strComputer & "/Guest")

Wscript.Echo "Guest account disabled: " & objUser.AccountDisabled

objUser.AccountDisabled = True
objUser.SetInfo

Note that no error is generated if you try to disable an account that’s already disabled. Therefore, we simply echo the current account status and then go ahead and disable it. If the account is already disabled, hey, no problem; the script will just gracefully terminate.


How Can I Distinguish Between Local Users and Domain Users?

Hey, Scripting Guy! When using a script to return members of the local Administrators account, how can I distinguish between local users and domain users?

-- KS

Hey, KS. You know, many years ago there was a TV game show called What’s My Line? The premise of the show was that some person with an unusual occupation would come out on stage and a panel of celebrities would quiz this person, asking a bunch of Yes-No questions and trying to determine what his or her unusual occupation was. (Hey, all we said was that it was a game show; we never said it was an exciting and fun-filled game show.) If a panelist asked the right questions, then he or she might be able to determine the person’s occupation. If they asked the wrong questions, though ….

We have the same sort of situation here. (Hey, who said, “Beginning with the fact that this column is neither exciting nor fun-filled?” What could possibly be more exciting and fun-filled than distinguishing between local user accounts and domain user accounts?) Suppose we have a script that returns the members of the local Administrators group on a computer named atl-fs-01. That script might look something like this:

strComputer = "atl-fs-01"

Set colGroups = GetObject("WinNT://" & strComputer & "/Administrators")

For Each objUser In colGroups.Members
    Wscript.Echo objUser.Name
Next

When we run the script we’ll get back information similar to this:

Administrator
Domain Admins
kenmyer
InfoSec Secure Environment
pilarackerman
jonathanhaas

That’s great but, as you pointed out, KS, we have no way of knowing whether kenmyer (to pick one) is a local user or a domain user. As celebrity panelists (the Scripting Guys are about as famous as most of the “celebrities” who appeared on What’s My Line?), our job is to figure out what question we can ask that will tell us whether these are local users or domain users. That’s not an easy task, either, because we need to use the WinNT provider to get information about these users; as we all know, the WinNT provider (unlike its Active Directory counterpart) can tell us only a few things about a given account on a local computer.

Fortunately, though, one of the questions we can ask of the WinNT provider is this: “What is the ADsPath for the user?” (OK, so it’s not a Yes-No question. But we’re not really playing What’s My Line? here. That was just an analogy.) The ADsPath for a user account is roughly equivalent to a UNC path for a file. For example, suppose we have a file with the UNC path \\atl-fs-01\public\kenmyer\test.doc. The UNC path tells us exactly where the file Test.doc can be found: it’s on the computer atl-fs-01, on the shared folder Public, in the KenMyer folder.

Likewise, suppose we have an ADsPath like this: WinNT://FABRIKAM/atl-fs-01/Administrator. That tells us that we have a local account named Administrator which can be found on the computer atl-fs-01, which is a member of the Fabrikam domain. In other words, local accounts will always feature the name of the computer as part of the ADsPath; domain accounts will not. For example, here’s what we get back when we echo back the ADsPath for the members of the Administrators group:

WinNT://FABRIKAM/atl-fs-01/Administrator
WinNT://FABRIKAM/Domain Admins
WinNT://FABRIKAM/kenmyerp
WinNT://FABRIKAM/InfoSec Secure Environment
WinNT://FABRIKAM/atl-fs-01/pilarackerman
WinNT://FABRIKAM/jonathanhaas

There you have it. If you look closely, you’ll see that the string /atl-fs-01/ appears in two of the paths: the one for Administrator and the one for PilarAckerman. Consequently, those two must be local accounts, and the other accounts must be domain accounts.

But, needless to say, we don’t want to look closely, do we? Instead, we want the script to just tell us which accounts are local users and which accounts are domain users. The following script should do just that:

strComputer = "atl-fs-01"
strTestString = "/" & strComputer & "/"

Set colGroups = GetObject("WinNT://" & strComputer & "/Administrators")

For Each objUser In colGroups.Members
    If InStr(objUser.AdsPath, strTestString) Then
        Wscript.Echo "Local user: " & objUser.Name
    Else
        Wscript.Echo "Domain user: " & objUser.Name
    End If
Next

As you can see, this script starts out by assigning the name of the computer to a variable named strComputer. We then take the name of that computer (atl-fs-01) and add a / to the beginning and the end (/atl-fs-01/). This value is assigned to a variable named strTestString. Why do we need to add the / to the beginning and the end? To be honest, it’s more a safeguard than anything else. It’s unlikely you would, say, have a domain named atl-fs-01999, but putting /’s before and after the computer name helps ensures we don’t inadvertently identify a domain account as a local account.

After setting up our variables we then use this line of code to bind to the local Administrators account on the computer atl-fs-01:

Set colGroups = GetObject("WinNT://" & strComputer & "/Administrators")

What we want to do now is loop through the value of the Members property, a multi-valued attribute that lists each of the members of the Administrators group. For each user in the group we use this line of code to determine whether our test string (/atl-fs-01/) can be found anywhere within the user’s ADsPath:

If InStr(objUser.AdsPath, strTestString) Then

If the string is found, we assume that this is a local user and echo back that fact; if the string is not found then we assume that this is a domain user, and we echo back that fact. When all is said and done, we get back a nice little report that looks like this:

Local user: Administrator
Domain user: Domain Admins
Domain user: kenmyerp
Domain user: InfoSec Secure Environment
Local user: pilarackerman
Domain user: jonathanhaas

It’s not the fanciest output in the world, but it tells you what you need to know.

So there you have it: the scripting version of What’s My Line? Next time out we’ll do a scripting version of Fear Factor. With that in mind, you might want to brush up on your spider eating and your banana slug daiquiri drinking. And don’t forget to spend some time sitting in a bathtub filled with toads and alligators (although that’s something every scripter should be doing anyway).


How Can I Get a List of All the Local Users Who Have an Expiring Password?

Hey, Scripting Guy! How can I get a list of all the local users who have an expiring password?

-- JM

Hey, JM. Thanks for the question. Before we answer it, however, we have a question for you: you don’t by any chance have any spare dice lying around, do you? That’s right, dice, the kind you use when playing board games and craps and stuff like that. As it turns out, the Scripting Guys have an urgent need for dice; in fact, we could use 1,000 individual die ASAP. Unfortunately, we’re having a little bit of trouble locating that many dice at a price we can afford.

Mainly because if something actually has a price, we can’t afford it.

Why do the Scripting Guys need so many dice? Oh, no reason, really. After all, we are technical writers at Microsoft, and 1,000 dice is pretty much standard equipment for a technical writer. It’s hard enough being a Scripting Guy as it is. But just try being a Scripting Guy who doesn’t have any dice.

See? What did we tell you: you really can’t be a Scripting Guy without plenty of dice, can you?

Anyway, JM, if you happen to have any dice you aren’t using we’d be happy to take them off your hands. And we’d like to extend that offer to anyone else who might be reading this column. Got boxes and boxes of dice filling up your attic or your basement? Then send them to this address:

The Microsoft Scripting Guys
Microsoft Corporation
Building 42/4039
One Microsoft Way
Redmond, WA 98052

We’ll be happy to take them, in all shapes, sizes and colors.

Except maybe the fuzzy ones.

Of course, the Scripting Guys would never be so rude as to ask for dice without offering a little something in return. Let’s see what we got here … hey, how about a script that lists all the local users who have an expiring password:

Const ADS_UF_DONT_EXPIRE_PASSWD = &H10000

strComputer = "atl-ws-01"

Set colAccounts = GetObject("WinNT://" & strComputer & "")

colAccounts.Filter = Array("user")

For Each objUser In colAccounts
    If Not objUser.UserFlags AND ADS_UF_DONT_EXPIRE_PASSWD Then
        Wscript.Echo objUser.Name
    End If
Next

OK, let’s see if we can figure out how this baby works. As you can see, we start out by defining a constant named ADS_UF_DONT_EXPIRE_PASSWD (a delightful name, don’t you think?) and setting the value to &H10000. And you’re right, this constant will help us determine whether or not a user has a password that expires. We next assign the name of the computer (the computer where the local accounts are to be found) to a variable named strComputer, then use this line of code to bind to the computer’s System Account Manager (SAM):

Set colAccounts = GetObject("WinNT://" & strComputer & "")

By default, binding to the SAM in this fashion returns information about all the local user accounts on the computer; it also returns information about all the local groups on that computer, all the services installed on that computer, all the printers installed on that computer, etc. The truth is, we don’t care about those other things; all we care about are user accounts. (As his colleagues will attest, the Scripting Guy who writes this column is definitely a people person.) Therefore, our next step is to apply a Filter that limits the returned data to user accounts:

colAccounts.Filter = Array("user")

You know that’s a good question: why did we return a collection of all the user accounts? Wouldn’t it have been faster and easier to write a SQL query that searches the local computer and returns a collection consisting of only those users who have an expiring password? To tell you the truth, yes, that would have been much faster and much easier. Unfortunately, though, there’s a bit of a problem with that approach: it won’t work. Unlike Active Directory, we can’t run SQL queries against the SAM. Consequently, any time we need to work with local accounts we need to do things the old-fashioned way: return a collection of all the accounts, and then individually check each one to see whether or not that user has a password that expires.

Which, coincidentally enough, brings us to the next part of our script. After setting up a For Each loop to walk through each account in the collection we then execute this line of code:

If Not objUser.UserFlags AND ADS_UF_DONT_EXPIRE_PASSWD Then

Believe it or not, what we’re doing here is checking to see whether the user’s password ever expires. Password expiration information happens to be stored in a bitmasked attribute named UserFlags. If you aren’t familiar with bitmasked attributes, this type of attribute is a single property that can hold multiple values. (For a complete list of the values stored in UserFlags, see the WinNT User Object documentation on MSDN.) How can a single property contain multiple values? Well, without going into too much detail, a bitmask consists of a series of “switches,” each with a unique value (in this case, the switch for expiring passwords has a value of &H10000). If a given switch is on, that means the property is enabled; if the switch is off, the property is disabled. The syntax for checking the value of a switch is a bit clumsy, but this portion of the code tells us whether or not the user has a non-expiring password (that is, it tells us whether the ADS_UF_DONT_EXPIRE_PASSWD switch is on):

objUser.UserFlags AND ADS_UF_DONT_EXPIRE_PASSWD

Good point: we aren’t interested in users with non-expiring passwords, are we? That’s why we also used the equally-clumsy syntax If Not:

If Not objUser.UserFlags AND ADS_UF_DONT_EXPIRE_PASSWD Then

This is simply VBScript’s way of saying, “If the user doesn’t have a non-expiring password then run the next block of code.”

Note. So then why didn’t VBScript just say that rather than making us use a crazy coding convention like If Not? We don’t know. Chalk that up as one of life’s great unanswered questions, alongside “What is the meaning of life?” and “Why doesn’t Microsoft just give the Scripting Guys dice any time they say they need dice?”

If it turns out that the user does, indeed, have a password that expires we then use this line of code to echo back the user name:

Wscript.Echo objUser.Name

And then it’s back to the top of the loop where we repeat the process with the next item in the collection.

Didn’t someone already ask that question, the one about why we need 1,000 dice? Well, to tell you the truth, we don’t; we actually need 2,000 dice. However, we decided to start with 1,000 and then go from there.

Other than that, well, we can’t really tell you anything; that would spoil the surprise. (Besides, there’s a good chance we’ll never actually implement what we have in mind anyway.) So, in other words, you’ll just have to trust that the Scripting Guys will put those dice to good use. But that’s no big deal; after all, have the Scripting Guys ever let you down before?

Oh, right; we forgot about that. But this time we won’t let Peter anywhere near the project. Promise!


How Can I Get the Full Name and Description For My Local User Accounts?

Hey, Scripting Guy! How can I get the full name and description for each of my local user accounts?

-- EB

Hey, EB. You know, this might be hard to believe, but many people view the Scripting Guys with suspicion. OK, good point; the part that is hard to believe is why they view the Scripting Guys with suspicion. As it turns out, many people look at the scripts in this column, and the scripts scattered throughout the Script Center, and think, “Those are too easy; there’s no way scripting can be that easy. These guys must be hiding something, they must be keeping all the good stuff for themselves.” It’s the Priory of Scion all over again!

Note. Of course, you might want to check here for opposing viewpoints, at least when it comes to the Priory of Scion.

The truth is, the Scripting Guys have no top-secret scripting knowledge. (Or, if we do, then we’ve somehow managed to keep this knowledge secret even from ourselves.) However, we understand why people sometimes feel this way. After all, scripting has a reputation as being really hard; there are plenty of people here at Microsoft who say things like, “Nobody writes scripts; nobody can write scripts.” When we Scripting Guys turn around and provide an easy solution to a problem, many people think, “Oh, sure, that was easy. A little too easy.”

So why are we rambling on like this? Well, EB wanted to know how to get the full name and description for all the local user accounts on a computer. We’ll tell everyone how to do that, but we felt we should warn you in advance: as you’re about to see, the solution is incredibly easy. We hope that that will be OK with you.

Here’s the script:

strComputer = "atl-fs-01"

Set colAccounts = GetObject("WinNT://" & strComputer & "")
colAccounts.Filter = Array("user")

For Each objUser In colAccounts
    Wscript.Echo objUser.Name
    Wscript.Echo objUser.FullName
    Wscript.Echo Description
    Wscript.Echo
Next

We warned you that this was going to be easy. As you can see, we assign the name of the computer in question (in this case, atl-fs-01) to a variable named strComputer. We then use the WinNT provider and the following line of code to bind to the System Account Manager (SAM) on that machine:

Set colAccounts = GetObject("WinNT://" & strComputer & "")

As you probably know, you can use the WinNT provider to do more than just get information about local user accounts: the WinNT provider can also retrieve information about local group accounts, installed services, installed printers, etc. Because we care about only user accounts, we next create a Filter that, well, filters the returned data, allowing only user account information to come back:

colAccounts.Filter = Array("user")

And sure, we can filter for other things. Want information about local groups, and only local groups? Here you go:

colAccounts.Filter = Array("group")

Here’s a variation that returns information about both users and groups (but nothing else):

colAccounts.Filter = Array("user", "group")

Etc.

Incidentally, we tried to find a way to make filtering harder, but we couldn’t. We guess that some things in life really are easy. (The only remotely tricky part here? Making sure you put everything into an array, even if you’re filtering for only one type of object.)

All that’s left now is to set up a For Each loop that loops through the collection of user accounts. For each account in the collection we echo back the values of the Name, FullName, and Description attributes:

For Each objUser In colAccounts
    Wscript.Echo objUser.Name
    Wscript.Echo objUser.FullName
    Wscript.Echo Description
    Wscript.Echo
Next

If you set aside your entire day assuming it would take you several hours to figure out how this script works, well, the Scripting Guys apologize for any inconvenience the simplicity of this code might have caused. Hopefully you’ll be able to find something to fill all that spare time.

Oh, wait: we know something that’s bound to be hard. If you’re anything like the Scripting Guys then there’s a good chance that many of your local accounts have never been assigned full names or descriptions. That’s the answer right there; after all, writing a script to modify the values of local user account properties has to be hard, right?

Never mind; that was easy, too:

strComputer = "atl-fs-01"

Set objUser = GetObject("WinNT://" & strComputer & "/Test")

objUser.FullName = "Test User Account"
objUser.Description = "This account should used for test purposes only."
objUser.SetInfo

Turns out that all we need to do here is bind directly to the user account we want to modify; in this case, that’s an account named Test on the computer atl-fs-01. We then assign values to the FullName and Description attributes:

objUser.FullName = "Test User Account"
objUser.Description = "This account should used for test purposes only."

And then, to actually save those changes, we call the SetInfo method:

objUser.SetInfo

And that’s it. All in all, this script might have been even easier than the first script we showed you. Dang!

Oh, well; we give up. We’re sorry, everyone, but it looks like scripting is nowhere near as hard as it’s cracked up to be. (We tried our best to make it difficult, but we couldn’t do it.) Apparently that’s something we’ll all just have to learn to live with.


How Can I Get the Name of the Last User to Log on to a Computer?

Hey, Scripting Guy! How can I get the name of the last user to log on to a computer?

-- SV

Hey, SV. You know, the Scripting Guy who writes this column never watches the news on TV. (OK, sports news being the one exception.) That’s not because he doesn’t like to keep up with current events, it’s just because – well, let’s give you an example. The other night the Scripting Guy who writes this column was at the gym, dutifully riding the exercise bike. The news happened to be on, and he glanced up from his exertions and caught a story about the “wacky weather” (their phrase, not his) that recently roared through Europe. After showing a number of suitably wacky scenes (mostly of people and things being blown around by the wind) the segment concluded with the newscaster noting, “41 people died in the recent windstorms.”

Forty-one people died in those “wacky windstorms?” That’s why the Scripting Guy who writes this column doesn’t watch the news on TV. That’s also why he writes Hey, Scripting Guy!, the daily scripting column where you get all of the wackiness without any of the fatalities.

Or at least without as many fatalities.

For example, here’s a script that reports back the name of the last user to log on to a computer, and does so without causing anyone any pain or suffering.

Or at least not any permanent pain or suffering:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon"
strValueName = "DefaultUserName"

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Wscript.Echo strValue

As it turns out, any time a user logs on to a computer his or her logon name is saved in the registry; in particular, that name is stored in HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\WinLogon\DefaultUserName. (As an added bonus, the user’s domain is stored in the same registry key, in the value DefaultDomainName.) If we want to know the name of the last user to log on to a computer all we have to do is read the value of DefaultUserName.

Now, admittedly, this is not 100% guaranteed; that’s because it’s possible to change the value of (or even erase) DefaultUserName. In theory, someone with the user name kenmyer could log on to a computer and then change the value of default user to, say, pilarackerman. We’re willing to bet, however, that scenarios such as that don’t happen all that often; consequently, while this approach isn’t guaranteed it’s still going to work more often than not.

Which is something you can’t say about the Scripting Guy who writes this column.

Note. Does that mean that you use this same script to determine which user is currently logged-on to a computer? Well, maybe, assuming that someone is logged on to the computer; after all, the value of DefaultUserName remains in the registry even if no one is logged-on to the machine. If you’re interested in determining which user is currently logged-on to a computer you might check to see if Explorer.exe is running and, if it is, determine the owner of that process. But that’s another column for another day.

As you can see, our script begins by defining a constant named HKEY_LOCAL_MACHINE, with a value of &H80000002; this constant tells WMI’s Standard Registry provider which registry hive we want to work with. After defining the constant we then connect to the WMI service on the local computer, taking care to bind the root\default namespace (not the much-more commonly-used root\cimv2 namespace). And yes, we could easily connect to the WMI service on a remote computer; all we have to do is assign the name of that remote machine to the variable strComputer:

strComputer = "atl-fs-01"

After hooking up with the WMI service we then assign values to a pair of variables: the variable strKeyPath is assigned the registry path within HKEY_LOCAL_MACHINE, and the variable strValueName is assigned the name of the registry value we want to read (DefaultUserName). We then use the GetStringValue method to read the registry and store the name of the last user to log on to the computer in an out parameter named strValue:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Note. What’s an “out parameter?” An out parameter is simply a variable that we hand to a method; notice that strValue is the final parameter supplied to the GetStringValue method. In turn, the method assigns a value to that variable for us. In this case, of course, that value happens to be the value of DefaultUserName.

All we have to do now is echo back the value of the variable strValue and we’re done:

Wscript.Echo strValue

Incidentally, if you’re interested in getting back the domain name as well as the logon name, well, here’s a script that does just that:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon"

strValueName = "DefaultUserName"
objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Wscript.Echo strValue

strValueName = "DefaultDomainName"
objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Wscript.Echo strValue

And by the way, this really is true: Hey, Scripting Guy! has been running for over two and a half years now, and, to the best of our knowledge, no one has ever died from reading the column. (See? We told you that you couldn’t really be bored to death.) Admittedly, doctors point to the fact that Scripting Guy Peter Costantini has been in a coma for years as cause for concern. However, even that issue is controversial: Peter insists that he’s not in a coma, but the doctors refuse to change their diagnosis. The rest of the Scripting Guys have no comment on the issue, although we’re not sure whether, in Peter’s case, it really makes all that much difference anyway.


How Can I Prevent a Local User From Changing His or Her Password?

Hey, Scripting Guy! How can I configure a local user account so that the user can’t change his or her password?

-- DC

Hey, DC. The secret here lies in the mysterious userFlags attribute. We’ll show you how to set a user account so that the user can’t change his or her password, then we’ll fill you in on some of the other local user account properties that can be managed with the userFlags attribute. And by then it will hopefully be time for lunch!

First the script that prevents a user from changing his or her password:

Const ADS_UF_PASSWD_CANT_CHANGE = &H0040

Set objUser = GetObject("WinNT://atl-ws-01/kenmyer")

If Not objUser.UserFlags AND ADS_UF_PASSWD_CANT_CHANGE Then
    objPasswordNoChangeFlag = objUser.UserFlags XOR ADS_UF_PASSWD_CANT_CHANGE
    objUser.Put "userFlags", objPasswordNoChangeFlag
    objUser.SetInfo
End If

We start out by defining a constant (with the catchy name ADS_UF_PASSWD_CANT_CHANGE) that we’ll need to identify the proper “switch” inside the userFlags attribute. The userFlags attribute is an example of a bitmask attribute, a single attribute that contains multiple properties and property values. For now, just consider a bitmask as being a bank of switches, with each switch representing a different property. If the switch for User Can’t Change Password is on, then the user can’t change his or her password; if the switch is off, then the user can change their password. That part is fairly intuitive; the only hard part of dealing with a bitmask is that the “switches” don’t have names like User Can’t Change Password. Instead, they have hexadecimal values, like &H0040. To carry out our task, we need to flip the &H0040 switch. That’s why we define this constant.

Next we connect to the kenmyer account on the computer atl-ws-01. At that point, we check to see if the switch in question is already on. When working with bitmasks, you’ll often see code like this:

If objUser.UserFlags AND ADS_UF_PASSWD_CANT_CHANGE Then

In plain English, this can be read as “If the userFlags attribute is present and if the ADS_UF_PASSWD_CANT_CHANGE switch is on, then this statement is true and we should do something.” In our case, we’re not interested in switches that are on; if the can’t change password flag is already set, then our work is done. We’re only interested in switches that are off. Hence we use this line of code, which takes action only if the switch is not on:

If Not objUser.UserFlags AND ADS_UF_PASSWD_CANT_CHANGE Then

Now we’re really going to confuse you. Take a look at this line of code:

objPasswordNoChangeFlag = objUser.UserFlags XOR ADS_UF_PASSWD_CANT_CHANGE

Actually – all appearances aside – this is really pretty simple code. All we’re doing here is toggling the value of the user can’t change password switch. That’s what the XOR command does. If the switch is on, XOR turns if off; if the switch is off, XOR turns it on. We’re taking the current value of the userFlags attribute and toggling the user can’t change password switch. Because we know this switch is off (remember the If Not statement we just used?), the XOR command will turn that switch on. Our variable objPasswordNoChangeFlag will then contain exactly the same values that are in the current userFlags attribute, with one exception: the user can’t change password switch will now be on instead of off.

Still with us? The rest of the script is easy. This line of code write the value of the variable objPasswordNoChangeFlag to the userFlags attribute:

objUser.Put "userFlags", objPasswordNoChangeFlag

We then use the SetInfo command to write those changes to the user account. Just like that, local user Ken Myer will no longer have the right to change his password on the computer atl-ws-01.

And what if wanted to let Ken Myer change his password? Hey, that’s easy. All we have to do is check to see if the user can’t change password switch is on, and then use XOR to turn it off:

Const ADS_UF_PASSWD_CANT_CHANGE = &H0040

Set objUser = GetObject("WinNT://atl-ws-01/kenmyer")

If objUser.UserFlags AND ADS_UF_PASSWD_CANT_CHANGE Then
    objPasswordNoChangeFlag = objUser.UserFlags XOR ADS_UF_PASSWD_CANT_CHANGE
    objUser.Put "userFlags", objPasswordNoChangeFlag
    objUser.SetInfo
End If

The only difference is that we removed the word Not from our If-Then statement. That’s because now we want to find instances where the switch is on, the better to turn it off.

We agree: these bitmask attributes are confusing. If you’d like a little more information (and a picture or two), you might check out this portion of the Microsoft Windows 2000 Scripting Guide. And, as promised, here are some other local user account properties that can be managed using the userFlags attribute:

Property | Constant | Value
Logon script will be executed | ADS_UF_SCRIPT | &H0001
Account is disabled | ADS_UF_ACCOUNTDISABLE | &H0002
Account requires a home directory | ADS_UF_HOMEDIR_REQUIRED | &H0008
Account is locked out | ADS_UF_LOCKOUT | &H0010
Account does not require a password | ADS_UF_PASSWD_NOTREQD | &H0020
User cannot change password | ADS_UF_PASSWD_CANT_CHANGE | &H0040
Encrypted text password allowed | ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED | &H0080
Account password never expires | ADS_UF_DONT_EXPIRE_PASSWD | &H10000
Smartcard required for logon | ADS_UF_SMARTCARD_REQUIRED | &H40000
Password has expired | ADS_UF_PASSWORD_EXPIRED | &H800000

If you get bored sometime, substitute these values into the user can’t change password script and see what happens. (Of course, as always, we recommend you use a test machine – or at least a test account – when experimenting with scripts like this.)


How Can I Rename a Local User Account on a Windows XP Computer?

Hey, Scripting Guy! How can I rename a local user account on a Windows XP computer?

-- TY

Hey, TY. You know, nowadays the “in” thing is to poke fun at Microsoft and Windows XP. We know: we’ve seen the new commercials from that one computer company. (Remember, the computer company that used to be good, a long time ago?) But as much as we Scripting Guys would like to be thought of as cool we’re not going to make fun of Windows XP today; in fact, we’re going to sing its praises. Is that because Microsoft signs our paycheck? Well, OK, sure. But in addition to that, Windows XP (and Windows Server 2003) makes it possible for you to use a script to rename local user accounts. That might not be cool, but it is useful.

Which is almost as good.

Here – in all its uncool glory – is the script we came up with:

strComputer = "."

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

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where LocalAccount = True And Name = 'kmyer'")

For Each objAccount in colAccounts
    objAccount.Rename "kenmyer"
Next

The first thing you might have noticed about this script is that it uses WMI instead of ADSI. Wait a second, you’re thinking: don’t the Scripting Guys always tell us to use ADSI when it comes to managing users and groups? Yes, we do, and that’s exactly what you should do … most of the time. Renaming local user accounts happens to be a rare exception, however. With ADSI you need to use the MoveHere method to rename user accounts. That’s fine, except for the fact that MoveHere only works for domain accounts; if you try renaming a local user account you’ll get the much-beloved “unspecified error” error message. The unspecified error: you can’t use MoveHere to rename a local user account.

Fortunately, though, you can use WMI to rename local user accounts, provided you’re running Windows XP or Windows Server 2003. Why just those two versions of Windows? That’s because those are the only versions in which the Win32_UserAccount class includes the Rename method. You can’t use WMI to rename local user accounts on a Windows 2000 computer because the Rename method doesn’t exist in Windows 2000.

And, no, at this point in time there’s probably no chance that this capability will be added to Windows 2000.

But you don’t care about that do you, TY, because you specifically asked about performing this task on a Windows XP computer. With that in mind, let’s examine the code in a little more detail, beginning with the fact that the script starts out by connecting to the WMI service on the local computer. What if the account we wanted to rename happens to reside on a remote computer? No problem; just set the value of the variable strComputer to the name of the remote machine.

Ah, come on: that’s at least kind of cool, isn’t it? Well, some of the Scripting Moms thought it was kind of cool.

After making the connection to the WMI service we next encounter this line of code:

Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where LocalAccount = True And Name = 'kmyer'")

What we’re doing here is retrieving a collection of all the user accounts that we want to rename. Of course, there’s only one account we want to rename: the local account kmyer. Hence our Where clause, which asks the script to “select all the accounts where the value of the LocalAccount property is true (meaning this is a local account rather than a domain account) and where the account Name is equal to kmyer.” Because account names must be unique on a computer, that means this query will return, at most, a single item.

Note. Whatever you do, don’t leave off LocalAccount = True. If you do the script will not only retrieve and sort through all your local user accounts, but all your Active Directory user accounts as well. That could take awhile to complete; in addition, it could lead to problems if you happen to have an Active Directory user account named kmyer.

Believe it or not we’re almost done. All we have left to do is set up a For Each loop to cycle through all the items in the collection. Inside the loop we use the Rename method to rename the user account, in this case changing the account name to kenmyer:

For Each objAccount in colAccounts
    objAccount.Rename "kenmyer"
Next

That’s it: just like that the account will be renamed. The next time Ken Myer logs on to the local computer he’ll need to log on as kenmyer rather than kmyer.

And, yes, you can indeed use this script to rename the local Administrator account. For example, this revised code renames the Administrator account to NotAdministrator:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colAccounts = objWMIService.ExecQuery _
    ("Select * From Win32_UserAccount Where LocalAccount = True And Name = 'Administrator'")

For Each objAccount in colAccounts
    objAccount.Rename "NotAdministrator"
Next

Now that is cool!

Come to think of it, you don’t suppose anyone would be interested in a commercial about using scripts to rename local user accounts, do you? Good point. But there’s probably not much chance we could get Microsoft to spring for a bunch of bikini- and/or Speedo-clad extras.


How Can I Use Windows PowerShell to Determine the Status of the Guest Account?

Hey, Scripting Guy! How can I use Windows PowerShell to determine whether or not the Guest account has been disabled on a computer?

-- RB

Hey, RB. You know, last week at this time the Scripting Guy who writes this column was on vacation, a vacation spent primarily sitting outside in the sun, only occasionally getting up to watch TV. During that week the toughest question he had to deal with was this: “Dad, are you going to make lunch or are you just going to go out and pick something up?”

Actually, that ended up being a pretty easy question to answer.

But that was then, and this is … well, not then. Last week this Scripting Guy had to decide whether he wanted to make a sandwich or just go buy a sandwich. This week he has to decide how use Windows PowerShell to determine whether or not the Guest account has been disabled on a computer. It hardly seems fair, does it?

So why is this such a difficult question to answer, at least compared to “Would you rather do some work or have someone else do the work for you?” Well, the problem is that Windows PowerShell has no built-in cmdlet for working with local user accounts. That makes working with local user accounts a little bit tricky, and has led to some surprisingly clever (albeit complicated) workarounds involving the .NET Framework. (If you don’t know what cmdlets are – or, for that matter, if you don’t know what Windows PowerShell is – then you should take a look at the Script Center’s What Can I Do With Windows PowerShell? page.)

Admittedly, in some cases those clever and complicated workarounds are required; for example, there doesn’t appear to be an easy way to change a user’s password without resorting to the .NET Framework. Fortunately, though, this isn’t one of those cases. If we want to know the status of the Guest account on the computer atl-fs-01 then we don’t have to do anything fancier than this:

Get-WmiObject Win32_UserAccount -computername "atl-fs-01" -filter "LocalAccount=True AND Name='Guest'" |
Select-Object Domain,Name,Disabled

Note. The Windows PowerShell commands shown in this column should all be typed on a single line.

What we’re doing here is taking advantage of the Get-WMIObject cmdlet (and the Win32_UserAccount class) in order to retrieve information about the Guest account on the computer atl-fs-01. As it turns out, Get-WMIObject works great for our purposes: it can easily return information about any and all local user accounts. We should point out, however, that it might not suit everyone’s fancy, and for at least two reasons.

For one, the –filter parameter we used works only on Windows XP and Windows Server 2003; that’s because those are the only two operating systems that support the LocalAccount property. In order to run this command on a Windows 2000 computer, you’d not only need to have Windows PowerShell installed but you’d have to modify the –filter parameter.

For another, the Win32_UserAccount class does have a few limitations, most notably the inability to set or change a user’s password. That means you can’t use this WMI class as a complete substitute for ADSI and the WinNT provider. On the bright side, however, many of the Win32_UserAccount properties are read-write, making it possible for you to do things like enable a disabled account. We won’t bother explaining this particular command in any detail, but here’s at least one way to enable the Guest account using Windows PowerShell:

$a = Get-WmiObject Win32_UserAccount -computername "atl-fs-01" -filter "LocalAccount=True AND Name='Guest'";
 $a.disabled=$false; $a.Put()

Note. Why did we say “at least one way” to enable the Guest account? Well, Windows PowerShell usually offers several different routes to the end goal. If you know of an alternate or – gasp! –a better way to accomplish this same task, let us know. We’ll soon be featuring alternatives/improvements to this column in a new feature we call Hey, Wait a Minute, Scripting Guy!

Caveats and cautions aside, however, the first command we showed you a minute ago should return information about the Guest account and its status. Which is all you really wanted to do anyway:

Domain             Name                        Disabled
------             ----                        --------
ATL-FS-01          Guest                       True

Good question: how does this command work? To begin with, we should note that we actually have two commands here, a pair of commands chained together using the Windows PowerShell pipeline (the | character). On the left-hand side of the pipeline we use the Get-WMIObject cmdlet to retrieve information from the Win32_UserAccount class. Notice that, in addition to indicating the WMI class we want to use, we also pass Get-WMIObject the following two parameters:

· -computername "atl-fs-01". This, of course, is the name of the computer we’re interested in. To retrieve information for the local computer just leave this parameter out.

· -filter "LocalAccount=True AND Name='Guest'". This is equivalent to the Where clause (minus the word Where) in a WQL query. As you can see, we’re simply requesting that the returned data be limited to those instances of the Win32_UserAccount class where the LocalAccount property is equal to True and the Name property is equal to Guest. Simple, huh?

The Get-WMIObject command is all we need to retrieve all the properties of the Guest account (including the Disabled property). However, it’s not enough to display all the properties of the Guest account. As it turns out, Windows PowerShell has the ability to use XML files that specify what (and how) data is displayed. By default, Windows PowerShell pre-formats many WMI classes, including Win32_UserAccount. If we only use the left-hand side of our command we’ll end up getting back something similar to this:

AccountType : 512
Caption     : ATL-FS-01\Guest
Domain      : ATL-FS-01
SID         : S-1-5-21-1292428093-1580436667-725345543-501
FullName    :
Name        : Guest

Nice, but the output doesn’t exactly tell us whether or not the account has been disabled.

But don’t despair: Windows PowerShell also provides us a number of ways to override the default formatter. For example, you retrieve the information of interest and then pipe it over to the Select-Object cmdlet; from there you can use the Select-Object cmdlet to pick out (and display) the properties of interest to you:

Select-Object Domain,Name,Disabled

Which is exactly what we did.

And so now it’s two hours until quitting time, and we’re faced with yet another tough question: get a head start on the next day’s column, or go home early? Decisions, decisions, decisions ….


How Can I Verify that None of My Local User Accounts have a Blank Password?

Hey, Scripting Guy! How can I verify that none of my local user accounts have a blank password?

-- TB

Hey, TB. To begin with, we should point out that at least some of you won’t need to make this check on computers within your domain; that’s because you’re using password policies in the domain that would prohibit the use of a blank password. (For example, you might require passwords to be at least 7 characters long, a policy that prevents anyone from using a blank password.) For organizations who don’t have a password policy (or for workgroups or home computers), however, you can use a script like this to check for blank passwords:

On Error Resume Next

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName

strPassword = ""

Set colAccounts = GetObject("WinNT://" & strComputer)
colAccounts.Filter = Array("user")

For Each objUser In colAccounts
    objUser.ChangePassword strPassword, strPassword
    If Err = 0 or Err = -2147023569 Then
        Wscript.Echo objUser.Name & " is using a blank password."
    End If
    Err.Clear
Next

Admittedly, some of you might be looking at this and thinking, “That’s a nice little script and all. But how come you didn’t just run some sort of query asking for a list of users who have a blank password?” Well, there’s a good reason for that: there’s no way to run such a query. Even as administrators it’s impossible for us to write a script that retrieves a user’s password; there’s no way to get that information.

Therefore, we have to be a bit sneaky about this. What we do, instead, is try to change the user’s password using the ChangePassword method. ChangePassword requires us to know the user’s current password; in this script, we’ll try using a blank password as the user’s current password. If the script fails, that means the user is not using a blank password. If the script succeeds, however, that can only mean one thing: the user is using a blank password. In that case, we’ll echo back the name of that user.

To do this we start out by adding the On Error Resume Next statement, creating an instance of the Wscript.Network object, and then retrieving the value of the ComputerName property; fortuitously enough, ComputerName happens to be the name of the local computer. We set a variable named strPassword to an empty string (“”), then use this line of code to bind to the local computer:

Set colAccounts = GetObject("WinNT://" & strComputer)

Having made the connection, we next apply a Filter to ensure that we are working only with user accounts (we don’t want to mess around with printers, services, groups, and other objects that are found in the local Security Accounts Manager):

colAccounts.Filter = Array("user")

Applying a filter leaves us with a collection consisting solely of local user accounts; in turn, we then set up a For Each loop to walk through that collection. For each account in the collection we call the ChangePassword method, passing the variable strPassword as both the first parameter (current password) and the second parameter (the new password):

objUser.ChangePassword strPassword, strPassword

We then immediately check the value of the Err object. If ChangePassword succeeds, then Err will be equal to 0; that means the user has a blank password. Alternatively, Err might be equal to -2147023569; that means the method would have succeeded except that, due to local policy, not enough time has elapsed since the last password change. But because the method would have succeeded, we know that the user has a blank password. If either condition is True we echo back the name of the user.

Note. Yes, technically we also change the password, although we’re simply “changing” it to a blank password, which is its current value. That means the user is still able to logon with a blank password. If we wanted to, we could make the second parameter passed to ChangePassword something other than an empty string. In that case, running the script would give the user a new password (whatever value we used as the second parameter).

We then clear the value of the error object (Err.Clear), loop around, and test the next account.

And yes, you can easily modify this script to work against Active Directory accounts. Here’s a sample script that tests all the accounts in the domain fabrikam.com:

On Error Resume Next

Const ADS_SCOPE_SUBTREE = 2

strPassword = ""

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT AdsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE
objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
strPath = objRecordSet.Fields("AdsPath").Value
Set strUser= GetObject(strPath)
strUser.ChangePassword strPassword, strPassword
If Err= 0 or Err = -2147023569 Then
Wscript.Echo strUser.CN
End If
Err.Clear
objRecordSet.MoveNext
Loop

If you use this script bear in mind that it tries to change the password for all your user accounts. If you have hundreds or thousands of accounts, the script could take an hour, maybe even more, to complete. There’s nothing wrong with that, just something to be aware of.

And, of course, you can also modify this script to test for other passwords. Want to see if anyone has a password of password? Then just set the value of strPassword accordingly:

strPassword = "password"


Can I Retrieve Just Failure Events from the Security Event Log?

Hey, Scripting Guy! Is there a way to retrieve just Failure Audit events from the Security event log?

-- KA

Hey, KA. Interesting, isn’t it: any time the subject is failure, people turn to the Scripting Guys. What makes you think we know anything about failure?

Ok, you’re right: silly question. As far as your question goes, it’s very easy to retrieve just Security Failure Audit events from the Security event log; in fact, we just happened to have a script lying around that does that very thing:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate,(Security)}!\\" & _
        strComputer & "\root\cimv2")
Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * FROM Win32_NTLogEvent WHERE Logfile = 'Security' " & _
        "AND EventType = 5")
For Each objEvent in colLoggedEvents
    Wscript.Echo "==================================================="
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Computer Name: " & objEvent.ComputerName
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
    Wscript.Echo "Event Type: " & objEvent.Type
    Wscript.Echo "User: " & objEvent.User
    Wscript.Echo
Next

A pretty simple little script, but there are at least two things you should take note of. First, notice that we included the (Security) parameter when connecting to WMI:

Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate,(Security)}!\\" & _
        strComputer & "\root\cimv2")

You must include this parameter any time you’re working with the Security event log; leave it out, and the script won’t work. And, yes, we know you’re a local administrator and we know you have the right to read the Security event log. For better or worse, though, WMI doesn’t care about that: you still have to include the (Security) parameter.

Second, note the two parts of our WHERE clause:

("Select * from Win32_NTLogEvent WHERE Logfile = 'Security' " & _
        "AND EventType = 5")

For this script, we only want to retrieve events that meet two criteria: they’re recorded in the Security event log, and they have an EventType of 5. As you probably figured out, in WMI-speak an EventType of 5 means a Failure Audit. Alternatively, you could search for EventTypes of 1 (Error), 2 (Warning), 3 (Information), or 4 (Security Audit Success). Because we want Failure Audit events, we look for events in the Security Logfile with an EventType of 5. Thus:

WHERE Logfile = 'Security' AND EventType = 5

Cool, huh? If you’d like more information about working with event logs (including some sample queries you might find useful), check out the Logs chapter in the Microsoft Windows 2000 Scripting Guide.

And as long as we have your attention, we might want to add one more thing. The script, as it currently stands, will display the TimeWritten property (that is, the date and time that the event was recorded in the event log) using WMI’s default Universal Time Coordinate format. In other words, you’ll get back results similar to this:

20041025124000.000000-420

How … nice …. But don’t despair. Here’s a modified version of the script that includes a function (WMIDateStringTodate) that will convert this UTC value to something a bit easier to read:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate,(Security)}!\\" & _
        strComputer & "\root\cimv2")
Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * FROM Win32_NTLogEvent WHERE Logfile = 'Security' " & _
        "AND EventType = 5")
For Each objEvent in colLoggedEvents
    Wscript.Echo "==================================================="
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Computer Name: " & objEvent.ComputerName
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    dtmEventDate = objEvent.TimeWritten
    strTimeWritten = WMIDateStringToDate(dtmEventDate)
    Wscript.Echo "Time Written: " & strTimeWritten
    Wscript.Echo "Event Type: " & objEvent.Type
    Wscript.Echo "User: " & objEvent.User
    Wscript.Echo
Next

Function WMIDateStringToDate(dtmEventDate)
    WMIDateStringToDate = CDate(Mid(dtmEventDate, 5, 2) & "/" & _
        Mid(dtmEventDate, 7, 2) & "/" & Left(dtmEventDate, 4) _
            & " " & Mid (dtmEventDate, 9, 2) & ":" & _
                Mid(dtmEventDate, 11, 2) & ":" & Mid(dtmEventDate, _
                    13, 2))
End Function

We won’t bother explaining how this works today, but if you have any questions about it, let us know. Maybe we’ll go into it in more detail in a future column.


How Can I Backup an Event Log to a Text File?

Hey, Scripting Guy! How can I backup an event log to a text file?

-- IMBDS

Hey, IMBDS. You know, the Scripting Guys have dedicated their lives to helping others. (Well, OK, originally we dedicated our lives to having others help us. But when no one seemed very interested in that we changed our minds.) That means that, on those rare occasions when we actually learn something, we make a point of keeping the rest of the world informed as well.

For example, just yesterday we were learned (much to our surprise) that the Do Not Call registry in the US (designed to prevent telemarketers from barraging you with phone calls) has an expiration date: if you register your phone number, that registration is only good for 5 years. (Because, apparently, most people can only go so long before they absolutely have to hear from a telemarketer.) For example, the Scripting Guy who writes this column just entered his home phone number into the registry’s online verification tool, and discovered that his registration is due to expire in a year.

Wonderful.

Note. If you want to know the truth, we’re not sure that the Do Not Call registry is all that effective anyway. For example, even though his phone number is registered, the Scripting Guy who writes this column gets several phone calls a day asking him for money.

And those are just from the Scripting Son.

Anyway, we thought everyone should know that the Do Not Call registry is anything but permanent. We also thought people should know that there’s no simple, straightforward way to backup an event log to a text file; as it is, WMI’s BackupEventLog method can only save data using the event log binary format. Of course, that doesn’t mean that you can’t save event log data to a text file; it just means you have to do that using a script like this one instead:

strComputer = "."

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

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where LogFile='Application'")

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\Events.txt")

For Each objEvent in colEvents
    strTimeWritten = objEvent.TimeWritten

    dtmTimeWritten = CDate(Mid(strTimeWritten, 5, 2) & "/" & _
        Mid(strTimeWritten, 7, 2) & "/" & Left(strTimeWritten, 4) _
            & " " & Mid (strTimeWritten, 9, 2) & ":" & _
                Mid(strTimeWritten, 11, 2) & ":" & Mid(strTimeWritten, 13, 2))

    dtmDate = FormatDateTime(dtmTimeWritten, vbShortDate)
    dtmTime = FormatDateTime(dtmTimeWritten, vbLongTime)

    strEvent = dtmDate & vbTab
    strEvent = strEvent & dtmTime & vbTab
    strEvent = strEvent & objEvent.SourceName & vbTab
    strEvent = strEvent & objEvent.Type & vbTab
    strEvent = strEvent & objEvent.Category & vbTab
    strEvent = strEvent & objEvent.EventCode & vbTab
    strEvent = strEvent & objEvent.User & vbTab
    strEvent = strEvent & objEvent.ComputerName & vbTab

    strDescription = objEvent.Message
    If IsNull(strDescription) Then
        strDescription = "The event description cannot be found."
    End If
    strDescription = Replace(strDescription, vbCrLf, " ")
    strEvent = strEvent & strDescription

    objFile.WriteLine strEvent
Next

objFile.Close

Admittedly, this is a somewhat complicated-looking script. And there’s a good reason for that: some of the things we need to do here are somewhat complicated. But don’t fret; we’re going to walk you through the entire thing and explain exactly how it all works.

Just as soon as we answer this phone call from an Unknown Caller.

The script starts out simply enough, connecting to the WMI service on the local computer. And, being the Microsoft Scripting Guys, we know what you’re thinking. And the answer to your question is yes, you can run this script against a remote computer; all you have to do is assign the name of that remote computer to the variable strComputer, like so:

strComputer = "atl-dc-01"

Ah, that’s no big deal. When you’re a Scripting Guy you’re supposed to know about things like that.

After making the connection to the WMI service we then execute the following line of code, a line of code that returns a collection of all the events found in the Application event log:

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where LogFile='Application'")

Note. Needless to say, you aren’t limited to backing up just the Application event log; if you want to, you can back up any (or even all) your event logs. For more information (particularly information about working with the Security event log) see the Microsoft Windows 2000 Scripting Guide.

That brings us to these two lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\Events.txt")

As we noted earlier, there’s no built-in method for backing up an event log as a text file; that is, there’s no WMI method like, say, BackupAsTextFile. That means that there’s only one way for us to programmatically save the contents of an event log as a text file: we need to write the information to the text file ourselves. So how do we do that? Well, for starters, we use the preceding two lines of code to create an instance of the Scripting.FileSystemObject object, and then create a brand-new text file named C:\Scripts\Events.txt.

And then it’s time to do some real work. We should probably note that it actually is possible to save an event log as a text file … provided that you open up Event Viewer and do all the work yourself. If you do that, Event Viewer will save the event log as a tab-delimited file. We’ve decided to use this same format: we’re going to save all the events in the Application event log in tab-delimited format. (That is, we’ll separate the individual fields for each event – things like the event code and the event description – using tabs.)

To that end, the first thing we do is create a For Each loop that loops us through each of the events in our collection. Inside the loop, we grab the value of the TimeWritten property and stash it in a variable named strTimeWritten:

strTimeWritten = objEvent.TimeWritten

Here’s one place where things get a tad bit complicated. As you probably know, WMI stores dates and times using the UTC (Universal Time Coordinate) format; that means the value of the TimeWritten property is going be something like this:

20070905121045.578000-420

Nice, huh? We won’t bother explaining how to decipher a UTC date-time value; you can find a pretty good explanation of that in the Scripting Guide. Suffice to say, however, that a value like 20070905121045.578000-420 isn’t quite as intuitive as information like this:

9/5/2007 12:10 PM

But how in the world do we convert a UTC value to something a human being can read and understand? Why, like this, of course:

dtmTimeWritten = CDate(Mid(strTimeWritten, 5, 2) & "/" & _
Mid(strTimeWritten, 7, 2) &
"/" & Left(strTimeWritten, 4) _
& " " & Mid (strTimeWritten, 9, 2) & ":" & _
Mid(strTimeWritten, 11, 2) & ":" & Mid(strTimeWritten, 13, 2))

Again, you can find a complete description of what we’re doing in the Scripting Guide. As a quick summary, all we’re really doing here is grabbing the individual pieces of the UTC value and constructing our own date-time value. For example, the first four digits in a UTC value represent the current year; thus 20070905121045.578000-420 indicates that we’re dealing with the year 2007. In turn, that means code like this, which snags the first four characters in our UTC value, also lets us know that we’re dealing with the year 2007:

Left(strTimeWritten, 4)

If you follow the logic of the script all the way through, you’ll see that, eventually, the variable dtmTimeWritten gets assigned a value like this:

9/5/2007 12:10:45 PM

Now that’s something we can work with.

As it turns out, the tab-delimited file format used by Event Viewer separates the date (9/5/2007) from the time (12:10:45 PM). That’s fine; because of that, we next use the FormatDateTime function and the VBScript constants vbShortDate and vbLongTime to store the date in a variable named dtmDate and the time in a variable named dtmTime:

dtmDate = FormatDateTime(dtmTimeWritten, vbShortDate)
dtmTime = FormatDateTime(dtmTimeWritten, vbLongTime)

Once we’ve taken care of the date and time, our next step is to begin constructing the first line of the text file (which will correspond to the information in the first event recorded in the event log). That’s what this block of code is for:

strEvent = dtmDate & vbTab
strEvent = strEvent & dtmTime & vbTab
strEvent = strEvent & objEvent.SourceName & vbTab
strEvent = strEvent & objEvent.Type & vbTab
strEvent = strEvent & objEvent.Category & vbTab
strEvent = strEvent & objEvent.EventCode & vbTab
strEvent = strEvent & objEvent.User & vbTab
strEvent = strEvent & objEvent.ComputerName & vbTab

As you can see, there’s nothing too fancy here. In the first line, we assign the date (represented by the variable dtmDate) and a tab character (the VBScript constant vbTab) to a variable named strEvent. (Why a tab character? Because we want each field to be separated by a tab.) In line 2, we assign strEvent the current value of strEvent plus the value of the variable dtmTime plus another tab character. We then repeat this process several times, tacking the values of WMI properties such as SourceName and Type to the value of strEvent.

That brings us to this block of code:

strDescription = objEvent.Message
If IsNull(strDescription) Then
    strDescription = "The event description cannot be found."
End If
strDescription = Replace(strDescription, vbCrLf, " ")
strEvent = strEvent & strDescription

Perhaps the most important property of an event log event is the Message property; the Message property, as you doubtless know, provides a (more or less) detailed description of the event. Unfortunately, however, the Message property can potentially cause havoc with our script. So what do we do about that? That’s easy: we take steps to defuse these problems before they occur.

What kind of problems are we talking about? Well, for one thing, the Message property often includes one or more carriage return-linefeed characters; in other words, the value of the Message property might look like this:

Security policy in the Group policy objects has been applied successfully.

For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

What’s wrong with that? Well, in a tab-delimited file, all the information for a single event must fit on one line; that’s not going to be the case if the Message includes a carriage return-linefeed character. Consequently, we use the Replace function to replace any carriage return-linefeed characters (vbCrLf) with a blank space:

strDescription = Replace(strDescription, vbCrLf, " ")

That’s great, but, in turn, this introduces another problem. For some events the Message property doesn’t actually have a value. In Event Viewer, that’s no problem; in a case like that, Event Viewer displays a message similar to this:

The description for Event ID ( 0 ) in Source ( iPod Service ) cannot be found. The local computer may not have the
necessary registry information or message DLL files to display messages from a remote computer. You may be
able to use the /AUXSOURCE= flag to retrieve this description; see Help and Support for details. The following
information is part of the event: Service started.

But that works only in Event Viewer; that information does not get passed to the Message property. Instead, the value of the Message property comes back as Null.

Is that a problem? As a matter of fact, it is; running the Replace function against a Null value is a recipe for disaster. (Assuming, of course, that you consider a script that crashes to be a disaster.) Therefore, before we call the Replace function we execute this block of code:

If IsNull(strDescription) Then
    strDescription = "The event description cannot be found."
End If

All we’re doing here is using the IsNull function to determine whether the Message property (which is stored in the variable strDescription) happens to be Null. If it’s not, well, no problem. If it is, then we simply set the value of strDescription to the generic message “The event description cannot be found.”

After dealing with any potential Null values, we call the Replace function, then tack the value of strDescription onto the end of our event string:

strEvent = strEvent & strDescription

And then we simply call the WriteLine method to write this event to the text file:

objFile.WriteLine strEvent

From there we zip back to the top of our For Each loop and repeat the process with the next event in the collection. After we’ve added all the events to the text file we execute the Close method, close the file, and call it a day.

We hope that answers your question, IMBDS. If it doesn’t, don’t bother to send us an email; instead, just give us a call. After all – sigh – that’s what everyone else does. Oops; gotta go. The phone’s ringing.

Note. In the true spirit of entrepreneurship, there are companies that will actually call you, on the phone, and (for a small fee) offer to put your phone number on the Do Not Call registry. Only in America ….


How Can I Find the Date of the Oldest Event in an Event Log?

Hey, Scripting Guy! How can I find the date of the oldest event in an event log?

-- JL

Hey, JL. You know, you just had to make a reference to “old” in your question, didn’t you? Just today one of the Scripting Guys received a letter from the American Association of Retired Persons (AARP). Although the letter acknowledged that this Scripting Guy wasn’t 50 years old yet (and thus wasn’t eligible for membership), it did invite him to pre-register for membership; after all, the letter implied, it won’t be that much longer, will it? Thoughtfully included in the letter was a sample membership card with the Scripting Guy’s name emblazoned on it. Trust us: if you’ve never felt old before, then you’ve never seen your name on an AARP membership card.

But enough about that; after all, you aren’t looking for old Scripting Guys but old events in an event log. (Lucky for you, seeing as how there aren’t any old Scripting Guys. Although we sometimes wonder about Jean.) At first this proved to be a bit of a problem, and for two reasons. For one, it’s difficult to translate an idea like “find the oldest item in a collection” into a WQL query; the WQL query language doesn’t support keywords like MIN or MAX. Granted, you can always just grab all the events and then determine which event is the oldest, but that introduces another problem: because you can’t specify a sort order in a WQL query, how will you know which event is the oldest? So many questions revolving around what ought to be a simple enough request!

Ah, but where there’s a will there’s a way, huh? (No, not that kind of will. We keep telling you, we’re not that old!) As it turns out, each time an event is written to the event log that event is assigned a record number: the first event written to a log is assigned the number 1, the second event is assigned the number 2, etc. This remains true even when an event log is cleared. Suppose you have 1,000 events in an event log and you clear the log. Guess what record number will be assigned to the first event to be written to the cleared log? You got it: 1.

Why does that matter to us? That matters because it means the oldest event in an event log will always have record number 1. In turn, that means we can find the oldest record in an event log simply by locating event 1. And we can do that by using a script like this one:

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

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where Logfile = 'System' " & _
        "AND RecordNumber = 1")

For Each objEvent in colEvents
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
Next

As you can see, this is a fairly run-of-the-mill WMI script. We start off by connecting to the WMI service on the local computer, although – as we never tire of pointing out – this script can also be run against a remote machine. We then use this WQL query to return all the events from the System event log that have a RecordNumber equal to 1:

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where Logfile = 'System' " & _
        "AND RecordNumber = 1")

Because record numbers must be unique we know we’ll only have one item in the collection. And because the first event written to the event log will have a record number of 1, we also know that we’ve located the oldest event. All that’s left now is to set up a For Each loop to walk through our one-item collection and echo back the value of the TimeWritten property:

For Each objEvent in colEvents
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
Next

We should add that, for better or worse, TimeWritten will be returned as a UTC (Universal Time Coordinate) value. But that’s all right: if you’re looking for an easy way to convert that UTC value to a regular (and readable) date-time value take a look at this sample script in the Script Center Script Repository.

Now, we know what you’re thinking: if it’s possible to find the oldest event in an event log does that mean it’s possible to find the newest event that’s been added to an event log? You bet. However, you’ll have to wait until tomorrow to find out how to do that. After all, at our age one column a day is the best we can do.


How Can I Monitor Event Log Messages for Specific Words?

Hey, Scripting Guy! How can I monitor event log messages for a specific word or phrase?

-- GH

Hey, GH. You know, now that the 2007 Scripting Games are over (have we mentioned that the 2008 Games are just a year away?) it’s time for the Scripting Guys to rejoin the real world. Before we answer your question, however, could you give us a second to thumb through this pile of newspapers and see if we missed anything in the last month or so? Let’s see: boring; boring; no one cares; boring.

Oh, wait, here’s something: allegedly a NASA astronaut went off the deep end, as they say, and set out to kidnap a romantic rival. Which leads to an obvious question: have the Scripting Guys ever gone off the deep end and tried to kidnap someone?

Well, we’re ashamed to admit it, but yes, we did. Shortly after Peter Costantini accepted his new position as a Microsoft Program Manager the remaining Scripting Guys went a tad bit crazy; in fact, we kidnapped Peter, tied him up in the basement of Scripting Guys Headquarters, and delivered an ultimatum: either rejoin the team, or bake us a batch of cupcakes. (The cupcakes were Dean’s idea.) We gave him 24 hours to make up his mind.

So, yes, we did go off the deep end – once. But we’re back to normal now and, to prove it, we’ll write you a script that monitors for specific words in an event log message. (Interestingly enough, that’s the very same task Washington State Patrol officers use to test someone for drunk driving!)

For example, suppose you have a script named Test.vbs. You’d like to be notified any time an event written to the event log includes the term Test.vbs. How can you do that? Here’s how:

strComputer = "."

Set objWMIService = GetObject("winmgmts:{(Security)}\\" & _
        strComputer & "\root\cimv2")

Set colEvents = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceCreationEvent Where " _
        & "TargetInstance isa 'Win32_NTLogEvent'")

Do
    Set objEvent = colEvents.NextEvent
    If InStr(LCase(objEvent.TargetInstance.Message), "test.vbs") Then
        Wscript.Echo Now
        Wscript.Echo "Category: " & objEvent.TargetInstance.Category
        Wscript.Echo "Event Code: " & objEvent.TargetInstance.EventCode
        Wscript.Echo "Message: " & objEvent.TargetInstance.Message
        Wscript.Echo "Record Number: " & objEvent.TargetInstance.RecordNumber
        Wscript.Echo "Source Name: " & objEvent.TargetInstance.SourceName
        Wscript.Echo "Event Type: " & objEvent.TargetInstance.Type
        Wscript.Echo
    End If
Loop

As you can see, we start out by connecting to the WMI service on the local computer (although this script can also monitor event logs on remote machines). Notice, however, that our WMI binding string includes the Security privilege: Set objWMIService = GetObject("winmgmts:{(Security)}\\". Is that important? You bet it is, provided that you want to monitor all the event logs, including the Security event log. If that’s the case then you have to include the Security privilege. If you’re not interested in the Security event log, however, then the answer is no, this isn’t important at all.

After connecting to the WMI service we issue the following query, a query that asks WMI to notify us any time a new event gets written to any of our event logs:

Set colEvents = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceCreationEvent Where " _
        & "TargetInstance ISA 'Win32_NTLogEvent'")

What we’re doing here is monitoring for new object instances (__InstanceCreationEvent), but only if those new objects happen to be members of the Win32_NTLogEvent class. You might have noticed that we don’t include a “polling interval” (e.g., WITHIN 10) in this query. Why not? Because polling intervals are used only when monitoring WMI classes that don’t have a custom event provider. That’s not the case with the event logs; the event logs do have a custom event provider. Because of that we can get instant notification of any new events, without having to include code that instructs WMI to periodically go out and check for new events.

Note. Of course that doesn’t make any sense; didn’t you realize this is the Hey, Scripting Guy! column?!? But seriously, folks, if you could use a little more information about WMI monitoring scripts – as well as a definition of terms like polling interval – then you might want to view the Scripting Guys webcast An Ounce of Prevention: An Introduction to WMI Events.

After issuing our query we set up a Do loop that’s designed to run forever and ever. That, by the way, is one reason why you should run this script in a command window under the CScript script host; if you ever decide that you do want to terminate the script you can do so simply by closing the command window in which the script is running.

Inside that loop we use this block of code to instruct the script to sit around and wait until an event gets written to one of the event logs:

Set objEvent = colEvents.NextEvent

And what happens when an event does get written to one of the event logs? Well, when that happens we use the following line of code to see if the event message includes the term test.vbs:

If InStr(LCase(objEvent.TargetInstance.Message), "test.vbs") Then

What are we doing here? Well, first of all we’re grabbing the Message property from the TargetInstance object, an object that represents the record that was just written to the event log. We use the LCase function to convert all the characters in that message to lowercase, then use the InStr function to determine whether the value test.vbs can be found anywhere in that message. If test.vbs does not appear anywhere in the message then we simply loop around and wait for the next event to occur.

But let’s assume that the event message does include the text test.vbs. In that case we simply use this block of code to echo back information about that event:

Wscript.Echo Now
Wscript.Echo "Category: " & objEvent.TargetInstance.Category
Wscript.Echo "Event Code: " & objEvent.TargetInstance.EventCode
Wscript.Echo "Message: " & objEvent.TargetInstance.Message
Wscript.Echo "Record Number: " & objEvent.TargetInstance.RecordNumber
Wscript.Echo "Source Name: " & objEvent.TargetInstance.SourceName
Wscript.Echo "Event Type: " & objEvent.TargetInstance.Type

Needless to say, this is pretty standard WMI scripting: we simply echo back the current date and time (using the Now function), then echo back property values such as Category, EventCode, and Message. If you aren’t sure what those property values are used for, well, too bad for you, huh?

No, hey, just kidding. What we meant to say was this: if you’d like more information about event logs and the Win32_NTLogEvent class then you might want to take a look at this section of the Microsoft Windows 2000 Scripting Guide.

Oh: and if you’d like to test today’s script here’s some code that will write an event to the Application event log, an event that – fortuitously enough, includes the term test.vbs:

Const EVENT_SUCCESS = 0

Set objShell = Wscript.CreateObject("Wscript.Shell")
objShell.LogEvent EVENT_SUCCESS, "Test.vbs started."

So what did happen with Peter? Believe it or not, we turned him loose after about 15 minutes; it only took that long for us to remember why we let him go in the first place. To add insult to injury, though, not only did Peter win his freedom, but he somehow also talked the rest of us into baking him cupcakes. But, then again, that’s probably how Peter came to be such a powerful and important Program Manager while the rest of us are, well, Scripting Guys.


How Can I Monitor the Event Logs for the Occurrence of a Specific Event?

Hey, Scripting Guy! How can I monitor the event logs for the occurrence of a specific event?

-- JP

Hey, JP. Why, you use an event log monitoring script, of course. (Yes, it’s hard to believe, but they really do pay us to come up with brilliant answers like that.)

OK, maybe we should be a little more specific: you use an event log monitoring script similar to this one:

strComputer = "."

Set objWMIService = GetObject("winmgmts:{(Security)}\\" & _
        strComputer & "\root\cimv2")

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("Select * from __InstanceCreationEvent Where " _
        & "TargetInstance ISA 'Win32_NTLogEvent' " _
            & "and TargetInstance.EventCode = '0' ")

Do
    Set objLatestEvent = colMonitoredEvents.NextEvent
    Wscript.Echo objLatestEvent.TargetInstance.User
    Wscript.Echo objLatestEvent.TargetInstance.TimeWritten
    Wscript.Echo objLatestEvent.TargetInstance.Message
    Wscript.Echo
Loop

We won’t spend any time in this column discussing the ins and outs of monitoring WMI events; if you’d like more information about event monitoring you might want to view our Scripting Week 2 webcast on the subject. Instead, we’ll just mention that what we’re going to do is create a script that “subscribes” to a WMI event log event. Each time an event with a specific EventCode (in this case 0) is written to one of the event logs, our script will be notified and will report back values for the User, TimeWritten, and Message properties. The script will then slip back into suspended animation and patiently wait for the next event 0 to occur.

By the way, we chose event 0 because that’s the event code for Windows Script Host events. That means you can use a script like this one to write an event 0 to the Application log and thus test your monitoring script to ensure that it works:

Const EVENT_SUCCESS = 0

Set objShell = Wscript.CreateObject("Wscript.Shell")
objShell.LogEvent EVENT_SUCCESS, "Event written to an event log using a script."

As for the monitoring script, we begin by connecting to the WMI service. You might notice that when connecting to the WMI service we include the {(Security)} parameter. This allows us to subscribe to events written to all the event logs, including the Security log. Without this parameter we would receive events written from all the event logs except Security.

Next we use the ExecNotificationQuery method to register for event log events. Our query itself looks like this:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("Select * from __InstanceCreationEvent Where " _
        & "TargetInstance ISA 'Win32_NTLogEvent' " _
            & "and TargetInstance.EventCode = '0' "

What we’re saying here is this: Show us all new instances of the __InstanceCreationEvent class, provided that the new instance happens to be a new entry to the event log (Win32_NTLogEvent) and the new entry has an EventCode of 0. If we wanted to monitor for different events (say, an event with the EventCode 528) all we’d have to do is modify our query accordingly:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("Select * from __InstanceCreationEvent Where " _
        & "TargetInstance ISA 'Win32_NTLogEvent' " _
            & "and TargetInstance.EventCode = '528' "

After that we set up a Do Loop with no exit condition (e.g., no Do Until x = 1 kind of thing). This allows us to monitor events forever and ever: the script will continue to monitor until we reboot the computer or terminate the process under which the script runs. (Incidentally, you should run this script in a command window under CScript. If you run it under WScript, you’ll have to click a bunch of message boxes any time an event 0 is written to the event log.)

We then use this line of code to tell the script to sit there and wait for the next event to occur:

Set objLatestEvent = colMonitoredEvents.NextEvent

When a new event 0 is written to one of the event logs an exact copy of that event will be made available to our script; this replica object is known as the TargetInstance. At that point all we do is echo a few property values of this TargetInstance and then loop around and wait for the next event.

In other words, to monitor the event logs for the occurrence of a specific event just use an event monitoring script. (If only we’d said that in the first place….)


How Can I Retrieve Information About the Latest Event Added to an Event Log?

Hey, Scripting Guy! How can I retrieve information about the latest event added to an event log?

-- HG

Hey, HG. You know, it seems like only yesterday that someone was asking us how they could find the oldest event in an event log. And we remember how we told them that – oh, right: it was only yesterday, wasn’t it? Well, then we won’t waste any time reminiscing about the good old days. (Although yesterday was a pretty good day, all things considered.)

Instead, let’s get down to business. As we noted yesterday, each time an event is added to an event log it gets assigned a sequential record number: if you have 1000 records in an event log, the next event written to that log will be assigned record number 1001. That means that the event with the largest record number will also be the latest event written to the event log. To retrieve information about the latest event added to an event log all we have to do is determine which event has the largest record number.

But, as we also noted yesterday (boy, we worked hard yesterday, didn’t we?), you can’t write a WQL query that automatically goes out and retrieves the event with the largest record number; instead, we need to specify that record number in our query. And that’s a problem: how the heck are we supposed to know the largest record number currently in use in a given event log?

Did we say that was a problem? Sorry; turns out that’s not a problem at all. Let’s show you a script that retrieves date-time information about the most recent event added to an event log, and then we’ll let you in on the secret:

strComputer = "."
Set objWMIService = GetObject("winmgmts:{(Security)}\\" & _
        strComputer & "\root\cimv2")

Set colLogFiles = objWMIService.ExecQuery _
    ("Select * from Win32_NTEventLogFile where LogFileName='System'")

For Each objLogFile in colLogFiles
    intTotal = objLogFile.NumberOfRecords
Next

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where Logfile = 'System' " & _
        "AND RecordNumber = " & intTotal)

For Each objEvent in colEvents
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
Next

Here’s how this all works. We begin by connecting to the WMI service on the local computer. We then use this query to return information about the System event log:

Set colLogFiles = objWMIService.ExecQuery _
    ("Select * from Win32_NTEventLogFile where LogFileName='System'")

Notice that we’re using the Win32_NTEventLogFile class here; that’s because we want information about the properties of the event log itself rather than information about any of the individual events. In particular, we want to know the value of the NumberOfRecords property; as you might have guessed, this tells us the number of records in the event log. To get at that number we use this block of code to walk through our collection of event logs (because we specified the System event log there will be only one item in the collection) and store the value of the NumberOfRecords in a variable named intTotal:

For Each objLogFile in colLogFiles
    intTotal = objLogFile.NumberOfRecords
Next

Why do we do that? Well, suppose we have 1001 records in the System event log. That means the last event written to the log will have record number 1001. That’s how we’re able to know the largest record number currently in use in a given event log: the last event written will have the same record number as the number of events in the log.

Think about the math for a second, and you’ll see how that works. If we have one event in the event log, that event will have a record number of 1. Suppose we add a second event. OK, now we have 2 events, and the second event will have a record number of 2. Because 2 tends to equal 2, this approach works.

The rest is pretty straightforward; following the same methodology used yesterday, we use these lines of code to return and echo the value of the TimeWritten property for the most recent event in the System event log:

Set colEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent Where Logfile = 'System' " & _
        "AND RecordNumber = " & intTotal)

For Each objEvent in colEvents
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
Next

The secret here lies in the WQL query, where we use our variable intTotal to retrieve the record with the largest record number: RecordNumber = " & intTotal. That’s all we have to do to get back information about the last event to be written to the System event log.

Now, where were we? Oh, yes. It was the summer of 1907, and we had just received a telegram asking us how to retrieve the oldest event in an event log. Well, sir, we hunkered down and thought about that a spell, then we ….


How Can I Retrieve Information From My Event Logs Regarding Unsuccessful Logons?

Hey, Scripting Guy! How can I scan the event logs of my servers and return only information about unsuccessful logons?

-- LC

Hey, LC. We’ll assume that you have enabled security auditing on your servers. If you haven’t, that’s step one. What you’ll want to do is – at a minimum – audit for logon event failures. That way every time someone tries – and fails – to log on to the computer, an event with a specific event code will be written to the Security event log. For member servers and workstations, the event code is 529; for domain controllers, the event code is 675. For our example, we’ll use event code 529, simply because you referenced “servers” in your question rather than “domain controllers.”

Note. There might be other event codes of interest to you as well; needless to say, the Scripting Guys are not security experts. For the sake of simplicity, though, we’ll focus on 529 and 675, both of which represent “unknown user name or bad password” events. However, its’ easy enough to modify the WQL query we’ll be using to check for additional event codes.

Budding crime scene investigators out there might have already spotted an important clue: unsuccessful logons are reported in the Security event log, and each one is branded with the same event code: 529. That suggests a solution to our problem: to get back information about unsuccessful logons, all we need to do is query the Security event log for all events that have an event code of 529.

In other words:

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

Set colEvents = objWMIService.ExecQuery _
        ("Select * from Win32_NTLogEvent Where Logfile = 'Security' and " _
            & "EventCode = '529'")

For Each objEvent in colEvents
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Computer Name: " & objEvent.ComputerName
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
    Wscript.Echo "Event Type: " & objEvent.Type
    Wscript.Echo "User: " & objEvent.User
Next

Elementary, my dear Watson. The only thing out of the ordinary occurs when we connect to the WMI service. To do that, we must include the (Security) privilege (enclosed in square braces), like so:

Set objWMIService = GetObject("winmgmts:{(Security)}\\" & strComputer & "\root\cimv2")

You need to include this privilege in your script any time you want to access the Security event log. On top of that you (or at least your user account) must already possess the right to access the Security event log; including the privilege in the script won’t magically bestow this right on you. On the other hand, leaving the privilege out of the script will prevent you from accessing the Security event log, regardless of any rights, privileges, and permissions your user account might have.

After making the connection we simply select all instances of the Win32_NTLogEvent class where the Logfile is equal to Security and the EventCode is equal to 529. That returns a collection of unsuccessful logon events, which we proceed to echo back to the screen.

This particular script returns a list of all the unsuccessful logon events. It’s possible that you might want to retrieve those events only for a specified time period (for example, only unsuccessful logons from yesterday or for last week). Likewise, you might want to set up a monitoring system that would allow for instant notification any time such an event occurred. That sort of things lies a bit outside the scope of this column, but for more information and for sample scripts you might take a look at the Logs chapter in the Microsoft Windows 2000 Scripting Guide.


How Can I Retrieve Just Audit Failures, Warnings, and Errors from My Event Logs?

Hey, Scripting Guy! How can I retrieve just audit failures, warnings, and errors from my event logs?

-- OG

Hey, OG. You know, just for the heck of it, we decided to check the event logs on one of our computers to see whether this was a task worth doing. In the Security event log on this machine we had 42,815 events; of those, just 286 were failure events. If we assume that the failure events are the events we really care about then we have two choices here: we can either wade through 42,815 events, trying to pick out the relatively few failure events, or we can write a script that returns only failure events. Even for the Scripting Guys that wasn’t a very tough decision to make.

Here’s a script that returns just audit failures, warnings, and errors from all your event logs:

strComputer = "."

Set objWMIService = GetObject("winmgmts:" _
    & "{(Security)}\\" & strComputer & "\root\cimv2")

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where EventType <> 4 AND EventType <> 8")

For Each objEvent in colLoggedEvents
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
    Wscript.Echo "Event Type: " & objEvent.EventType
Next

Having shown you the script we should point out that this one runs only on Windows XP and Windows Server 2003. But don’t panic: we’ll show you a modified version in a minute or two that will work on Windows 2000.

Hey, no need to thank us; that’s what we’re here for.

To begin with, we need to bind to the WMI service on the computer in question (in this sample script, that’s the local computer). You might notice that, when making this connection, we included the (Security) privilege in the moniker string:

Set objWMIService = GetObject("winmgmts:" _
    & "{(Security)}\\" & strComputer & "\root\cimv2")

Is that important? Well, one of the things we want to retrieve is audit failures. Audit failures are recorded in the Security event log; if you don’t include the (Security) privilege you will not be able to retrieve events from the Security event log. Yes, we know you’re an administrator on the machine. Doesn’t matter: the (Security) privilege is still required in order to retrieve items from the Security event log.

In other words, yes, it’s important.

Next we issue the following WQL query, which limits the records retrieved to audit failures, warnings, and errors:

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where EventType <> 4 AND EventType <> 8")

What do you mean it’s not obvious how the query does this? It – oh, right: it’s not very obvious, is it? Well, as it turns out, the EventType property indicates the type of record being written to the event log; furthermore, EventType will always be one of the following values:

Value | Meaning
1 | Error
2 | Warning
4 | Information
8 | Security audit success
16 | Security audit failure

In our query, we asked for all the events where the EventType did not equal 4 (information events) and where the EventType does not equal 8 (security audit success). That filters out information and security audit success events; for example, an information event equals 4, which causes it to be excluded from the returned data. In turn, that leaves just three event types to be retrieved: audit failures, warnings, and errors.

Coincidentally, those just happen to be the three event types you’re interested in.

The rest of the script is nothing more than a For Each loop that echoes back pertinent information for each event. As we are wont to say: problem solved.

Well, unless you’re running on Windows 2000, that is. That’s because the EventType property isn’t found on Windows 2000; instead, the Win32_NTLogEvent class uses the Type property. On Windows 2000, Type will be one of the following string values:

Value | Meaning
error | Error
warning | Warning
information | Information
audit success | Security audit success
audit failure | Security audit failure

To get our script to work on Windows 2000, we need to filter on the Type property, eliminating records of type information and type audit success. Our revised WQL query looks like this:

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where Type <> 'information' AND Type <> 'audit success'")

And the revised script looks something like this:

strComputer = "."

Set objWMIService = GetObject("winmgmts:" _
    & "{(Security)}\\" & strComputer & "\root\cimv2")

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where Type <> 'information' AND Type <> 'audit success'")

For Each objEvent in colLoggedEvents
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
    Wscript.Echo "Event Type: " & objEvent.Type
Next

There you go.

Incidentally, the fact that we had 42,815 events doesn’t mean we don’t back up and clear our event logs on a regular basis. Instead, we let these records accumulate so that we’d … have a better example to use for today’s column. Now that the column is finished, we’ll probably back up and clear the event log, just like you’re supposed to.

No, not today, mind you. But soon, very soon ….


How Can I Return Only the Last Record Written to an Event Log?

Hey, Scripting Guy! How can I read only the last record written to an event log? In other words, what is the WMI equivalent to the SQL statement Select Top 1?

-- KM

Hey, KM. Well, as it turns out WMI doesn’t have an equivalent to the Select Top command; for better or worse, the WMI Query Language (WQL) has only a small subset of the commands found in SQL. That doesn’t mean we can’t return only the last record written to an event log, it just means we have to be a bit sneaky about it.

To get just the last record we need to do two things. To begin with, we need to figure out how many records are in an event log. That’s important information, because events are numbered sequentially when they are added to an event log; in other words, the first event written to an event log is record 1, the second is record 2, etc. If there are 4,912 events in an event log, then the very last record written to the log has to be record number 4912. After we know the total number of records, we can then write a query that returns only events with a record number equal to the total number of records. That record is the last record written to the log.

Here’s a simple little script that determines the total number of records in the Application event log, and stores that value in the variable intRecords:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

Set objInstalledLogFiles = objWMIService.ExecQuery _
    ("Select * from Win32_NTEventLogFile Where LogFileName = 'Application'")

For Each objLogfile in objInstalledLogFiles
    intRecords = objLogFile.NumberOfRecords
Next

Now we need a query that returns all events from the Application event log where the RecordNumber is equal to value of intRecords (and there will only be one such record, seeing as how record numbers are unique). Here’s a query that does just that:

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where Logfile = 'Application' AND " & _
        "RecordNumber = " & intRecords)

All that’s left now is to put these two script snippets together, and then add a For Each loop in which we echo the properties of this record:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

Set objInstalledLogFiles = objWMIService.ExecQuery _
    ("Select * from Win32_NTEventLogFile Where LogFileName = 'Application'")

For Each objLogfile in objInstalledLogFiles
    intRecords = objLogFile.NumberOfRecords
Next

Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * From Win32_NTLogEvent Where Logfile = 'Application' AND " & _
        "RecordNumber = " & intRecords)

For Each objEvent in colLoggedEvents
    Wscript.Echo "Category: " & objEvent.Category
    Wscript.Echo "Computer Name: " & objEvent.ComputerName
    Wscript.Echo "Event Code: " & objEvent.EventCode
    Wscript.Echo "Message: " & objEvent.Message
    Wscript.Echo "Record Number: " & objEvent.RecordNumber
    Wscript.Echo "Source Name: " & objEvent.SourceName
    Wscript.Echo "Time Written: " & objEvent.TimeWritten
    Wscript.Echo "Event Type: " & objEvent.Type
    Wscript.Echo "User: " & objEvent.User
Next

That should do the trick.


How Can I Automatically Open New Files Added to a Folder?

Hey, Scripting Guy! How can I monitor a folder for new files and then automatically open each new file that gets added to that folder?

-- CA

Hey, CA. You know, today is Valentine’s Day in the US, which means that right now most American men are saying to themselves, “What? Today is Valentine’s Day? Uh-oh ….” We’re not sure how many other countries around the world celebrate Valentine’s Day, but in the US today is the day when you’re supposed to lavish your significant other with jewelry, flowers, chocolates, heartfelt cards and other expensive gifts, all as a way to say thank you for the … wonderful … things they do for you.

Note. In case you’re wondering, the answer is no; instead, the Scripting Guy who writes this column and his son are going to a college basketball game tonight. On the other hand, what greater gift could this Scripting Guy give someone than to not hang around with them?

If you’re not familiar with the occasion, Valentine’s Day is named after St. Valentine, although which St. Valentine remains a matter of debate. (Apparently, in the early days of Christianity there were at least three St. Valentines.) According to legend, sometime in the third century a Roman emperor barred young men from getting married; he believed that unmarried men would make better soldiers. (Today most scholars disagree with that, pointing out that no one has more fighting experience than a married man.) As the story goes, St. Valentine was a priest who elected to defy the emperor, continuing to perform marriages in secret. When he was caught, he was martyred for his faith, although not before sending a note to a young lady signed, “From your Valentine.” The rest is history.

Or legend. Take your pick.

At any rate, if you’re like most American men you haven’t gotten that certain special someone anything for Valentine’s Day. Might we suggest a script that monitors a folder for new files and then automatically opens each new file that gets added to that folder:

Set objShell = CreateObject("Wscript.Shell")

strComputer = "."

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

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

Do
    Set objLatestEvent = colMonitoredEvents.NextEvent
    strNewFile = objLatestEvent.TargetInstance.PartComponent
    arrNewFile = Split(strNewFile, "=")
    strFileName = arrNewFile(1)
    strFileName = Replace(strFileName, "\\", "\")
    strFileName = Replace(strFileName, Chr(34), "")
    objShell.Run("notepad.exe " & strFileName)
Loop

And no, you don’t need glasses: the script really does look like that. But maybe it will help if we explain why it looks like that.

We start out simple enough, creating an instance of the Wscript.Shell object, the object we’ll use to open a file. We then do something equally straightforward, connecting to the WMI service on the local computer. (Although, as usual, this script works equally well against a remote machine.)

We then encounter this line of code:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE " _
        & "TargetInstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

We won’t explain this monstrosity in minute detail today; for that you might take a peek at the Scripting Guys webcast An Ounce of Prevention. In brief, this query is designed to check the folder C:\Scripts every 10 seconds and see if any new files have been added to that folder. How do we know that this query checks for new files? Well, we know that it’s looking for new items because it’s querying for instances of the __InstanceCreationEvent class. And we know that we’re looking for files because we’re specifying that we’re only interested in new members of the __InstanceCreationEvent class if they happen to also be members of the CIMDirectoryContainsFile class.

And that’s not all. The 10 second interval between checks is what the WITHIN 10 is all about; if that value is too long or too short then just adjust the time accordingly. Last but surely not least, we know that we’re looking at the C:\Scripts folder because of this:

'Win32_Directory.Name=""c:\\\\scripts""'

For better or worse, all those \’s and all those single and double quote marks are required.

Got all that? Well, to be honest, we don’t, either. But if you use the preceding example as a template and change only those items (such as the folder name) that you need to change you should be fine.

We’d like to tell you that all the craziness is over, but we can’t do that; there’s still a bit more craziness to come. Our next step is to set up a Do loop that’s designed to run forever; you might note that there’s no exit criteria like “Do Until x = 1” or “Do While x < 10”:

Do

Inside that loop the first thing we do is issue a command that causes the script to “block.” That simply means that the script will sit on this line of code until our query notifies us that an event of interest has occurred:

Set objLatestEvent = colMonitoredEvents.NextEvent

Needless to say, thanks to the criteria specified in our query, such notification can only mean one thing: a new file has been added to C:\Scripts.

Now the craziness returns. Each time a new file is added to the folder we get notified; in addition, we can take a peek at the PartComponent property to determine the file path:

strNewFile = objLatestEvent.TargetInstance.PartComponent

That’s the good news. The bad news is that the PartComponent looks something like this:

\\ATL-WS-01\root\cimv2:CIM_DataFile.Name="c:\\scripts\\New Text Document.txt"

Yuck. But don’t despair; we can extract the path from that. Guaranteed.

To do that, we first use the Split function to split the PartComponent on the equals sign. That’s going to give us an array consisting of two items:

· \\ATL-WS-01\root\cimv2:CIM_DataFile.Name

· "c:\\scripts\\New Text Document.txt"

As you can see, the path we need is – more or less – contained in the second item in the array. (Which, arrays being what they are, the second item in the array is assigned the index number 1.) With that in mind, we use this line of code to put this item into a variable named strFileName:

strFileName = arrNewFile(1)

Now we just need to clean things up a bit. First, we use the Replace function to replace any double \ characters with a single \, like so:

strFileName = Replace(strFileName, "\\", "\")

That’s leaves strNewFile equal to this: "c:\scripts\New Text Document.txt". That right there is our file path. Or at least it will be after we use the Replace function to remove the double quote marks (which have an ASCII value of 34):

strFileName = Replace(strFileName, Chr(34), "")

And now that strFileName contains the real path to the new file we can use the Run method and open the new file in Notepad:

objShell.Run("notepad.exe " & strFileName)

Good question: do you have to open new files in Notepad? No. However, you do need to specify an executable file; we can’t just ask the Run method to directly open the file C:\Scripts\New Text Document.txt. We took the easy way out here and assumed that all the new files that might be added to C:\Scripts can be opened with Notepad. If that’s not the case then you’ll need to add some additional code to the script, code that checks the file extension and then runs the appropriate application (e.g., it calls Excel if we happen to be dealing with a .xls file). Most likely you can figure that part out on your own. If not, we’ll cover it in a future column.

With any luck that will not only answer your question, CA, but also take care of your Valentine’s Day shopping. If not, maybe we could be so presumptuous as to suggest a Dr. Scripto Bobblehead doll for that certain special someone. Granted, you can’t actually buy a Dr. Scripto Bobblehead doll, but there’s still time to win one as part of the 2007 Winter Scripting Games. All you have to do is enter an event and you’re automatically entered in a drawing to win one of 250 Dr. Scripto bobbleheads.

Sure, it doesn’t sound like much. But what man or woman could say no to a face like this:


How Can I Be Notified Any Time a Network Cable Gets Unplugged?

Hey, Scripting Guy! Is there a way I can be notified any time a network cable gets unplugged on a computer?

-- DD

Hey, DD. A lot of people, when growing up, had a grandmother or an aunt or someone somewhere who had a house with a big attic, an attic overflowing with stuff. Admittedly, most of the time you had no idea what you were looking at when you wandered through there, but every now and then you came across something really cool and really useful. Every visit to the attic was like a treasure hunt, and you never knew what you might find there.

The WMI equivalent of Grandma’s attic is the root\wmi namespace. There’s quite a lot of stuff in there – 374 classes on our Windows XP test machine – but a lot of the classes either are not implemented or don’t return any data. Every now and then, however, you stumble upon a class that does something really useful, something like, say, notifying you any time a network connection is lost. Just like a treasure hunt.

As it turns out, the MSNdis_StatusMediaDisconnect class – a WMI event class – can monitor a computer and notify you any time a network connection has been lost (whether that’s because of a network cable being pulled or some other reason). As we often do, let’s show you the script and then explain how it works:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\wmi")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("Select * from MSNdis_StatusMediaDisconnect")

Do While True
    Set strLatestEvent = colMonitoredEvents.NextEvent
    Wscript.Echo "A network connection has been lost:"
    WScript.Echo strLatestEvent.InstanceName, Now
    Wscript.Echo
Loop

If you have at least some experience with WMI event scripts, this one should look very familiar. (If you don’t have much experience with WMI event scripts, you might want to check out this Scripting Week 2 webcast.) We begin by connecting to the WMI service; note that we connect to root\wmi rather than root\cimv2. We then use ExecNotificationQuery to request notification any time a new instance of the MSNdis_StatusMediaDisconnect class is created. As you might have guessed, a new instance of this class is created any time a network connection is lost.

We then set up an infinite loop; by design this script will continually monitor the computer for new instances of the MSNdis_StatusMediaDisconnect class. (In fact, the only way to get it to stop monitoring is to terminate the script or the script process.) The script then pauses on this line of code, waiting for the next network disconnection event:

Set strLatestEvent = colMonitoredEvents.NextEvent

So what happens if we pull the cable and we disconnect the computer from the network? In that case a network disconnection event will occur, a new instance of the MSNdis_StatusMediaDisconnect class will be created, and the script will issue a notification that looks something like this:

A network connection has been lost:
Microsoft Wireless Notebook Adapter MN-720 3/172005 8:28:34 AM

Cool, huh? The script will then loop around and wait for the next network disconnection to occur. Should there be another such disconnection then another notification will be issued.

Note. Of course, you aren’t limited to echoing messages to the screen. You could have your script do pretty much anything in the event of a network disconnection; we just wanted to keep things simple.

Incidentally, there’s a similar class – MSNdis_StatusMediaConnect – that can notify you any time a network connection has been made. Here’s a sample script that does just that:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\wmi")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("Select * from MSNdis_StatusMediaConnect")

Do While True
    Set strLatestEvent = colMonitoredEvents.NextEvent
    Wscript.Echo "A network connection has been made:"
    WScript.Echo strLatestEvent.InstanceName, Now
    Wscript.Echo
Loop

Unfortunately we don’t know of an easy way to combine the two scripts into one (that is, a single script that could notify you of both network connections and network disconnections). There is at least one way this can be done, but there are some security concerns with that approach so we can’t recommend it. For the time being, then, you’ll need to run two separate scripts if you’d like to monitor for both kinds of events. But we’ll keep rummaging through the WMI attic; who knows what else we might find in there.


How Can I Be Notified Any Time a Service Goes Down?

Hey, Scripting Guy! How can I be notified any time a service goes down?

-- RN

Hey, RN. As usual, there are a couple different ways you can do this, but perhaps the simplest approach is to create a script that monitors WMI events. We don’t have time to do a detailed explanation of WMI events in this column, but the basic idea is that you can ask WMI to notify you when certain things happen. For example, you might request notification when hard disk space drops below a specified amount, or when CPU consistently spikes above a pre-determined level. Likewise, you can request notification any time a service changes state; that is, any time a service that was running is stopped, or any time a service that was stopped is re-started. If a service goes down, you can ask WMI to tell you about it.

The following script checks for changes in service status; every 30 seconds it looks to see if any services have changed state. The script is set up in an endless loop, which means that, once started, it will continue to run forever and ever, or at least until you reboot the machine or terminate the script process.

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService. _
    ExecNotificationQuery("Select * from __InstanceModificationEvent " _
        & "within 30 where TargetInstance isa 'Win32_Service'")
i = 0
Do While i = 0
    Set objService = colServices.NextEvent
    If objService.TargetInstance.State <> _
        objService.PreviousInstance.State Then
        Wscript.Echo objService.TargetInstance.Name _
            &  " is " & objService.TargetInstance.State _
                & ". The service previously was " & objService.PreviousInstance.State & "."
    End If
Loop

As you can see, pretty easy. Any time a service is modified in any way, an instance of the __InstanceModificationEvent class is created. The script compares the current state of the modified service (TargetInstance.State) with the previous state of the service (PreviousInstance.State). What if the two are the same; that is, what if the service is currently running, and was previously running? In that case, we just ignore the event; that means that some other property of the service was modified. (For example, you might have changed the service password, or maybe switched the service from Manual start to Autostart.) If the two states differ – that is, if the service used to be running but now is stopped – a message box pops up telling us the name of the service, its current state, and its previous state. Cool, huh?

WMI events can be incredibly useful to system administrators; for more information check out this Tales from the Script column.


How Can I Check Every 30 Minutes to See If a File Has Been Modified?

Hey, Scripting Guy! How can I monitor an application’s log file and issue an alert if the timestamp on that file hasn’t changed in the last 30 minutes?

-- JA

Hey, JA. You know that’s a very interesting question, and one that – say, wait a second. You’re not JA from Stanwood, WA are you? Are you sure about that? Well, OK; we’ll take your word for it, and we’ll try to answer your question. But if you are from Stanwood, WA ….

Not that the Scripting Guy who writes this column has anything against Stanwood, WA, mind you; it’s just that the Scripting Guy who writes this column has something against Stanwood, WA. The other day this Scripting Guy found himself with a little time to kill in Stanwood, the Scripting Son having to arrive in town some 90 minutes prior to a doubleheader with the Skagit Sox. Being behind on his work, and having snuck out of the office at noon in order to get to Stanwood on time, the Scripting Guy who writes this column was glad to have 90 minutes to kill: that gave him just enough time to go get some ice cream before the game started.

Or so he thought. But here’s a tip for you: if you ever find yourself in Stanwood, WA, don’t bother looking for ice cream; they don’t have any. With seemingly-plenty of time on his hands the Scripting Guy who writes this column walked over to a nearby shopping center, one chockfull of stores and restaurants, none of which actually sold ice cream. (You know, real ice cream, like a hot fudge sundae.) At one point the Scripting Guy who writes this column saw a huge sign proclaiming Ice Cream; as he rounded the corner, however, he discovered that the place was locked up tight as a drum.

Note. So did the ice cream shop gone out of business, or did they simply lock the doors and pull the blinds when they saw this Scripting Guy headed their way? Good question ….

Eventually the Scripting Guy who writes this column gave up on finding an old-fashioned hot fudge sundae and decided to get some ice cream at McDonald’s instead. “Oh, ice cream,” said the girl behind the counter. “We’re all out of ice cream ….”

Right ….

In the end, the Scripting Guy who writes this column never did get his ice cream, and the Scripting Son and his team went on to lose both ends of the doubleheader. Coincidence? We think not.

Oh, well; in the great good news/bad news scheme of life, that’s the bad news. The good news? With no hot fudge sundae to occupy his time, the Scripting Guy who writes this column was able to do something he hadn’t really planned on doing: write this column.

As you might expect, there are a number of different ways we could have approached the problem of monitoring a log file and issuing an alert if the timestamp on that file hasn’t changed in the last 30 minutes. After carefully weighing all these alternative approaches can we assume that the Scripting Guys chose the best possible method for carrying out this task? Heavens no. However, you can assume that the Scripting Guys chose the easiest possible method for carrying out this task:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_Datafile Where Name = 'C:\\Scripts\\Application.log'")

For Each objFile in colFiles
    strOriginalTimestamp = objFile.LastModified
Next

Wscript.Echo "Monitoring application log file: " & Now
Wscript.Echo

Do While True
    Wscript.Sleep 1800000
    Set colFiles = objWMIService.ExecQuery _
        ("Select * from CIM_Datafile Where Name = 'C:\\Scripts\\Application.log'")

    For Each objFile in colFiles
        strLatestTimestamp = objFile.LastModified
    Next

    If strLatestTimestamp <> strOriginalTimestamp Then
        strOriginalTimestamp = strLatestTimeStamp
    Else
        Wscript.Echo "ALERT: " & Now
        Wscript.Echo "The application log file has not been modified in the last 30 minutes."
        Wscript.Echo
        strOriginalTimestamp = strLatestTimeStamp
    End If
Loop

Let’s see if we can explain how this works. To begin with, we connect to the WMI service on the local computer. This was one of the first decisions we had to make: do we use WMI to carry out this task, or do we use some alternate technology, such as the FileSystemObject? We opted to go with WMI, and for one simple reason: this enables us to write a script that can monitor a log file on a remote computer just as easily as it can monitor a log file on the local computer. Need to perform this task against a log file on the remote computer atl-fs-01? No problem; just assign the name of that computer to the variable strComputer:

strComputer = "atl-fs-01"

Our next step is to determine the last time the file was changed; we need to know the current value of the file’s LastModified property in order to tell whether or not that value (and thus the file itself) has changed in the past 30 minutes. With that in mind, we use this line of code to select all the instances of the CIM_Datafile class that have a Name (path) equal to C:\\Scripts\\Application.log:

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_Datafile Where Name = 'C:\\Scripts\\Application.log'")

And yes, that is C:\\Scripts\\. Because the \ is a reserved character in WMI, any \ that appears in a WQL query needs to be “escaped;” that is, we need to preface it with another \.

Admittedly that’s a little strange. But nowhere near as strange as a town completely devoid of ice cream.

After making the connection we set up a For Each loop to loop through the collection of files returned by the query. (Needless to say, there can be only one such file in the collection.) We then use this line of code to assign the value of the LastModified property to a variable named strOriginalTimestamp:

strOriginalTimestamp = objFile.LastModified

All of that is required just to give us a starting point; now we need to do some real work. To that end, we set up a Do While loop designed to run forever and ever (or at least for as long as True is equal to True):

Do While True

What’s the first thing we do inside that loop? In a sense, nothing at all; the first thing we do is call the Sleep method and pause the script for 30 minutes:

Wscript.Sleep 1800000

Note. In case you’re wondering, the 1800000 represents 1,800,000 milliseconds. There are 1,000 milliseconds in a second; 60,000 milliseconds in a minute; and 1,800,000 milliseconds in 30 minutes (1,000 x 60 x 30).

When those 30 minutes are up we check the current value of the log’s LastModified property; to do that, we reconnect to the file, then store the value of the LastModified property in a variable named strLatestTimestamp:

strLatestTimestamp = objFile.LastModified

That brings us to this line of code:

If strLatestTimestamp <> strOriginalTimestamp Then

What are we doing here? Well, here we’re comparing the original timestamp with the current timestamp. Suppose the two timestamps are different; what does that mean? That can only man one thing: the file must have been modified sometime during the past 30 minutes. Therefore, we simply assign the current timestamp (strLatestTimestamp) to the variable strOriginalTimestamp, loop around, pause the script for another 30 minutes, then perform this check again.

That was easy. Now, what if the two timestamps are identical? Again, there’s only one possible explanation: if the two timestamps are identical that means the file must not have been modified in the past 30 minutes. In that case, we use the following lines of code to echo a message back to the command window (and yes, like most monitoring scripts you really want to run this one in a command window under the CScript script host):

Wscript.Echo "ALERT: " & Now
Wscript.Echo "The application log file has not been modified in the last 30 minutes."
Wscript.Echo

That’s going to result in a message similar to this appearing onscreen:

ALERT: 7/20/2007 1:26:35 PM
The application log file has not been modified in the last 30 minutes.

We then assign the current timestamp to the variable strOriginalTimestamp, loop around, pause the script for another 30 minutes, then perform the check again. This process continues until the end of time, or until you terminate the script or close the command window.

Whichever comes first. And we’re guessing all of those things will happen long before they get any ice cream in Stanwood.

Needless to say, you aren’t limited to echoing back an alert; if you want to you can send an email, write an event to the event log, or do pretty much anything else you want. We’ll leave that up to you.

We hope that answers your question, JA. If it turns out that you really are JA from Stanwood, WA then no doubt you have another question as well: you’re wondering if the Scripting Guy who writes this column will hold it against you just because he couldn’t find any ice cream in your town. Well, JA, the answer to your unasked question is this: you bet he does. But don’t worry; just send a hot fudge sundae to the following address and we’ll call it good:

The Scripting Guy Who Writes That Column
Microsoft Corporation
Building 42/4039
Redmond, WA 98052

P.S. Extra whipped cream, and no maraschino cherry. Thanks!


How Can I Determine the System Time on a Computer?

Hey, Scripting Guy! Is there any way to retrieve the system time as configured on a remote computer?

-- JJ

Hey, JJ. There are at least two ways to do this, both using WMI. If you’re running Windows XP or Windows Server 2003, you can use the WMI class Win32_LocalTime. As you can see, that makes it pretty easy to get the system time: you simply connect to the remote computer, query the Win32_LocalTime class, and then echo the desired time values. In other words:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_LocalTime")

For Each objItem in colItems
    Wscript.Echo "Month: " & objItem.Month
    Wscript.Echo "Day: " & objItem.Day
    Wscript.Echo "Year: " & objItem.Year
    Wscript.Echo "Hour: " & objItem.Hour
    Wscript.Echo "Minute: " & objItem.Minute
    Wscript.Echo "Second: " & objItem.Second
Next

If you want to get fancy, you can combine all these separate values into a standard date-time string, something that looks like this: 1/26/2005 8:47:04 AM. Here’s a script that can do just that:

On Error Resume Next

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_LocalTime")

For Each objItem in colItems
    intMonth = objItem.Month
    intDay = objItem.Day
    intYear = objItem.Year

    dtmDate = intMonth & "/" & intDay & "/" & intYear

    intHour = objItem.Hour
    If intHour < 12 Then
        strAMPM = "AM"
    Else
        intHour = intHour - 12
        strAMPM = "PM"
    End If

    intMinutes = objItem.Minute
    If intMinutes < 10 Then
        intMinutes = "0" & intMinutes
    End If

    intSeconds = objItem.Second
    If intSeconds < 10 Then
        intSeconds = "0" & intSeconds
    End If

    dtmTime = intHour & ":" & intMinutes & ":" & intSeconds & " " & strAMPM

    Wscript.Echo dtmDate & " " & dtmTime
Next

We won’t explain each line of code in the script, but for the most part all we’re doing is combining values. In fact, we only do two things even slightly tricky. First we check the hour to see if it’s less than 12; if it is, we add AM to the end of the string; if it’s not, we add a PM to the end of the string. And, yes, this means that the hour is returned in 24-hour format; thus 3:00 in the afternoon comes back as 15:00. That’s why, when checking the hour, we also subtract 12 if the hour is greater than 12. That way, 15:00 gets converted to 3:00 PM.

For both the minutes and seconds, we check to see if the returned value is less than 10. Why? Well suppose we have 8 hours, 6 minutes, and 5 seconds. If we combine these values as-is, our time will be 8:6:5. That doesn’t really look like a time, does it? Therefore, if the minutes or the seconds are less than 10, we add a leading zero. When we do that, we’re now combining 8, 06, and 05, and we get a time that looks like this: 08:06:05. Much better.

Of course, that’s all well and good as long as you’re using Windows XP or Windows Server 2003. But what about people using Windows 2000? Well, that’s OK; you can still get this same information, just not in as straightforward a fashion.

As we noted earlier, the Win32_LocalTime class doesn’t exist on Windows 2000. However, you can still get the system time by querying the Win32_OperatingSystem class and checking the value of the LocalDateTime property. The only problem with this approach is that the date and time come back in UTC (Universal Time Coordinate) format. Consequently, you get a date and time that looks like this: 20050127080605.000000+480. Yuck.

We won’t go into the details surrounding the UTC format; if you’re interested you might want to check out this section of the Microsoft Windows 2000 Scripting Guide. However, we will show you a script that grabs the LocalDateTime value and converts it to a slightly-more readable format:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer &
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from
Win32_OperatingSystem")

For Each objItem in colItems
dtmLocalTime = objItem.LocalDateTime
dtmMonth = Mid(dtmLocalTime, 5, 2)
dtmDay = Mid(dtmLocalTime, 7, 2)
dtmYear = Left(dtmLocalTime, 4)
dtmHour = Mid(dtmLocalTime, 9, 2)
dtmMinutes = Mid(dtmLocalTime, 11, 2)
dtmSeconds = Mid(dtmLocalTime, 13, 2)
Next

dtmNewDate = dtmMonth & "/" & dtmDay & "/"
& dtmYear
dtmNewTime =  dtmHour & ":" & dtmMinutes &
":" & dtmSeconds

Wscript.Echo dtmNewDate & " " & dtmNewTime

We haven’t done much in the way of formatting here; for example, we don’t convert 15:00 to 3:00, nor do we indicate whether this is AM or PM. But you can do that yourself, using the same approach we took with the Win32_LocalTime.


How Can I Determine the Uptime for a Server?

Hey, Scripting Guy! How can I determine the uptime for a server?

-- LF

Hey, LF. This is an easy one. After all, we’re assuming you’re talking about a Windows server, and Windows servers never go down, right? Therefore, the uptime must be forever. Problem solved.

Well, OK, we suppose that maybe a Windows server could go down (probably because a non-Windows user snuck in and pulled the plug or something). If you need to know the system uptime on either a Windows XP or a Windows Server 2003 computer you can use a script like this:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colOperatingSystems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfFormattedData_PerfOS_System")

For Each objOS in colOperatingSystems
    intSystemUptime = Int(objOS.SystemUpTime / 60)
    Wscript.Echo intSystemUptime & " minutes"
Next

As you can see, we connect to the WMI service and then use the ExecQuery method to retrieve all instances of the Win32_PerfFormattedData_PerfOS_System class. (This, by the way, explains why the script runs only on Windows XP or Windows Server 2003: those are the only two versions of Windows that include the Win32_PerfFormattedData_PerfOS_System class.)

Win32_PerfFormattedData_PerfOS_System contains a number of performance counters related to the operating system, including SystemUpTime, which tells you how many second the machine has been running. We use this line of code to grab the value of SystemUpTime, divide it by 60 (thus giving us the uptime in minutes rather than seconds), and then convert it to an integer, stripping away anything after the decimal point:

intSystemUptime = Int(objOS.SystemUpTime / 60)

Yes, that is an awful lot for one little line of code to do, isn’t it? We then echo the value of the variable intSystemUptime and we’re done.

Well, unless you’re running Windows 2000, that is. Like we said, the preceding script won’t run on Windows 2000; Windows 2000 has never even heard of the Win32_PerfFormattedData_PerfOS_System class. Fortunately there is a workaround; it’s a bit clumsy, but it’s a workaround nevertheless:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colOperatingSystems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

For Each objOS in colOperatingSystems
    dtmBootup = objOS.LastBootUpTime
    dtmLastBootupTime = WMIDateStringToDate(dtmBootup)
    dtmSystemUptime = DateDiff("n", dtmLastBootUpTime, Now)
    Wscript.Echo dtmSystemUptime & " minutes"
Next

Function WMIDateStringToDate(dtmBootup)
    WMIDateStringToDate = CDate(Mid(dtmBootup, 5, 2) & "/" & _
        Mid(dtmBootup, 7, 2) & "/" & Left(dtmBootup, 4) _
            & " " & Mid (dtmBootup, 9, 2) & ":" & _
                Mid(dtmBootup, 11, 2) & ":" & Mid(dtmBootup,13, 2))
End Function

In this script we connect to the WMI service and then query the Win32_OperatingSystem class; that’s because Win32_OperatingSystem includes a property – LastBootUpTime – that can tell you the last time the computer started. That’s useful: if we subtract LastBootUpTime from the current time we’ll know – that’s right – how long the computer has been running.

The only tricky part is the fact that, like all WMI dates and times, LastBootUpTime is stored in the UTC (Universal Time Coordinate) format. That means you’re going to get back a value that looks like this:

20050726071152.500000-420

Before you try, don’t bother subtracting that from the current date and time; it won’t work. Instead, we need to convert this into something that looks more like a real date and time; that’s why this line of code calls a function we wrote (WMIDateStringToDate) that converts a UTC datetime value into a regular old date-time value:

dtmLastBootupTime = WMIDateStringToDate(dtmBootup)

Note. We won’t explain how the function works. If you’re interested in that information, see this portion of the Microsoft Windows 2000 Scripting Guide.

After making the conversion we use the DateDiff function to subtract the last bootup time from the current time:

dtmSystemUptime = DateDiff("n", dtmLastBootUpTime, Now)

As you can see, we pass DateDiff three parameters:

· “n”, which means to report the time difference in minutes.

· dtmLastBootUpTime, a variable holding the converted datetime value.

· Now, the current date and time.

All we do then is echo back the system uptime in minutes.

Like we said, we can’t imagine why you’d ever need a script like this for a Windows computer, but just in case ….


How Can I Determine When a Removable Drive Gets Connected?

Hey, Scripting Guy! How can I determine when a USB flash drive gets connected?

-- PS

Hey, PS. OK, we admit it: the Scripting Guys are lazy. (Especially on a Friday, which is the day when we have to write our Monday column.) There might be a way to monitor specifically for the insertion of a USB flash drive; however, we couldn’t find one, at least not right away. Admittedly, we probably could have looked a bit harder; however, we decided instead to write a script that tells you when any removable drive gets connected (or disconnected) from a machine. We like to think that this way we’re providing you with additional value and flexibility.

Hey, we didn’t say we really were providing you with additional value and flexibility. We just like to think that we are.

Actually, this is kind of a handy little script. And it has the added benefit of running on any version of Windows. (Originally we toyed with the notion of using the Win32_VolumeChangeEvent class to accomplish this task, but that particular WMI class is found only on Windows Server 2003.)

Here’s what the code looks like:

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

Set colEvents = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceOperationEvent Within 10 Where " _
        & "TargetInstance isa 'Win32_LogicalDisk'")

Do While True
    Set objEvent = colEvents.NextEvent
    If objEvent.TargetInstance.DriveType = 2 Then
        Select Case objEvent.Path_.Class
            Case "__InstanceCreationEvent"
                Wscript.Echo "Drive " & objEvent.TargetInstance.DeviceId & _
                    " has been added."
            Case "__InstanceDeletionEvent"
                Wscript.Echo "Drive " & objEvent.TargetInstance.DeviceId & _
                    " has been removed."
        End Select
    End If
Loop

And yes, this is similar to the screensaver monitoring script we showed you the other day. We like to think that reusing the same script day-after-day provides you with additional value and flexibility. (Although mostly it just ensures that we don’t have to work very hard.)

This script starts off by connecting to the WMI service on the local computer. We then issue this query:

Set colEvents = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceOperationEvent Within 10 Where " _
        & "TargetInstance ISA 'Win32_LogicalDisk'")

What’s going on here? Well, here we’re using the ExecNotificationQuery method to subscribe to a particular set of WMI events. What WMI events? (Boy, you have a lot of questions, don’t you?) In this case we want to be notified any time changes are made to an instance of the Win32_LogicalDisk class; as you’ll see momentarily, those changes will include creating a new instance of the class (i.e., adding a removable drive) and deleting an existing instance of the class (i.e., removing a removable drive). The Within 10 simply means we’re going to check every 10 seconds to see whether any of the Win32_LogicalDisk instances have changed.

And yes, that means if you insert a removable drive and then remove it 6 seconds later we might never know about it. If that’s a problem, then change 10 to a smaller number. You can also change 10 to a higher number. For example, change 10 to 60 and your check will occur every 60 seconds rather than every 10 seconds.

See? We even built added value and flexibility into the code itself!

After that we set up one of those crazy Do loops designed to run forever:

Do While True

And then we encounter this line of code:

Set objEvent = colEvents.NextEvent

As we noted in the previous column, this line of code causes the script to “block;” that simply means the script will pause on this line until there’s a change to the Win32_LogicalDisk class. That change (which could be the creation of a new instance or the deletion/modification of an existing instance) will then cause the script to execute the remaining lines of code in the Do loop.

Good question: what exactly do those remaining lines of code do? Well, first we check to see if the drive that generated the event happens to be a removable drive (a DriveType of 2 as far as WMI is concerned):

If objEvent.TargetInstance.DriveType = 2 Then

If the DriveType is not 2, then we just loop around and wait for the next event to occur. If the DriveType is 2 then we use a Select Case block to determine which type of event occurred. We can do that by determining the Class of the event:

Select Case objEvent.Path_.Class

Why do we do that? Two reasons: First, we aren’t interested in any changes to existing instances. For example, we don’t care if the amount of free drive space has changed on drive C. If you look at the Select Case code you’ll notice we don’t bother checking for the __InstanceModificationEvent. Why not? Because we don’t care about the __InstanceModificationEvent (the event type generated when an existing instance is modified in some way).

Second, we want to draw a distinction between the __InstanceCreationEvent (which tells us a new drive has been created) and the __InstanceDeletionEvent (which tells us an existing drive has been deleted). By identifying the event type we can echo back a different (and appropriate) message. For example, here’s the code that determines whether a new drive was created and, if so, echoes back a message to that effect:

Case "__InstanceCreationEvent"
    Wscript.Echo "Drive " & objEvent.TargetInstance.DeviceId & _
        " has been added."

And here’s the code that tells us whether an existing drive has been deleted:

Case "__InstanceDeletionEvent"
    Wscript.Echo "Drive " & objEvent.TargetInstance.DeviceId & _
        " has been removed."

Once we’ve echoed the appropriate message we loop around and wait for the next event to occur. By default the script will run forever, or at least until you terminate the script process. Whichever comes first.

Like we said, it’s not exactly what you asked for, but it should do the job. Oh, and don’t forget the additional value and flexibility. That should more than make up for the fact that we didn’t really answer your question. (We like to think we answered a question. Just not your question.)


How Can I Log a User Off After 5 Minutes of Inactivity?

Hey, Scripting Guy! How can I force a user to log off after 5 minutes of inactivity?

-- CS

Hey, CS. You know, no doubt at least some of you are wondering why we Scripting Guys are so excited about the Winter Scripting Games (coming February 12-23, 2007 to a Script Center near you). What’s the deal here: do the Scripting Guys get a commission for each person who enters the Scripting Games?

Hmmm, that’s not a bad idea …. As it turns out, though, we don’t get any tangible reward for the Scripting Games; instead, we get excited about the Games for two reasons: 1) they’re fun; and, 2) at least two of the Scripting Guys thrive on competition, even if they aren’t the ones doing the competing. The truth is, several years ago, when we first became Scripting Guys, a lot of people looked upon scripting as being sheer drudgery. We see the Scripting Games as a way to show everyone that scripting is not only a useful tool for system administration, but it can be fun, too. What could be better than that?

And, of course, the Scripting Games also give us a chance to play Santa Claus and hand out hundreds of prizes, including Dr. Scripto bobbleheads and copies of the book Windows PowerShell in Action.

But you want to know the real reason we like the Scripting Games? For once someone else has to do all the work! With the Scripting Games the tables are turned: this time we ask the questions and you have to write all the scripts. This is the one time of year when the Scripting Guys just kick back and relax and watch as everyone else work their fingers to the bone.

Well, come to think of it, we actually have to write these same scripts, too; after all, as the Games proceed we’ll publish sample solutions. And we have to write explanations that accompany each of these sample solutions. And, of course, we had to come up with all the events in the first place. And then there’s the official Scripting Games program and the training article. Oh: and we’re faced with the prospect of running and scoring hundreds and hundreds of scripts over the next couple weeks. And …

.

On second thought, is it too late to cancel this year’s Scripting Games?

No, hey, we’re just kidding; the Scripting Games are by far the most fun thing we do all year. And yes, that includes performance reviews, changing offices, and those delightful three-hour “all-hands” meetings.

In fact, about the only thing we can think of that is more fun than the Scripting Games is a script that can log a user off a computer after 5 minutes of inactivity:

strComputer = "."

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

Set objEventSource = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 10 WHERE TargetInstance ISA 'Win32_Process'")

Do While True
    Set objEventObject = objEventSource.NextEvent()
    If Right(objEventObject.TargetInstance.Name, 4) = ".scr" Then
        Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")
        For Each objItem in colItems
            objItem.Win32Shutdown(4)
        Next
    End If
Loop

Before we explain how this script works we should note that we cheated a little here. This is the kind of question we get asked quite a bit: how can you write a script that does something after a computer has been idle for x number of minutes? We understand why this is of interest to people; the only problem is that it’s not easy to determine – at least not by using a script – whether a computer is actually idle or not. Therefore, we decided the best thing to do was cheat: we don’t even bother trying to determine whether or not the computer is idle; instead, we let the computer make that determination for us.

How do we do that? Well, our script assumes that the screensaver has been configured to pop on if the computer has been idle for 5 minutes. Working under this assumption, we don’t have to decide whether or not the computer has been idle for 5 minutes; instead, we simply check periodically to see if the screensaver is running. If it is, then we assume that the computer has been idle for 5 minutes and we go ahead and log the user off.

In other words, “Let someone else do it.” Which would be a good motto for the Scripting Guys, wouldn’t it?

So then how do we determine whether or not the screensaver is running? Well, as you can see, the script starts off by connecting to the WMI service on the local computer (although you could also run this script against a remote computer). We then set up an event notification query that ventures out every 10 seconds and returns a list of all the processes running on the computer:

Set objEventSource = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 10 WHERE TargetInstance ISA 'Win32_Process'")

Note. You say you don’t know much about WMI events and event notification queries? That’s OK; this Scripting Guys webcast should answer all your questions.

After issuing our query we set up a Do While loop that’s designed to run forever. Inside that loop we use this line of code to instruct the script to wait until our event notification query actually notifies us of something:

Set objEventObject = objEventSource.NextEvent()

This line of code is going to cause the script to “block;” that is, it’s simply going to sit there until the query has some information for us. In this case, that’s going to occur every 10 seconds (that’s what the WITHIN 10 clause is for in the query): every 10 seconds our query will gather up a collection of running processes and hand them off to the script.

What we need to do next is determine whether the screensaver is included in that collection of running processes. That’s something we can do by looping through the collection and checking to see if the Name (the file name) of any process ends in .scr. That’s what we do here:

If Right(objEventObject.TargetInstance.Name, 4) = ".scr" Then

As you can see, we simply use the Right function to look at the last 4 letters in the file name. If those four letters happen to be .scr (for example, ssstars.scr) then we assume that the screensaver is running.

Note. Did you know that you can download your very own Script Center screensaver? Oh. Well, now you know.

Let’s assume that the screensaver is running. If so, we then issue a second QMI query, one designed to bind us to the Win32_OperatingSystem class:

Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

This query returns a collection consisting of the current operating system running on the computer. (Note that this doesn’t mean all the operating systems installed on the computer. If you have a multi-boot machine you’ll still get back just one item: the current operating system.) At that point we can then set up a For Each loop to loop through the collection and, for each operating system in the collection, call the Win32Shutdown method:

objItem.Win32Shutdown(4)

In case you’re wondering – and we know you are – the 4 simply tells the Win32Shutdown method to do a forced logoff. If you’d prefer to restart or completely shutdown a computer, well, that’s fine: Win32Shutdown accepts a number of different parameters. See the Microsoft Windows 2000 Scripting Guide for details.

And that should do it. Like we said, we cheated a little, but it works.

That’s a good question: if the Scripting Guys cheated does that mean that cheating is allowed in the Scripting Games? Listen, not only is cheating allowed, but cheating is encouraged: feel free to use any resources you can find that will help you complete the assigned tasks. In the case of the beginner events, for example, you can find Script Center scripts that will flat out solve some of the challenges for you: if you can copy and paste you can earn points in the Games. Remember, you don’t get any extra points for creativity or originality; you simply get points for submitting a solution that works.

Even better, if all you want is a chance at winning a prize your solution doesn’t even have to work; you just need to enter one of the events. In other words, with minimal effort you can send us a script that doesn’t do anything and yet still come out a winner. Now do you see why we Scripting Guys have such a soft spot in our hearts for the Scripting Games?


How Can I Log the Start Time and the End Time of the Screensaver?

Hey, Scripting Guy! How can I log the start time and the end time of the screensaver?

-- JS

Hey, JS. You know, one of the Scripting Guys (hey, who said “It must be Greg”?) is old enough to remember the days when screensavers first came out. Back then a script like this would have been meaningless: after all, once a screensaver started everyone was so fascinated by it they never wanted it to end. In fact, one of the first things this Scripting Guy had to do as a computer support person was to make little shortcuts on everyone’s desk so that they could launch their Flying Toasters any time they wanted.

People were so easy to amuse back then.

Ah, but there’s no point living in the past, right? In today’s modern world you obviously have a need not only to stop a screensaver, but to take note of when it stopped. With that in mind, let’s take a look at a WMI event monitoring script that will keep track of each time a screen saver starts and stops:

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

Set objEventSource = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process'")

Do While True
    Set objEventObject = objEventSource.NextEvent()
        If Right(objEventObject.TargetInstance.Name, 4) = ".scr" Then
        Select Case objEventObject.Path_.Class
            Case "__InstanceCreationEvent"
                Wscript.Echo "Screensaver " & objEventObject.TargetInstance.Name & _
                    " started: " & Now
            Case "__InstanceDeletionEvent"
                Wscript.Echo "Screensaver " & objEventObject.TargetInstance.Name & _
                    " ended: " & Now
        End Select
    End If
Loop

Yes, it does look a little complicated, doesn’t it? But don’t panic: by design WMI event scripts always look a little complicated. Fortunately, the scripts only look complicated; as you’ll see they’re really not all that hard to understand.

Note. OK, we’d better qualify that last statement: they’re not all that hard to understand as long as you’ve been exposed to the basic ideas behind WMI eventing. If that isn’t the case, then you might want to take time out and watch this Scripting Week 2 webcast. The webcast will give you all the background information you need to make sense of today’s column.

Good point; probably nothing will help you make sense of one of our columns. But at least it’ll help you make sense of the script code.

This particular script begins in the time-honored fashion of connecting to the WMI service on the local computer. It’s right about here that we usually execute a WMI query to return information. As it turns out, we’re going to do that in this script, too; it’s just that our query is going to look a little different:

Set objEventSource = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process'")

Needless to say, this isn’t the type of WMI query you’re used to writing; for one thing, we’re calling the ExecNotificationQuery method rather than ExecQuery. (Why? Because ExecNotificationQuery is the method you have to use in order to monitor WMI events.) We won’t explain this query in minute detail today, but suffice to say that we’re asking WMI to notify us any time a WMI event occurs (something is created, something is deleted, something is modified). There’s just one catch: we want to be notified only if the TargetInstance (the item being created, deleted, or modified) is an instance of the Win32_Process class.

Note. OK, technically there’s a second catch: we’re only checking for new events every 5 seconds. If the screensaver starts and then ends 3 seconds later, we’ll probably never be notified.

In other words, suppose a new file is created. Is a new file an instance of the Win32_Process class? Nope; it’s an instance of the CIM_DataFile class. Therefore, we don’t want to be notified. Suppose a service gets modified. Do we want notification? No; a service is an instance of the Win32_Service class. OK then, suppose a new process – like a screensaver – starts up. Do we want to be notified? You bet: after all, a new process is an instance of the Win32_Process class. We want to be notified any time a process is created, deleted, or modified.

But you already figured that out, didn’t you?

In order to get these notifications we set up a Do loop that runs while True is equal to True:

Do While True

That’s kind of weird syntax, but it’s a syntax that enables the script to continue to run – and continue to monitor the creation, deletion, and modification of processes – until we either terminate the script or restart the computer. If we didn’t have a loop like this, the script would notify us that a screensaver had started, but then the script would end. Consequently, we’d never be notified when the screensaver ended.

The first thing we do inside the loop is execute this line of code:

Set objEventObject = objEventSource.NextEvent()

What we’re doing here is telling the script to wait until the next event of interest occurs; in other words, the script will sit on this line of code until a process is created, deleted, or modified. Suppose our processes never change, suppose we never create, delete, or modify a process. Well, in that case the script will simply sit here forever, waiting patiently. Just in case.

Now, we know what you’re thinking. You’re thinking: “Hey, wait a second. We only care about screensavers. Microsoft Word runs in a process. If we start Microsoft Word, thus creating a new instance of the Winword.exe process, won’t that trigger a notification?”

You’re right: it will. And that’s what our next line of code is for. Starting up Word (or any executable file, for that matter) will, indeed, issue a notification. But we can account for that by using this line of code:

If Right(objEventObject.TargetInstance.Name, 4) = ".scr" Then

Here we’re using the Right function to check the Name of the process that triggered the notification. If the rightmost four characters in the name are equal to .scr, then we’re assuming that we’re dealing with a screensaver; that’s because screensavers have names like Marquee.scr. If the last four characters in the name aren’t .scr then we simply loop around and wait for the next event to occur.

So what if the last four characters are .scr? Well, in that case we’re interested in two possibilities: a screensaver started or a screensaver ended. (We don’t care whether or not someone modifies the properties of a screensaver.) To handle these two possibilities we set up a Select Case block that examines the Class of the event instance:

Select Case objEventObject.Path_.Class

If the Class is equal to __InstanceCreationEvent, that means a new process (i.e., a new screensaver) has been created. In our first Case statement we check to see if the Class is equal to __InstanceCreationEvent. If it is, we echo the fact that a particular screensaver (represented by the process name) started at a particular time (using the VBScript function Now):

Case "__InstanceCreationEvent"
    Wscript.Echo "Screensaver " & objEventObject.TargetInstance.Name & " started: " & Now

That makes sense, doesn’t it? Now, suppose the screensaver has ended, something which would cause the screensaver process to be deleted. To handle that possibility, we check for any new instances of the __InstanceDeletionEvent class. If our event happens to belong to that class (meaning a screensaver process has been deleted) we echo the fact that a given screensaver stopped at a given time:

Case "__InstanceDeletionEvent"
    Wscript.Echo "Screensaver " & objEventObject.TargetInstance.Name & " ended: " & Now

And there you have it. When you run the script you’ll get back information similar to this:

Screensaver Script Center.scr started: 2/9/2006 9:11:07 AM
Screensaver Script Center.scr ended: 2/9/2006 9:11:17 AM

Note. What the heck is Script Center.scr? Download it and find out.

Two things we should add. First, this script is best run in a command window under Cscript; that means that, to start the thing, you might want to open a command window and type something like this (depending on what you name the script, of course):

cscript screensaver_monitor.vbs

Second, and as we pointed out, this script is designed to run forever. On the other hand, nothing lasts forever, does it? If you want to stop monitoring all you have to do is press Ctrl+C, close the command window, or terminate the CScript.exe process. Remember, the Scripting Guys would never leave you trapped inside an endless loop with no way out. (What do you know: that’s a pretty good description of our jobs right there.)


How Can I Modify the Output of the Ping Command?

Hey, Scripting Guy! How can I place the current date and time at the beginning of each line of Ping.exe output?

-- AL

Hey, AL. We have to admit, we were a little shocked by your question. Change the output of the Ping command? Really, AL. Would you ask us to rewrite Romeo and Juliet, giving it a more happy and upbeat ending? Would you ask us to fix up the Mona Lisa, giving Mona blonde hair and maybe a cute little butterfly tattoo? Would you ask us to take the peanuts out of peanut M&M’s and replace them with soybeans? Some things should not be tampered with; they’re perfectly fine just the way they are.

But, to tell you the truth, we don’t consider Ping.exe to be one of those things:

Set objShell = CreateObject("WScript.Shell")

Set objWshScriptExec = objShell.Exec("ping 192.168.1.1")

Set objStdOut = objWshScriptExec.StdOut

Do Until objStdOut.AtEndOfStream
    strLine = objStdOut.ReadLine
    If Len(strLine) > 2 Then
        WScript.Echo Now & " -- " & strLine
    Else
        Wscript.Echo strLine
    End If
Loop

So what are we doing here? Well, to begin with, we’re creating an instance of the Wscript.Shell object. After that, we call the Shell object’s Exec method, asking Exec to run the command-line command ping 192.168.1.1:

Set objWshScriptExec = objShell.Exec("ping 192.168.1.1")

If you aren’t familiar with the Exec method (which was introduced as part of Windows Script Host version 5.6) it’s actually a nifty little command. Exec is similar to the Run method; the primary difference is that while the Run method runs a command-line tool in a visible command window, the Exec method runs that same command-line tool in a hidden window. When you call Ping.exe using Exec, you’ll never see Ping – or its output – onscreen.

That’s a good point: Ping isn’t really all that useful if you never see the results, is it? But that’s OK: we can still get at the output. That’s because a command-line tool run using Exec has all its output stored in the StdOut property of the WSHScriptExec object (an object we created at the same time we called the Exec method). To get at the output of Ping.exe we simply need to read the value of the StdOut property.

So let’s see how we go about doing that. To begin with, here’s a line of code that binds us to the StdOut property:

Set objStdOut = objWshScriptExec.StdOut

After we’ve made the connection we set up a Do Until loop that reads StdOut line-by-line until there are no more lines to read (that is, until the AtEndOfStream property is true). Admittedly, we don’t need to read StdOut line-by-line; we’re only doing it in this case because we want to put the current date and time at the beginning of each line.

Note. Admittedly, we’re cheating a tiny bit: we’re actually calling Ping, waiting for the command to finish, and then – slightly after the fact – adding the date and time to the beginning of each line. That means our times will always be a few seconds off. However, that’s about the best we can do: we can’t ask Ping to issue the first ping, wait for us to grab the results and tack on the date and time, and only then run the second ping. If you feel the need to get that level of accuracy – and if you’re running Windows XP or Windows Server 2003 – then you might want to use the WMI class Win32_PingStatus and, in essence, create your own, customized version of Ping.exe.

Inside the loop we use this line of code to read the first line from StdOut:

strLine = objStdOut.ReadLine

We then use the Len function to determine whether or not the line has more than two characters in it. Why? Well, Ping’s output typically includes a few blank lines; because we don’t want to stick the current date and time on a blank line we use a little bit of programming intelligence to weed out blank lines. So then why do we check for lines greater than 2 characters? Shouldn’t we check for lines that have 0 characters?

That would seem to make sense. However, in command-line output a blank line doesn’t really have 0 characters; instead a blank line actually has two characters: a carriage return and a linefeed. If we checked for lines that had a length equal to 0 characters we’d never find one.

Suppose a line does have more than two characters in it. In that case we echo back the current date and time (using the VBScript function Now) followed by the line of code we read in from StdOut:

WScript.Echo Now & " -- " & strLine

If the line doesn’t have more than two characters then it must be a blank line. In that case, we don’t attach the current date and time but instead echo back just the value read in from StdOut:

Wscript.Echo strLine

Will that do the trick? Judge for yourself:

8/6/2006 2:42:45 PM -- Pinging 192.168.1.1 with 32 bytes of data:

8/6/2006 2:42:45 PM -- Reply from 192.168.1.1: bytes=32 time<1ms TTL=128
8/6/2006 2:42:46 PM -- Reply from 192.168.1.1: bytes=32 time<1ms TTL=128
8/6/2006 2:42:47 PM -- Reply from 192.168.1.1: bytes=32 time<1ms TTL=128
8/6/2006 2:42:48 PM -- Reply from 192.168.1.1: bytes=32 time<1ms TTL=128

8/6/2006 2:42:48 PM -- Ping statistics for 192.168.1.1:
8/6/2006 2:42:48 PM --     Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
8/6/2006 2:42:48 PM -- Approximate round trip times in milli-seconds:
8/6/2006 2:42:48 PM --     Minimum = 0ms, Maximum = 0ms, Average = 0ms

Note. Incidentally, many years ago the legendary Dean Tsaltas wrote a Tales From the Script column that, among other things, used this same approach to show how you can modify the output you get back from command-line tools. If that sounds interesting to you check it out. (This, by the way, was the very first column published in the Script Center. And it remains as useful today as it was back in 2002!)

We hope that helps, AL. As for putting a butterfly tattoo on the Mona Lisa, though, well, we’re afraid we can’t help you there:


How Can I Monitor Changes to a Registry Key?

Hey, Scripting Guy! Is it possible to use a script to monitor changes to a registry key? I’d like to be notified any time someone makes changes to the HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run key.

-- SB

Hey, SB. And by the way, thanks for wording the question the way you did: it’s actually pretty easy to receive notification any time someone makes a change to a specific registry key. Had you gone on to ask, “And then can you tell me who made the change and what change they made?” well, in that case we’d have a problem on our hands (and we’ll explain why in a moment). But you didn’t ask that, so for now we’ll just forget we even mentioned it.

Let’s take a look at a script that monitors the HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run registry key and then echoes back a message any time that key is changed:

strComputer = "."

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

Set colEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM RegistryKeyChangeEvent WHERE Hive='HKEY_LOCAL_MACHINE' AND " & _
        "KeyPath='SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run'")

Do
    Set objLatestEvent = colEvents.NextEvent
    Wscript.Echo Now & ": The registry has been modified."
Loop

We begin by connecting to the WMI service; more specifically, we connect to the root\default namespace, which is where the registry event classes live. We then use the ExecNotificationQuery method to issue the following query:

("SELECT * FROM RegistryKeyChangeEvent WHERE Hive='HKEY_LOCAL_MACHINE' AND " & _
        "KeyPath='SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run'")

All we’re doing here is requesting notification any time an instance of the RegistryKeyChangeEvent class is created; as you might have guessed, an instance of this class is created any time a specified registry key is changed (for example a new value is added or an existing value is modified or deleted).

Sounds good, but how do we specify the registry key? That’s easy; we just need to configure the following two properties of the RegistryKeyChangeEvent class:

· Hive. This is the registry hive (location) where the key lives. We want to monitor a key that lives in HKEY_LOCAL_MACHINE, so we set the value of the Hive property to HKEY_LOCAL_MACHINE. If we wanted to monitor a registry key that lives in HKEY_CURRENT_USER, we’d set the value accordingly. Note that we do not have to define and use constants when configuring the Hive property; just type in the actual name of the registry hive.

· KeyPath. This is the path within the hive that leads to our registry key. Because the \ is a reserved character in WMI, notice that we need to escape each \ with a second \. Thus, a key path like Software\Microsoft\Windows\CurrentVersion\Run must be written out as Software\\Microsoft\\Windows\\CurrentVersion\\Run.

After issuing the query we set up a Do Loop that will simply run forever, dutifully waiting for the next instance of the RegistryKeyChangeEvent class to appear. To make the script wait for such an appearance we use this line of code:

Set objLatestEvent = colEvents.NextEvent

This line of code causes the script to “block,” which means the script will just sit there and wait for the next event to occur (as you well know, the event we are monitoring for is new instances of the RegistryKeyChangeEvent class). When such an event does occur, we simply echo the current date and time and the fact that the registry has been modified in some way. We then loop around and wait for the next such occurrence. (Press Ctrl+C to get out of the loop and end the script.)

Why don’t we do something a bit more specific than merely note that the registry has been modified in some way? Well, mainly because we can’t do anything more specific: details such as what was changed and who changed it are not captured by the registry event provider. We could determine some of this information by grabbing the initial state of the registry key and then, when an event occurs, comparing the new state to the initial state. We’d then have to set up a procedure that allows us to continue comparing the latest state of the registry key to the most recent previous state. But that’s something we’ll have to let you work out for yourself. (Unless, of course, we hear otherwise. After all, we live to serve!)


How Can I Monitor for Different Types of Events With Just One Script?

Hey, Scripting Guy! Is there any way to monitor a folder for file creations and deletions, all with just one script?

-- HA

Hey, HA. By the way, thanks for the question: you temporarily saved us from answering a different question, one that was proving a bit difficult thanks to some surprising differences between Windows XP Service Pack 1 and Windows XP Service Pack 2. (We’ll tackle that question in tomorrow’s column.) Your question, by contrast, is an easy one to answer, even if most people aren’t aware of that.

Most people are aware that you can monitor for file creations, deletions, or modifications by using one of these three event classes (note the two underscores at the beginning of all three class names):

· __InstanceCreationEvent. This is used for monitoring creation events; that is, you can be notified any time a new instance of something gets created. In our case, notification will occur any time a new file is created in the folder C:\Scripts.

· __InstanceDeletionEvent. You’re way ahead of us: this class is used for monitoring deletion events; we’ll be notified any time a file is deleted from C:\Scripts.

· __InstanceModificationEvent. Finally, we can be notified any time a file in C:\Scripts is modified in some way (you change the file name, you add or delete something from the file, etc.).

Now that’s all great except for one thing: each of these is a separate WMI class and, if you’re familiar with WMI, you know that WMI limits you to selecting objects from only one class at a time. For example, this is a valid event query, a query that monitors only __InstanceCreationEvent:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

However, this next query – which tries to monitor instances of both the __InstanceCreationEvent and __InstanceDeletionEvent classes – is doomed to fail:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceCreationEvent OR __InstanceDeletionEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

You just can’t do it; instead, you get the terse error message: “Unparsable query.” Yikes.

Is this a problem? Of course it is. Although there are exceptions, you’ll often want to know about any activity that goes on in C:\Scripts, not just file creations or file deletions. Because of that people often resort to writing – and running – separate scripts to monitor and report on each of the WMI event classes.

But guess what: there’s a better way. Turns out there’s actually a fourth WMI event class: __InstanceOperationEvent. Best of all, our other three event classes are all subsumed under __InstanceOperationEvent; you can use __InstanceOperationEvent to monitor for event creations, event deletions, and event modifications. We’ll use it today to monitor for both file creations and deletions.

What we do is monitor for instances of the __InstanceOperationEvent class; new instances of this class are created pretty much any time anything occurs on a computer. We then check the value of the Path_.Class property to determine whether the new instance was created in response to a file creation or file deletion. That’s how we can monitor for file creations and deletions in a single script.

Let’s take a look at the code:

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

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

Do While TRUE
    Set objEventObject = colMonitoredEvents.NextEvent()

    Select Case objEventObject.Path_.Class
        Case "__InstanceCreationEvent"
            Wscript.Echo "A new file was just created: " & _
                objEventObject.TargetInstance.PartComponent
        Case "__InstanceDeletionEvent"
            Wscript.Echo "A file was just deleted: " & _
                objEventObject.TargetInstance.PartComponent
    End Select
Loop

Keep in mind that looks can be deceiving here; the script isn’t quite as complicated as it might first appear. All we’re really doing here is using a basic ExecNotificationQuery to check every 10 seconds (WITHIN 10) for new instances of the __InstanceOperationEvent class; in particular, we’re looking for instances involving the folder C:\Scripts. That’s what this crazy piece of code does:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceOperationEvent WITHIN 10 WHERE " _
        & "Targetinstance ISA 'CIM_DirectoryContainsFile' and " _
            & "TargetInstance.GroupComponent= " _
                & "'Win32_Directory.Name=""c:\\\\scripts""'")

Note. If none of this makes sense to you, don’t worry; instead, check out this WMI Events webcast from Scripting Week 2. In that webcast we discuss WMI events in detail, and we explain things like ExecNotificationQuery and WITHIN 10.

After issuing the query we simply create a Do Loop and wait for the next event to occur. When it does we use the Path_.Class property to determine the event type; we then echo a message that an event just took place. This message also includes the WMI object path for the new instance (information which happens to be stored in the PartComponent property). The object path for a file looks something like this:

\\ATL-WS-01\root\cimv2:CIM_DataFile.Name="c:\\scripts\\New Text Document.txt"

We didn’t bother doing so here, but you could add a few lines of code to the script to pull out just the file path (in this case, C:\Scripts\New Text Document.txt). Otherwise, you’ll just have to make do with the WMI object path.

Here’s where all that excitement takes place:

Select Case objEventObject.Path_.Class
    Case "__InstanceCreationEvent"
        Wscript.Echo "A new file was just created: " & _
            objEventObject.TargetInstance.PartComponent
    Case "__InstanceDeletionEvent"
        Wscript.Echo "A file was just deleted: " & _
            objEventObject.TargetInstance.PartComponent
End Select

That’s basically it; we then loop around and wait for the next event. And while we’re waiting, we’ll get back to work on that other question, the one that inspired us to take the easy way out and answer this question instead.


How Can I Monitor for the Creation of Different Processes?

Hey, Scripting Guy! How can I use one script to monitor for the creation of four or five different processes?

-- BC

Hey, BC. You know, it’s too bad you didn’t ask how you could monitor four or five TV shows at the same time. Had you done that we would have had the perfect answer for you: get yourself a teenaged son. Although the Scripting Dad is rarely impressed by anyone or anything, he does stand in awe of the Scripting Son’s ability to watch two football games, Baseball Tonight, one movie, and whatever happens to be on Cartoon Network, all at the same time and all without missing a beat. The Scripting Dad, meanwhile, finds himself totally lost within seconds. “I thought Houston already played today,” he’ll say.

“Uh, Dad, that’s my video game.”

Oh. Right.

There’s no doubt that the Scripting Son – and his friends – possess an amazing talent; however, that probably won’t help you much, BC. After all, monitoring multiple processes sounds an awful lot like work, a word you rarely see used in the same sentence as “teenaged son.” That means you’ll have to make do with a script similar to this one:

arrProcesses = Array("freecell.exe","sol.exe","spider.exe","winmine.exe")

strComputer = "."

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

i = 0

Set colMonitoredProcesses = objWMIService. ExecNotificationQuery _
    ("Select * From __InstanceCreationEvent Within 5 Where TargetInstance ISA 'Win32_Process'")

Do While i = 0
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    strProcess = LCase(objLatestProcess.TargetInstance.Name)

    For Each strName in arrProcesses
        If strName = strProcess Then
            Wscript.Echo strName & " has started."
        End If
    Next
Loop

Before we talk about the script we should note that there were a couple different ways we could have attacked this problem. Our original thought was to write a complicated event query that would retrieve information for a only a specific set of processes. That sounded an awful lot like work, however, a word you rarely see used in the same sentence as “the Scripting Guy who writes this column.” Because of that, we opted for a much simpler approach: we get information about all the processes, then check each to see if the process is in our target list. It might not be as elegant, but it’s much easier and – because you have relatively few processes running on a computer – is nice and fast as well.

As for the script itself, it starts out by defining an array named arrProcesses and then adding four items to that array:

arrProcesses = Array("freecell.exe","sol.exe","spider.exe","winmine.exe")

And yes, those are the four processes we’re interested in.

With the array defined we connect to the WMI service on the local computer (although this script can also run against remote computers) and then assign the value 0 to a counter variable named i. That brings us to our event query:

Set colMonitoredProcesses = objWMIService. ExecNotificationQuery _
    ("Select * From __InstanceCreationEvent Within 5 Where TargetInstance ISA 'Win32_Process'")

What we’re doing here is asking WMI to stop every 5 seconds (that’s what the Within 5 is for) and see if there are any new instances of the __InstanceCreationEvent class; new instances of this class are automatically created any time a new something – a process, a service, an event log entry, anything WMI knows about – is created. Of course, we aren’t interested in new services or new event log entries; the only thing we care about are new processes. That’s why we tack on the Where clause that limits returned data to instances of the Win32_Process class.

Note. Yes, we know: if you’re new to WMI events that’s a very cursory explanation. But don’t worry; you can always take a look at the Scripting Week 2 webcast An Ounce of Prevention: An Introduction to WMI Events if you need more information about writing WMI event scripts.

After issuing our query we next set up a Do While loop that’s designed to run until the variable i is equal to 1. Of course, we’ve designed the script so that i will never be equal to 1; that way the script will run forever and ever, or at least until you shut down the computer or kill the script process. We do that so that you can be notified any time one of the target processes is created.

Note. Because the script is designed to run forever you might want to run the this baby in a command window under the CScript script host; that way you can stop the script simply by closing said command window. The script will run under WScript, but you’ll then have to use something like Task Manager to terminate the script.

So what happens inside the Do loop? We had a feeling you’d ask that. The very first thing we do inside that loop is execute the following line of code:

Set objLatestProcess = colMonitoredProcesses.NextEvent

What happens here is that the script stops dead in its tracks: it sits there, patiently, and waits for the next event to occur; that is, the script waits for the next member of the __InstanceCreationEvent class to be created (provided, of course, that the item triggering the instance creation happens to be a process). If no new processes are ever created then the script will simply sit there forever and ever.

Of course, sooner or later a new process is bound to be created. When this happens we take the name of the new process, convert it to all lowercase letters (to make sure we don’t run into problems comparing uppercase letters and lowercase letters), and then store the value in a variable named strProcess:

strProcess = LCase(objLatestProcess.TargetInstance.Name)

Once that’s done our next task is to determine whether or not this new process happens to be one of our four target processes, the group of processes we stored in the array arrProcesses. A very easy way to do that is to simply loop through the items in the array and determine whether or not the names of any of those array items happen to match the name of the new process. In fact, that’s what we do here:

For Each strName in arrProcesses
    If strName = strProcess Then
        Wscript.Echo strName & " has started."
    End If
Next

As you can see, this code is about as simple as it gets. All we do is determine whether the first name in the array happens to match the name of the new process (strProcess). If we have a match then we echo the name of the process and the fact that this process has just started. If we don’t have a match we loop around and check the next name in the array. After we’ve checked all four names we then zip back to the beginning of the loop and then wait for the next __InstanceCreationEvent to occur.

That should do it, BC. Like we said, there might be more elegant ways to do this, but, then again, elegance is also a word you rarely see used in the same sentence as “the Scripting Guy who writes this column.” In addition, this monitoring solution should work under any and all conditions, something which isn’t true of the Scripting Son. A week or so ago the batteries in the remote control called it quits, and the Scripting Son was in a panic. “Can’t you go to the store and get some batteries?” he asked. “How am I supposed to watch TV?”

“Can’t you just get up off the davenport and change the channel by hand?” asked the Scripting Dad.

At that point all it took was one look at the Scripting Son’s face for the Scripting Dad to know what had to be done: he went and got new batteries right away. No sense tempting fate, if you know what we mean.


How Can I Monitor the Number of Files in a Folder?

Hey, Scripting Guy! We have a third-party application that rarely crashes; however, when it does crash it seriously disrupts our business. We can always tell when the application is getting ready to crash; at that point, one of its directories starts filling up with log files. How can we be notified any time the number of log files in this folder exceeds a specified value?

-- AE

Hey, AE. You know, before we tackle this question there’s something we feel just has to be said: there are people in this world who are the embodiment of pure evil.

And no, Peter, for once we’re not thinking of you.

Although now that you mention it ….

The other night the Scripting Guy who writes this column stopped at the store on his way home and picked up a carton of milk. At that time the store was jam-packed, with 9 or 10 people standing in line at each checkstand … each checkstand, that is, except the express lane (15 items or less). With his single item safely in hand, the Scripting Guy who writes this column headed towards the express lane, only to be clipped from behind by a woman who shoved her shopping cart directly in his path, in order to get to the checkstand before he could.

Now, that was a bit rude (not to mention painful) but the Scripting Guy who writes this column was consoled by the fact that the lady was about to get her comeuppance. After all, she had way more than 15 items in the cart; there was absolutely no way the clerk would let her in line with that many items. But the Scripting Guy who writes this column didn’t count on the resourcefulness of the truly evil.

Before the clerk could say anything the woman spoke up. “I actually have two separate purchases here, “she said. “I need to pay for half of these items and get a receipt, then pay for the other half and get a separate receipt.” The implication, of course, is that she was buying food for two local orphanages, and needed separate receipts for tax purposes. (Although the Scripting Guy who writes this column couldn’t help but wonder, “What? Satan can’t do his own grocery shopping, you have to do it for him?”) As she explained the reason why she had barged into the express lane despite having way more than 15 items, she took all the groceries out of the shopping cart and piled them on the checkstand.

Note. In case you’re wondering, she had 15 items in pile 1 and 17 items in pile 2. Even her second pile by itself was over the 15-item limit!

Not that the Scripting Guy who writes this column was counting or anything.

To tell you the truth, the store clerk didn’t seem entirely comfortable with this. However, perhaps sensing the presence of pure evil, he decided to let it go. And what do you think happened? You got it: after he finished ringing up the last item in pile 1 the lady said, “You know, in order to make things easier for you, why don’t you just put everything on one order and I’ll pay for it all together.” (And yes, she actually did speak in italics. That’s what evil people do.) The clerk did what he was told, and a few minutes later the woman sashayed out of the store, having successfully purchased 32 items in the express lane (15 items or less).

Pure evil. And did we mention that she also drove an SUV and took up two parking spots in the parking lot? Even Dracula would only take one parking space, especially when the store was crowded.

But, then again, Dracula was at least part human.

Oh, well. Fortunately the Scripting Guys would never stoop that low. (Actually they would stoop that low, except the Scripting Guys would get beat up by angry patrons if they tried to sneak 32 items through the express lane.) For example, if you want to know how to monitor the number of files found in a particular folder, well, the Scripting Guys are only too happy to oblige:

strComputer = "."

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

Do While True
    Set colFileList = objWMIService.ExecQuery _
        ("ASSOCIATORS OF {Win32_Directory.Name='C:\Logs'} Where " _
            & "ResultClass = CIM_DataFile")

    If colFileList.Count >= 100 Then
        Exit Do
    End If

    Wscript.Sleep 60000
Loop

Wscript.Echo "There are at least 100 log files in the target folder."

There are actually several different ways we could approach this problem; we ended up picking one that seemed pretty easy to script and pretty easy to understand. We begin by connecting to the WMI service on the local computer, although we could just as easily use this same script to monitor a folder on a remote computer; in that case all we have to do is assign the name of that computer (e.g., atl-fs-01) to the variable strComputer:

strComputer = "atl-fs-01"

We next set up a Do While loop, a loop seemingly designed to run forever and ever (or, at the very least, to continue running as long as True is equal to True):

Do While True

But don’t worry; that only sounds like a mistake on our part. As you’re about to see, our loop includes a loophole that enables us bring this seemingly-endless loop to an end.

For our sample script we’ve decided that 100 is the trigger value: if we ever get to 100 or more files in our target folder (C:\Logs) we want to be notified. And that’s a good question: how do we know how many files are in C:\Logs in the first place? Well, a very simple way to determine that is to use the following WMI query, a query that returns a collection consisting of all the files in C:\Logs:

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

Admittedly, the syntax is a little weird here. However, all we’re really doing is looking for items that are associated with a particular instance of the Win32_Directory class. (Which instance? The one with a Name of C:\Logs.) Of course, we’re only interested in these associated items if they, in turn, happen to be instances of the CIM_DataFile class; as you can probably guess, if something is an instance of the CIM_DataFile class then that something must be a file. This is kind of a roundabout way of doing things, but it’s also WMI’s way of returning a collection of all the files found in a given folder. If your files are stored in a different folder, well, then just replace the path C:\Logs with the path to your folder.

As it turns out, one of the nifty things about WMI collections is that each collection has a property named Count; this property tells us how many items (in this case, how many files) can be found in the collection. Hence our next line of code, which checks to see if the value of the Count property is greater than or equal to 100:

If colFileList.Count >= 100 Then

Note. What if your target value is actually 10 files or 10,000 files or whatever number of files? No problem; just change 100 to the desired value.

Let’s assume that the Count isn’t greater than or equal to 100; what happens then? Well, in that case we pause the script for 60 seconds (60,000 milliseconds):

Wscript.Sleep 60000

When one minute is up we then loop around and requery the folder C:\Logs, once again checking to see how many files are in the folder. And yes, we do need to requery each time around; that’s the only way to know the current number of files in the folder.

This also means that we’re checking every minute to see if the number of files in C:\Logs has exceeded the target value of 100. However, you can also change this value to anything you desire. For example, maybe 60 seconds is too long; maybe the folder can fill up with files and bring the application to a crashing halt faster than that. OK; here’s a minor modification that causes the script to pause for just 15 seconds (15,000 milliseconds) between checks:

Wscript.Sleep 15000

Alternatively, maybe you don’t need to check every minute; maybe every 10 minutes is good enough. Fine. If there are 60,000 milliseconds in a minute (and there are), then there must be 600,000 milliseconds in 10 minutes. Thus this line of code, which pauses the script for 10 minutes between checks:

Wscript.Sleep 600000

Etc.

Now, what if the Count is greater than or equal to 100? In that case we simply exit our “endless” loop:

Exit Do

And once we exit the loop we echo back a message informing us that the target value has been exceeded:

Wscript.Echo "There are at least 100 log files in the target folder."

Now, admittedly, we don’t have to exit the loop; we did that simply so we wouldn’t get notified every minute that we had exceeded the target value. That’s the advantage to exiting the loop: fewer nags. The disadvantage? After we restart the application (or whatever we need to do to head off trouble) we’ll need to restart the monitoring script as well. If you don’t mind repeated notifications then you could rewrite the script so that it keeps running (and notifying you) even after the target value has been reached:

strComputer = "."

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

Do While True
    Set colFileList = objWMIService.ExecQuery _
        ("ASSOCIATORS OF {Win32_Directory.Name='C:\Logs'} Where " _
            & "ResultClass = CIM_DataFile")

    If colFileList.Count >= 100 Then
        Wscript.Echo "There are at least 100 log files in the target folder."
    End If

    Wscript.Sleep 60000
Loop

We should also mention that you aren’t limited to using Wscript.Echo as a way to issue a notification. For example, you might want to write an event to the Application event log, or maybe send an email using the SMTP service. That’s entirely up to you.

Of course, what would really be cool is if the script could fix the application for you. We can’t provide any sample code because we don’t know anything about the application. But it’s possible that the script could stop and then restart the application; that depends entirely on the application. (At the very least, you could no doubt terminate the application process and then restart it. But, depending on the application, that might create more problems than it solves.)

We hope that helps, AE, and we hope that, unlike the Scripting Guy who writes this column, you haven’t lost your faith in humanity. As it is, and to add insult to injury, when he came to work this morning he stopped by the kitchen and grabbed a plastic spoon out of the big bin labeled Plastic Spoons. As he sat down in his office and prepared to eat his daily raspberry yogurt he suddenly realized that he was holding a plastic fork: someone had put a plastic fork in the bin labeled Plastic Spoons!

Evil, pure evil.


How Can I Re-Launch Internet Explorer if No Other Instances are Running?

Hey, Scripting Guy! How can I re-launch Internet Explorer if no other instances are running?

-- MT

Hey, MT. Thanks for the question. You know, nowadays everyone picks on poor little Internet Explorer (even though the vast majority of Windows users still use Internet Explorer). But now we have a question from someone who wants to make sure that Internet Explorer is always up and running. See, Internet Explorer: someone still loves you!

As happy as we might have been for Internet Explorer, though, we have to admit that this question put us in a bit of a quandary. After all, there are several different ways to handle this problem, depending on whether you need Internet Explorer to re-start instantly or whether it’s OK to wait awhile. After pondering the matter for awhile we decided to do what we always do: go with the simplest solution, one that (in this example) checks every 60 seconds to see if any instances of Internet Explorer are running. If there are, then the script simply goes back to sleep and waits another 60 seconds before checking again. If there aren’t any instances of Internet Explorer running the script launches a new copy of Internet Explorer, then takes a quick nap and waits another 60 seconds before checking again.

And, yes, that’s somewhat similar to what a Scripting Guys day is like. Um, except for the part about waking up every 60 seconds in order to do something, if you know what we mean.

Here’s the script we came up with:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set objShell = CreateObject("Wscript.Shell")

Do While True
    Set colProcesses = objWMIService.ExecQuery _
        ("Select * from Win32_Process Where Name = 'iexplore.exe'")
    If colProcesses.Count = 0 Then
        objShell.Run "iexplore.exe"
    End If
    Wscript.Sleep 60000
Loop

That’s right: this is, at heart, just a run-of-the-mill WMI script. Because of that, it starts off by connecting to the WMI service on the local computer. But there is one key difference between this script and other WMI scripts. Usually at this point we say, “But you can also run this script against a remote computer.” Unfortunately, that’s not the case here. Technically you could run the script against a remote computer, but – at least on Windows XP and Windows Server 2003 – any instance of Internet Explorer you started up would run in an invisible window; you wouldn’t be able to see it on the screen. That’s a security feature built into the operating system: processes started remotely always run in a hidden window. Which, in turn, means that this script must be run on the local computer.

Note. Is there a workaround to this problem? As a matter of fact, there is, although it requires you to start the process locally rather than remotely. For an example of how you might do that, see this Hey, Scripting Guy! column.

After connecting to the WMI service we then create an instance of the Wscript.Shell object, which we’ll use to spawn any new instances of Internet Explorer. (And, yes, we could have used WMI for this, but most people find it easier to run programs using Wscript.Shell rather than WMI.) We then set up a Do loop that runs for as long as True is equal to True. (Which, barring any new developments in philosophy, means the script will run forever. To stop the script you’ll need to terminate the script process. If you’re running in a command window under CScript that’s as easy as pressing Ctrl+C or closing the command window.)

So what do we do inside the loop? Well, to begin with, we use this code to retrieve a collection of all the processes named iexplore.exe that are currently running on the computer:

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'iexplore.exe'")

At the risk of spoiling the suspense, that’s going to coincide with a collection of all the instances of Internet Explorer currently running on the computer. We then check to see if the value of the Count property (which tells us how many items are in the collection) is equal to 0:

If colProcesses.Count = 0 Then

If Count is equal to 0 that means there aren’t any instances of Internet Explorer running on the machine. Therefore, we use the Shell object and the Run method to start up a brand-new instance:

objShell.Run "iexplore.exe"

And that’s it. We then use the Sleep method to pause the script for 60 seconds (60,000 milliseconds). When 60 seconds are up, the script will wake up, loop around, and repeat the process, ad infinitum. If 60 seconds isn’t the desired interval you can simply adjust the value. For example, this line of code runs the check every 30 seconds (30,000 milliseconds):

Wscript.Sleep 30000

And this one runs the check every 10 minutes (60,000 milliseconds per minute times 10 minutes):

Wscript.Sleep 600000

Admittedly, that only checks every 10 minutes to see if Internet Explorer is doing something. With the Scripting Guys, there’s no point in checking every 10 minutes to see if they’re doing anything: after all, you already know the answer to that, script or no script.


How Can I Tell if a Server has Rebooted?

Hey, Scripting Guy! Is there any way to tell whether or not a server has rebooted?

-- MvdM

Hey, MvdM. We’re assuming that you’re performing a task and, at the end of the task, the computer is supposed to reboot. Needless to say, you want to know, “Well, did the server reboot?” How can you tell whether the machine restarted or not?

The easiest way is to check the system uptime and see how long the computer has been running. Suppose your procedure was supposed to run at midnight, and when you come to work at 8:00 AM you want to know whether the computer restarted or not. Well, if it did, then the system uptime should be about 8 hours or so. If it is, then the odds are pretty good that the computer rebooted; if the system uptime is appreciably longer than 8 hours, then it probably didn’t reboot.

So how do you determine system uptime? That’s easy: just use the WMI class Win32_OperatingSystem and check the value of the LastBootupTime property. Of course, there are two minor problems with that approach. First, the LastBootupTime property tells you when the computer last restarted; however, it doesn’t tell you how long ago that was. Second, like all WMI date-time properties, LastBootupTime is reported in UTC format, meaning you get back a date that looks like this: 200409070130.000000+480. Yuck.

But that’s OK; just make sure your script converts the UTC date to a standard date-time format, and then use VBScript’s DateDiff function to subtract the current date and time from the last bootup date and time; that will tell you how long the computer has been running since its last reboot. Here’s a sample script that does all those things:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colOperatingSystems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")
For Each objOS in colOperatingSystems
    dtmBootup = objOS.LastBootUpTime
    dtmLastBootupTime = WMIDateStringToDate(dtmBootup)
    dtmSystemUptime = DateDiff("h", dtmLastBootUpTime, Now)
    Wscript.Echo dtmSystemUptime
Next
Function WMIDateStringToDate(dtmBootup)
    WMIDateStringToDate = CDate(Mid(dtmBootup, 5, 2) & "/" & _
         Mid(dtmBootup, 7, 2) & "/" & Left(dtmBootup, 4) _
         & " " & Mid (dtmBootup, 9, 2) & ":" & _
         Mid(dtmBootup, 11, 2) & ":" & Mid(dtmBootup, _
         13, 2))
End Function

Note two important points here. First, we have the function WMIDateStringToDate that converts the UTC date to a standard date-time value. We won’t bother to explain all the code here, but all we’re doing is teasing apart the UTC date-time values and then rearranging them in standard format. For example, with the UTC date 200409070130.000000+480 the first four digits represent the year. Thus our function takes the first four digits – Left(dtmBootup, 4) – and assigns that to the year portion of our standard date. We put the rest of our date-time value together using the same approach.

Second, note the use of the DateDiff function to subtract the last bootup time from the current time. In this example, we’re determining how many hours the server has been running; hence the parameter “h”, which reports time back in hours. Had we wanted to know the time in minutes, we would have used the “m” parameter. All in all pretty easy.


How Can I Use Windows PowerShell to Be Notified When an Application Terminates?

Hey, Scripting Guy! We have an in-house application that, unfortunately, crashes a lot; when that happens we have to go through a lengthy procedure to restart the program. Using Windows PowerShell, is there any way I can be notified when this application crashes?

-- OI

Hey, OI. You know, at first we thoughts to ourselves, we thought, “Hey, we ain’t a-gonna answer none of them there questions today.” And we really wuzn’t a-gonna do it, no sir. But now we is. Ain’t that a kick in the head?

Sorry; we got a little carried away there. It’s just that we’re a bit giddy with excitement this morning. Remember that scene in The Wizard of Oz where everyone dances around singing, “Ding-dong the witch is dead”? Well, we feel the same way: this week the Scripting Editor is down in San Diego attending the Microsoft Management Summit (MMS); that gives us at least a temporary reprieve from her red-pencil reign of terror. For years we’ve been waiting for the chance to misspell words; now we can. For years we’ve been waiting for the chance to use bad grammar; now we can. For years we’ve been waiting for the chance to write entire paragraphs – no, make that entire columns – that don’t make a bit a sense. And now we – well, OK, that we have been doing. But at least now we can write meaningless columns without having her interrupt us every two minutes and ask, “What is this supposed to mean?”

Come on, everybody sing: ding-dong the witch is dead ….

Sorry. We’re still a little giddy.

Interestingly enough, another thing we’ve wanted to do for years is show you how, using Windows PowerShell, you can be notified any time a particular process is terminated. For some reason, however, the Scripting Editor would never let us do that, either. But now there’s no way that she can stop us.

Note. Evil? Well, evil is a pretty strong word. But, yes, we’d have to say that she’s evil.

As it turns out, working with event notifications in Windows PowerShell is a bit tricky; that’s because PowerShell’s Get-WMIObject Cmdlet doesn’t support event notification queries. (What’s an event notification query? See this Scripting Guys webcast for more information.) So why doesn’t Get-WMIObject support event notification queries? To be honest, we don’t know: you-know-who would never let us look into that. For now, however, all that matters is that we can’t use good old-fashioned WMI to receive a notification any time a process terminates. Instead, we have to take advantage of a couple other options available to us.

One of those options involves the Get-Process Cmdlet. In the scenario OI described, we don’t need notification any time a process is terminated; we only need notification when a particular process (for our purposes, Calculator) is terminated. Because of that we can create an event notification script with just three lines of code:

$a = get-process calc
$a.waitforexit()
"Calculator has stopped running."

To begin with, we use the Get-Process Cmdlet to bind to the process with the name Calc. (The name, by the way, is the executable file name minus the file extension.) It’s important to note that we’re assuming we have only one process with the name Calc. What if we have multiple instances of Calculator running? Well, in that case this first line of code will work – we’ll get back a collection consisting of all those instances of Calculator – but the script will fail on line 2. But don’t fret: we’ll show you a more generic process monitoring script in a few minutes.

Once we’ve gotten a handle to the Calc process we then call the WaitForExit() method:

$a.waitforexit()

What’s that going to do? That’s going to cause the script to “block;” that means that the script will simply sit there and wait until the process in question has terminated. When that process does terminate we then use the third and final line of code to echo back the fact that Calculator has stopped running.

Now, this script works just fine; try it and see for yourself. However, that doesn’t mean that the script is perfect; for example, each time Calculator stops the script stops as well. That means you’ll need to restart both Calculator and the monitoring script.

And yes, that is the way the Scripting Editor would make you do things. But remember, the Scripting Editor isn’t here; that means we can show you an alternate approach:

$b = 1

do
{
$a = get-process calc
$a.waitforexit()
Invoke-Item
c:\windows\system32\calc.exe

}
while ($b -eq 1)

What are we doing here? We’re setting up a Do loop that runs as long as the variable $b is equal to 1. (And because $b will always be equal to 1 that means that the loop will run forever and ever.) Inside the loop we again bind to the Calc process and call the WaitForExit() method. But notice what we do if and when Calculator terminates. Instead of echoing a message to the screen we use the Invoke-Item Cmdlet to restart the application:

Invoke-Item c:\windows\system32\calc.exe

And once Calculator restarts we loop around and resume monitoring. We don’t know what OI means by “a lengthy procedure” to restart the in-house application, but if it’s possible to do that programmatically then this might be a better way to go. Of course, in addition to restarting the application we can also report the fact that Calculator had to be restarted, maybe by echoing a message to the screen or writing a note to a log file or the event log or, well, whatever. That’s entirely up to you.

Note. Like we said, the preceding script is designed to run forever; the only way to stop it is to terminate that particular instance of Windows PowerShell. It’s possible to modify the script so that it stops any time you, say, press the Q key on the keyboard, but that’s a more involved procedure than we have time for today.

Like the first script we showed you, our fancy restart-the-process script works only if you have one instance of the target process running; if you have multiple instances of Calculator then you’re going to run into trouble. In addition, this script works only with processes; that’s because the Get-Process Cmdlet includes a WaitForExit() method. That’s not necessarily going to be the case for other entities.

With that in mind let’s take a look at a more generic event monitoring script, one that uses the .NET Framework. First we’ll show you the code, then we’ll briefly describe how the script works:

$a = 0

$timespan = New-Object System.TimeSpan(0, 0, 1)
$scope = New-Object System.Management.ManagementScope("\\.\root\cimV2")
$query = New-Object System.Management.WQLEventQuery `
    ("__InstanceDeletionEvent",$timespan, "TargetInstance ISA 'Win32_Process'" )
$watcher = New-Object System.Management.ManagementEventWatcher($scope,$query)

do
    {
        $b = $watcher.WaitForNextEvent()
        $b.TargetInstance.Name
    }
while ($a -ne 1)

With this script we’re checking, every second, for deleted processes. How do we know we’re going to check every second? Because of the way we configured our System.Timespan object:

$timespan = New-Object System.TimeSpan(0, 0, 1)

As you can see, we included three parameters – 0, 0, and 1 – when creating the new object. The first 0 represents hours, the second represents minutes, and the 1 represents seconds. What if we wanted to check for deleted processes every 15 minutes and 30 seconds? Then we’d use this line of code:

$timespan = New-Object System.TimeSpan(0, 15, 30)

And how do we know we’re looking for processes that have been deleted? Because that’s what we asked for when we set up our WQLEventQuery:

$query = New-Object System.Management.WQLEventQuery `
    ("__InstanceDeletionEvent",$timespan, "TargetInstance ISA 'Win32_Process'" )

See? We’re looking for instances of the __InstanceDeletionEvent class, but only if those instances happen to be processes (that is, members of the Win32_Process class).

Note. Yes, that’s a cursory explanation, at best, of terms like __InstanceDeletionEvent and TargetInstance. But you can take a look at the aforementioned-webcast for more details.

After creating an instance of the System.Management.ManagementEventWatcher class we then go into another endless loop and wait for processes to be terminated. Each time a process ends we use this line of code to report back the process Name:

$b.TargetInstance.Name

Of course, we could get fancier here and issue an alert only if, say, the terminated process happened to be an instance of Calculator. But we’ll let you do that sort of modification yourself.

Oh, and thanks for asking: as a matter of fact, the Scripting Editor is planning to report back on her trip to MMS. Will that report be as interesting and entertaining as this column? Heavens no. But at least every word will be spelled correctly.

Editor’s Note: The really amazing thing about the Scripting Editor is that she carries around a crystal ball (sometimes referred to as a laptop computer) that allows her to keep an eye on the other Scripting Guys from great distances. We won’t discuss the consequences they’ll suffer for their sad attempt to defy the powers of the Scripting Editor, but rest assured it will be…unpleasant. Not that they’ll ever learn their lesson….

And keep an eye out on the Script Center for the Scripting Editor’s riveting account of Microsoft Management Summit 2007 – or at least her riveting account of the weather in San Diego.


How Can I Wait Until One Application Has Quit Before Starting a Second Application?

Hey, Scripting Guy! I have a script that starts two applications; however, the second application can not start until the first program has finished running. How can I ensure that my second program doesn’t start until the first one ends?

-- OR

Hey, OR. Not too long ago the Scripting Guy who writes this column read an article about Walter Mossberg, who happens to write a technology column for the Wall Street Journal. This article speculated that Mossberg, who writes a column twice a week and then performs a few other duties, makes around $1 million a year. If any of you read this same article it probably made you think: “Hmmm, Walter Mossberg writes two columns a week and performs a few other duties, and he makes $1 million a year. The Scripting Guy who writes that other column writes 5 columns a week and performs a few other duties. Does that mean he makes $2.5 million a year?”

To tell you the truth, Microsoft doesn’t like us to divulge salary information. However, we can tell you this: no, the Scripting Guy who writes this column does not make $2.5 million a year. Instead, Microsoft pays him exactly what he’s worth.

That’s right: ouch!

But that’s OK. Sure, Walter Mossberg might get the big bucks, but he never gets the satisfaction of writing a script that runs an application and then waits until that program has finished before it runs a second application:

strComputer = "."

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

Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")

errReturn = objProcess.Create("Notepad.exe", null, null, intProcessID)

Set colMonitoredProcesses = objWMIService. _
    ExecNotificationQuery("select * From __InstanceDeletionEvent " _
        & " within 1 where TargetInstance isa 'Win32_Process'")

Do While True
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    If objLatestProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

errReturn = objProcess.Create("Calc.exe", null, null, intProcessID)

Now, some of you – maybe even Walter Mossberg – might take exception with this script. Why not use a far simpler script, one that looks like this:

Set objShell = CreateObject("Wscript.Shell")

objShell.Run "Notepad.exe", ,True
objShell.Run "Calc.exe"

We actually have two reasons for that. For one, OR told us that he tried a script exactly like the one above, but, for some reason, it didn’t work: when the first process terminated the second process failed to start. We’re not sure why that might be the case, so we decided to try a different, albeit somewhat more complicated, route.

Second, and more important, our script can be used against a remote computer. You just need to do a couple things first. For one, assign the name of the remote computer to the variable strComputer. For another, get rid of the applications Notepad.exe and Calc.exe, and replace them with applications that: 1) don’t require a visible window; and, 2) will run and then automatically terminate themselves. (Scripts are a good candidate for this; so are batch files.)

Why those stipulations? That’s easy: for security reasons you cannot create a visible process on a remote computer. We can start Notepad on a remote machine, but that instance of Notepad will run in a hidden window and the user will never see it. And if the user never sees it it’s going to be awfully hard for him or her to use the application and then terminate it.

Make sense? Well, sadly, that doesn’t matter. Like it or not, that’s just the way it works.

Note. What if you do need to start a GUI application on a remote machine, and what if you do need that application to run in a visible window? For one approach to that problem see this Hey, Scripting Guy! column.

As for the script itself, we begin by connecting to the WMI service on the local computer. We then use this line of code to bind directly to the Win32_Process class:

Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")

Once we’re bound to the Win32_Process class we can start an instance of Notepad by using this line of code:

errReturn = objProcess.Create("Notepad.exe", null, null, intProcessID)

As you can see, we simply call the Create method followed by four parameters:

· Notepad.exe, the name of the executable file (or script) that we want to start. If this file or script is not in your Windows path then make sure you specify the full path name here.

· Null, an optional parameter for specifying a working directory. Because the default directory for Notepad is fine for our purposes we pass a Null parameter, which is the same thing as saying “use the default value.”

· Null, another optional parameter which enables us to specify startup options for the process. Again, the default values are fine for our purposes, so we pass a Null parameter here, too.

· intProcessID, an out parameter. When the script runs and Notepad starts up, the process ID will automatically be assigned to this out parameter. That’s how we can keep track of our copy of Notepad, even if there are multiple copies of Notepad running on the computer.

As soon as Notepad is up and running we set up an event subscription to monitor the __InstanceDeletionEvent class:

Set colMonitoredProcesses = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceDeletionEvent " _
        & " Within 1 Where TargetInstance ISA 'Win32_Process'")

As the name implies, instances of this class are created any time an object is deleted. Of course, we’re not interested in all objects; we’re only interested in process objects. That’s why we add the Where clause that limits our event subscription to objects (the TargetInstance) that are members of (ISA) the Win32_Process class.

Note. OK, so maybe that is a lot of information packed into a single paragraph. For a kindler, gentler introduction to WMI events, see our Scripting Week 2 webcast on this very subject.

That brings us to this block of code:

Do While True
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    If objLatestProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

What we’ve done here is set up an “endless” loop that will pause the script until a process is deleted; that’s what the first two lines of code are for:

Do While True
    Set objLatestProcess = colMonitoredProcesses.NextEvent

When line 2 executes, the script will “block” (pause) until a process object gets deleted (that is, until a process is terminated). When a process does get deleted we then check to see if the process ID of the terminated process is equal to the process ID of the process we created:

If objLatestProcess.TargetInstance.ProcessID = intProcessID Then

Because process IDs must be unique at any given time, if the IDs match that can only mean one thing: the instance of Notepad we created has been terminated. In turn, we then call the Exit Do statement and exit the “endless” loop.

And what if the process IDs don’t match? In that case, that must mean our instance of Notepad is still running; it was some other process that was terminated. Thus we loop around and wait for the next event notification (the next process to be deleted).

When we finally do exit the loop we simply call the Create method and start up our second application, in this case, Calc.exe:

errReturn = objProcess.Create("Calc.exe", null, null, intProcessID)

And then we’re done.

You know, that sounds like a column that’s worth $2.5 million dollars, doesn’t it? Especially when you consider how much other useful tasks the Scripting Guys perform. For example, next week the Scripting Guys will be manning a booth at TechEd 2007. If you’re attending TechEd (and you are attending TechEd, aren’t you?) then swing by the Partners Expo Hall and look for the CMP Media Booth (booth 1301): the Scripting Guys will be there, handing out copies of Dr. Scripto’s Fun Book and giving away Dr. Scripto bobblehead dolls. In addition, the Scripting Guy who writes this column will be counting his money … which means he’ll have plenty of time to chat with anyone who drops by.

Note. So will Walter Mossberg be manning a booth at TechEd 2007? Not as far as we know. $1 million a year and he can’t even man a booth at TechEd. Interesting ….


How Can I Write a Script That Increments a Counter Each Time a Log File is Updated?

Hey, Scripting Guy! How can I write a script that increments a counter each time a particular log file gets updated?

-- HJ

Hey, HJ. You know, lately the Scripting Guy who writes this column has been victimized by a disturbing phenomenon: he keeps getting phone calls from machines. For the past several nights the phone has rung in the Scripting House, and each time there’s been a computer on the other end: “Hello, this is an automated survey being conducted by ….” To tell you the truth, the Scripting Guy who writes this column doesn’t like talking on the phone to begin with; as you might expect, then, he especially doesn’t like talking on the phone to a machine. With that in mind, it probably goes without saying that he’s developed his own “automated response” to these surveys.

And yet, as irritating as this is, these incidents have got him thinking about automated responses in general. Maybe, he thought, it would be possible to create an automated response mechanism for Hey, Scripting Guy! That shouldn’t be too difficult, and it would save him a ton of work. To begin with, the mechanism would need the ability to pick out a few keywords and then use those keywords to determine the question that was being asked. The machine would then have to prepare a response using the traditional Hey, Scripting Guy! format: a series of nonsensical anecdotes, ramblings, and rhetorical questions followed by a script that actually solves the problem. After playing around with this for a little while he managed to come up with a mechanism that produced the following:

Hey, HJ. You know, the Scripting Son pitched quite well last night; in fact,
he had [insert number] strikeouts and only walked [insert number]. Did you see
that new commercial? That has to be the [best/worst] commercial we’ve seen
in a long time. That Scripting Editor, she sure is [mean], isn’t she? As for
Peter Costantini, the oldest living Scripting Guy ….

Well, you get the idea. We have to admit that it’s an intriguing concept, and it is hard to tell the real column from the fake column. But it’s a moot point: after all, even the Scripting Guy who writes this column would never be so lazy as to let a machine do all his work for him.

With that in mind, why don’t … we … take a look at a script that increments a counter each time a particular log file gets updated:

intModifications = 0

strComputer = "."

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

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE " _
        & "TargetInstance ISA 'CIM_DataFile' and " _
            & "TargetInstance.Name='C:\\Scripts\\Test.txt'")

Do
    Set objLatestEvent = colMonitoredEvents.NextEvent
    If objLatestEvent.TargetInstance.LastModified <> objLatestEvent.PreviousInstance.LastModified Then
        intModifications = intModifications + 1
        Wscript.Echo "File modified: " & Now
        Wscript.Echo "Number of modifications: " & intModifications
        Wscript.Echo
    End If
Loop

Now comes the fun part: figuring out how this script actually works. As you can see, we start out in very simple fashion: we assign the value 0 to a variable named intModifications (which, needless to say, is the counter variable we’ll use to keep a running count of each time the file in question gets modified). We then connect to the WMI service on the local computer, although – and stop us if you’ve heard this one before – you can just as easily run the script against a remote computer; all you have to do is assign the name of that computer to the variable strComputer.

That brings us to this crazy-looking chunk of code:

Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
    ("SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE " _
        & "TargetInstance ISA 'CIM_DataFile' and " _
            & "TargetInstance.Name='C:\\Scripts\\Test.txt'")

Believe it or not, this is our event subscription query. We won’t explain the ins and outs of event subscriptions in today’s column; if you need more information on the subject you might want to take a look at our Scripting Week 2 webcast on WMI events. In a nutshell, we’re simply asking the script to check every second (WITHIN 1) to see if any new members of the __InstanceModificationEvent class have been created. (As the name implies, instances of this class are created any time an object is modified.) Of course, objects get modified all the time on a computer: processes use more (or less) CPU power; memory goes up and down; network connections are enabled and disabled. That’s all well and good, except that we don’t want to be notified when any of those things occur. Therefore, we’ve added a couple stipulations to our query:

· We only want to be notified if the new instance (the TargetInstance) of the __InstanceModificationEvent class happens to be a file (CIM_DataFile).

· We only want to be notified if this new file has a Name (path) of C:\\Scripts\\Test.txt.

Note. What’s that? Why the extra \’s in the file path? That’s easy: as it turns out, the \ is a reserved character in WMI. Therefore, any time you use a \ in a Where clause you must “escape” the character; that simply means you have to preface each \ with a second \. As a result, C:\Scripts\Test.txt gets written as C:\\Scripts\\Test.txt.

Still with us? Good. Next we create a Do loop designed to run forever and ever; notice that we don’t include any sort of exit condition (like, say, do until something happens, or do while something is true):

Do

Inside that loop, we then use this line of code to instruct the script to sit tight and wait until the target file (C:\Scripts\Test.txt) has been modified:

Set objLatestEvent = colMonitoredEvents.NextEvent

What happens if the file never gets modified? Nothing; the script will dutifully “block” on that line of code until the universe collapses in on itself. And even then it will continue to wait for the file to be modified.

But what happens if the file does get modified? That’s going to do two things. First, it’s going to create a new member of the __InstanceModificationEvent class. Second, because this new class member meets our event subscription criteria, that’s going to cause the script to unblock. And that, in turn, causes the script to execute this line of code:

If objLatestEvent.TargetInstance.LastModified <> objLatestEvent.PreviousInstance.LastModified Then

Depending on how you want to look at things, this line of code might be optional. In testing the script we discovered that just opening the file, even without making a single change to it, triggered a new instance of the __InstanceModificationEvent class. We didn’t like that; that made it difficult to separate times when someone actually modified the file from times when someone merely looked at the file. To get around that problem, we use the preceding line of code to determine when the file was last modified (something we do by checking the value of the LastModified property). In particular, what we do is compare the LastModified property for the latest version of the file (the TargetInstance) with the LastModified property of the previous version of the file (PreviousInstance); that is, we compare the file as it exists right now with the file as it existed before the new member of the __InstanceModificationEvent class was created. If the two dates and times match then we know that the file wasn’t actually modified.

And if they don’t match that means that a change of some kind was made. Therefore, we run this little block of code:

intModifications = intModifications + 1
Wscript.Echo "File modified: " & Now
Wscript.Echo "Number of modifications: " & intModifications
Wscript.Echo

Here we start off by incrementing the counter variable intModifications by 1; that’s because a new modification just took place. We then echo back two things: 1) the fact that the file has been modified, along with the current date and time (Now); and 2) the total number of modifications that have been made (intModifications). And then we loop around and repeat the process all over again.

All in all, so easy a, uh, machine could have done it.

A couple of things to keep in mind here. First, we recommend that you run this script in a command window under the CScript script host. Why? Because that way the script will be able to write its notifications to the command window and keep running. If you run it under WScript, you’ll need to dismiss several dialog boxes each time the file gets modified. Yuck.

Second, tallying takes place only while the script is running; as you probably guessed if the script is terminated then you won’t be able to keep track of modifications. (Plus the current tally will be erased forever.) Because of that, you might want to modify the script so that, instead of echoing information to the screen, it writes its tally to a text file. In addition, you might want to think about turning this into a permanent event consumer, something that would enable WMI to track file modifications without even needing a script. That’s not necessarily hard, but it can be a little tricky. For more information on permanent event consumers, see the WMI SDK on MSDN.

And now for the big question: who did write today’s column? Was it the Scripting Guy who typically writes this column, or was it some cold, unthinking machine that parsed the question as best it could, began its reply with a series of totally unrelated and irrelevant anecdotes, and then simply parroted back a script it probably copied off the Scripting Editor’s computer?

You know, when you put it like that it is hard to tell, isn’t it?


Why Does My Performance Monitoring Script Keep Returning the Same Incorrect Values?

Hey, Scripting Guy! I’m using a script to monitor process performance, but every time I run the script the percent processor time for all the processes comes out as 0. What am I doing wrong?

-- JL

Hey, JL. We’ve simplified your script a bit to better illustrate the problem you’re running into. (And don’t feel bad: this is a very common problem people encounter when trying to measure performance counters over time.) Here’s a modified version of your script, one that returns the values of the Name and PercentProcessorTime properties for each process running on a computer:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_PerfFormattedData_PerfProc_Process")

For i = 1 to 5
    For Each objItem in colItems
        Wscript.Echo objItem.Name & " -- " & objItem.PercentProcessorTime
    Next
    Wscript.Echo
    Wscript.Sleep 5000
Next

When you run this—and no matter how many times you run it—you get back output that looks like this:

Idle -- 0
System -- 0
smss -- 0
csrss -- 0
winlogon -- 0
services -- 0
lsass -- 0

So what’s the deal? Are your processes really using no processor time whatsoever (that’d be nice, wouldn’t it?) or is the Win32_PerfFormattedData_PerfProc_Process class simply broken?

As it turns out, neither one. Instead, here’s the problem. You begin by using ExecQuery to return performance counter data for all the processes and their properties. So far so good, with one minor caveat: for some reason, more often than not the first time you query a performance monitoring class you’ll get back either 0 or Null for all the property values. Why? To be honest, we don’t know; that’s just the way it works.

But that’s OK, because the next time you query a performance monitoring class (within the same script, at least) you will get back correct values. The only problem is that you never query Win32_PerfFormattedData_PerfProc_Process a second time. Take a look at your script again. You query the Win32_PerfFormattedData_PerfProc_Process class, and then you immediately enter a For Next loop that loops around 5 times. However, you never reissue your query while inside the loop. When you first query Win32_PerfFormattedData_PerfProc_Process the PercentProcessorTime for all your processes is likely to be 0. Because you never reissue your query, the property value never changes. That’s why you get 0 each time.

In fact, getting 0 is more an artifact of the way the performance monitoring classes work than anything else. Suppose you got a 77 instead. In that case each time through the loop the value would still be reported as 77, even if the process had long since been terminated. That’s because you’re still working with the original property values, not the current property values. Does that make sense? Imagine if back in 1790 we queried the United States for a list of all the states in the Union. And suppose we never issued that query again. If anyone ever asked, we’d simply report back the original 13 states. That’s because we never went out and got updated information.

So how do you fix the problem? Well, the best way to do this is to use the new SWbemRefresher object, found only on Windows XP and Windows Server 2003. (Which isn’t really a problem seeing as how the Win32_PerfFormattedData_PerfProc_Process and other formatted performance classes are also found only on Windows XP and Windows Server 2003.) We won’t explain the workings of SWbemRefresher in any detail today; for more information, see the WMI SDK on MSDN. However, here’s a revised script that will likely report null values the first time through the loop, but from then on will report correct processor use values:

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

Set objRefresher = CreateObject("WbemScripting.SWbemRefresher")
Set colItems = objRefresher.AddEnum _
    (objWMIService, "Win32_PerfFormattedData_PerfProc_Process").objectSet

For i = 1 to 5
    objRefresher.Refresh
    For Each objItem in colItems
        Wscript.Echo objItem.Name & " -- " & objItem.PercentProcessorTime
    Next
    Wscript.Echo
    Wscript.Sleep 5000
Next

This time around your output will look something like this:

OUTLOOK -- 5
WINWORD -- 23
cmd -- 0
iexplore -- 3
MSNGather -- 1
IPClient -- 0
cscript – 1

So what did we do different in this script? Well, we created an instance of the SWbemRefresher object and then added the Win32_PerfFormattedData_PerfProc_Process class to this instance; all that happens with these lines of code:

Set objRefresher = CreateObject("WbemScripting.SWbemRefresher")
Set colItems = objRefresher.AddEnum _
    (objWMIService, "Win32_PerfFormattedData_PerfProc_Process").objectSet

We then create a For Next loop, just like we did before. But notice the first line of code in our For Next loop:

objRefresher.Refresh

The Refresh method instructs SWbemRefresher to go out and grab the current values for any classes we’ve told it to keep track of. (And, as you recall, we used the AddEnum method to tell SWbemRefresher to keep track of the Win32_PerfFormattedData_PerfProc_Process class.) The Refresh method will grab the current values for all the properties of the Win32_PerfFormattedData_PerfProc_Process class and echo those to the screen. When we loop around the next time, the Refresh method will, again, go out and grab the current values and echo those to the screen. In other words, we’re essentially issuing a new query each time, and thus consistently grabbing the latest values.

Hopefully you’re not confused by all this; it actually makes sense once you catch on to how it works. Give it a try and you should be able to figure out it out. In the meantime, we’ll see what we can do about getting some additional documentation on SWbemRefresher posted to the Script Center.


Why Doesn't My Ping Script Run on Windows 2000 Computers?

Hey, Scripting Guy! I have a script that pings a computer before it tries to connect to it. The script works fine on Windows XP, but doesn’t work at all on my Windows 2000 computers. Do you have any idea why?

-- AK

Hey, AK. As a matter of fact, we do have an idea why. Here’s a simplified version of your script, which pings the computer with the IP address 192.168.1.1:

strComputer = "192.168.1.1"
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_PingStatus " & _
        "Where Address = '" & strComputer & "'")
For Each objItem in colItems
    If objItem.StatusCode = 0 Then
        WScript.Echo "Reply received."
    End If
Next

Why does this work on Windows XP but not Windows 2000? Simple: the Win32_PingStatus class is supported on Windows XP and Windows Server 2003, but not on any other versions of Windows. If you run this script on an XP machine it works; that’s because XP knows what the Win32_PingStatus class is. If you run it on Windows 2000 it fails; that because Windows 2000 has no idea what you’re talking about when you reference a class named Win32_PingStatus. Why, Windows 2000 has never even heard of such a thing!

Fortunately, there’s a way to work around this. After all, you don’t need the Win32_PingStatus class to ping a computer; Ping.exe works just fine. In this revised script, we just run Ping.exe, capture the output, and then check to see if the word Reply is found anywhere within that output. If it is, we assume that a reply was received from the remote machine. That’s not a 100% foolproof method, but it’ll work most of the time:

Set objShell = CreateObject("WScript.Shell")
Set objExecObject = objShell.Exec _
    ("%comspec% /c ping -n 3 -w 1000 192.168.1.1")
Do While Not objExecObject.StdOut.AtEndOfStream
    strText = objExecObject.StdOut.ReadAll()
    If Instr(strText, "Reply") > 0 Then
        Wscript.Echo "Reply received."
    Else
        Wscript.Echo "No reply received."
    End If
Loop

This script uses the Windows Script Host Exec method, which was introduced with WSH 5.6. Why do you care about that? Well, that means, if you haven’t done so already, you’ll have to upgrade to WSH 5.6 to get this script to run on your Windows 2000 computers. For more information about downloading and installing WSH 5.6, click here. And if you’d like to know a little bit more about the Exec method, check out this portion of the Microsoft Windows 2000 Scripting Guide.

Of course, if for some reason you can’t install or don’t want to install WSH 5.6 (and we’ve actually encountered a few people in that position), well, even then there’s no reason to despair. Here’s a script that will work on WSH 2.0. It’s a bit crazy, to say the least: it pings a computer, saves the output to a temporary text file, opens the text file and looks for the word Reply, deletes the file, and then reports the results. But, crazy or not, it’ll still do the trick:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("Wscript.Shell")
objName = objFSO.GetTempName
objTempFile = objName
objShell.Run "cmd /c ping -n 3 -w 1000 192.168.1.1 >" & _
    objTempFile, 0, True
Set objTextFile = objFSO.OpenTextFile(objTempFile, 1)
Do While objTextFile.AtEndOfStream <> True
    strText = objTextFile.ReadLine
    If Instr(strText, "Reply") > 0 Then
        Wscript.Echo "Reply received."
        Exit Do
    End If
Loop
objTextFile.Close
objFSO.DeleteFile(objTempFile)


How Can I Add the Contents of a Group of Text Files to an Access Database?

Hey, Scripting Guy! I have a folder that has hundreds of text files in it. I’d like to take each of those files and add the contents (as a memo field) to an Access database. How can I do that?

-- KB

Hey, KB. You know, a few years ago the Scripting Guy who writes this column gave a talk at a conference in Orlando. (And yes, he’ll be back in Orlando June 4-8 for TechEd 2007 … assuming, of course, that the city’s restraining order really has expired.) Because it’s no fun to go to Orlando by yourself, he did some negotiating, and ended up taking along his then 10-year-old son and one of his son’s friends. (Yes, he did take the two boys out of school for a week. But, in his view, at least, education is highly-overrated: after all, the Scripting Guy who writes this column has a master’s degree, and look where that got him.)

As it turned out, the trio happened to arrive in Florida during the closest thing Orlando has to an off-season. One evening they were at Disney World and, about an hour or so before the park was due to close, rode Splash Mountain. When the ride was over they realized that there was no one waiting in line; naturally, they walked around, got back on the ride, and rode again. When they got off, they noticed the same thing: pretty much everyone had already gone home for the day. And so they continued riding Splash Mountain, over and over again, until the folks at Disney finally told them it was time to go.

The moral of the story? There are some things that you just never get tired of; you’re willing to do them over and over and over again. For some people, that means riding rides at an amusement park; for most of us, though (including those who breathlessly followed the Hey, Scripting Guy! columns of May 17 and May 18) that means writing scripts that work with text files. In particular, that means writing scripts that work with all the text files in a folder:

Const ForReading = 1
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

Set objFSO = CreateObject("Scripting.FileSystemObject")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb"

objRecordSet.Open "SELECT * FROM TextFiles" , _
    objConnection, adOpenStatic, adLockOptimistic

strComputer = "."

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

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

For Each objFile In colFileList
    Set objTextFile = objFSO.OpenTextFile(objFile.Name, ForReading)
    strContents = objTextFile.ReadAll
    objTextFile.Close

    objRecordSet.AddNew
    objRecordSet("FileName") = objFile.Name
    objRecordSet("FileContents") = strContents
    objRecordSet.Update
Next

objRecordSet.Close
objConnection.Close

We’ll give everyone a moment to catch their breath and then we’ll see if we can figure out how this script works. We start out by defining a pair of constants, ForReading and adLockOptimistic. ForReading is the constant we’ll use to specify the file mode (for reading) when we open all the text files; adLockOptimistic is the constant we’ll use to indicate how we want to lock our recordset when working with the database. (Optimistic locking simply means that we’ll only lock the recordset when calling the Update method.) After defining the two constants we then create a pair of database objects: ADODB.Connection and ADODB.Recordset. From there we go ahead and create an instance of the Scripting.FileSystemObject, the object used to read text files. We won’t need this object right away, but this seemed as good a time as any to create it. (Fortunately it won’t go stale if we don’t use it immediately.)

Note. Before you ask, no, we’re not going to explain much about databases and database objects in today’s column. If you could use a refresher course on working with databases you might take a look at our Scripting Guys webcast Database Scripting for System Administrators.

Unlike the FileSystemObject, we’re going to immediately put our two database objects to use. First, we use the Connection object to connect us to the database C:\Scripts\Test.mdb:

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb"

Once we’ve made that connection we use the Recordset object to retrieve a collection of all the records found in the TextFiles table:

objRecordSet.Open "SELECT * FROM TextFiles" , _
    objConnection, adOpenStatic, adLockOptimistic

As you’ve probably already figured out, TextFiles is the name of the table where we plan to add the new records. For today’s script we’re assuming that the TextFiles table already exists. However, you could modify this script so that it first creates the new table and then adds records to this brand-new table. For more information on programmatically creating database tables, see the webcast we mentioned a few minutes ago.

Now that we have a database connection our next task is to grab hold of all the text files. To do that we first use this block of code to connect to the WMI service on the local computer:

strComputer = "."

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

And yes, due to limitations with the FileSystemObject, this script is designed to work only on the local computer. That doesn’t mean that you can’t ever, under any circumstances, read text files stored on a remote machine; it just means you’ll have to make some modifications to the script before you can do so. What kind of modifications? Why, the ones we discuss in this other column, of course.

You know, now that you mention it, there are a lot of links and see-alsos in today’s column, aren’t there? That couldn’t possibly mean that the Scripting Guy who writes this column is getting lazy?

Nah.

OK; what’s next? Well, next we use this bit of code to retrieve a collection of all the files found in the folder C:\Archive:

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

At this point, and at long last, we’re finally ready to roll: we’re ready to start reading text files and to start adding the contents of those files to our database.

Speaking of which, we should note that we’re using a very simple database table, one that includes just two fields:

· FileName, a text field where we’ll store the path of the file we opened.

· FileContents, a memo field where we’ll store the contents of the file we opened.

Just something we thought you should know.

OK, now it’s time to start adding records to the database. Thanks to our WMI query we have a collection of all the files in the folder C:\Archive, a collection we stashed away in a variable named colFileList. With that in mind we next set up a For Each loop to loop through each and every one of these files:

For Each objFile In colFileList

The first thing we do inside this loop is use the OpenTextFile method to open file No. 1 in the collection:

Set objTextFile = objFSO.OpenTextFile(objFile.Name, ForReading)

Notice that we pass OpenTextFile two parameters: objFile.Name, a WMI property that returns the complete path to the file; and the constant ForReading (because we want to read from this file). We then use the ReadAll method to read the contents of the file into a variable named strContents:

strContents = objTextFile.ReadAll

And once we have the contents stashed safely away in memory we call the Close method and close the file.

That brings us to the following block of code:

objRecordSet.AddNew
objRecordSet("FileName") = objFile.Name
objRecordSet("FileContents") = strContents
objRecordSet.Update

This – in case you were wondering – is the exciting, thrill-ride portion of today’s column. (Yes, we know: today’s entire column does seem like a thrill ride, doesn’t? But this is the really good part.) As you can see, the first thing we do here is call the Recordset object’s AddNew method; that’s going to add a new, blank record to our database table. We then use these two lines of code to assign the file path (Name) to the FileName field, and the contents of the file (strContents) to the FileContents field:

objRecordSet("FileName") = objFile.Name
objRecordSet("FileContents") = strContents

And then we call the Update method to actually write this new record to the database. Don’t forget to call this method; if you do forget, none of your changes (that is, none of your new records) will be added to the database.

Which, of course, would sort of defeat the purpose of the script.

After calling the Update method for file No. 1 we loop around and repeat the process with the next file in the collection (that is, the next file in the folder). Sooner or later we’ll have opened and read all the files in the collection; when that happens we automatically exit the For Each loop, then use these two lines of code to sever our connection to the database:

objRecordSet.Close
objConnection.Close

At that point our script, like all good things, comes to an end.

We hope that helps, KB. We also hope that if you (or anyone else) happens to be in Orlando June 4-8 that you’ll stop by the Scripting Guys booth and say hi. And don’t worry: you won’t be intruding on any family time. This time around the Scripting Son and his friend won’t be going; instead, the Scripting Guy who writes this column has the … privilege … of attending the conference alongside Scripting Guy Jean Ross. They say that it’s impossible to go to Orlando and not have a good time. But, then again, they never met Scripting Guy Jean Ross, did they?

Note. No, hey, we’re just kidding; Jean’s is the perfect person to go to Orlando with. Well, other than the fact that she won’t go on scary amusement park rides like Dr. Doom’s Fear Fall. And the fact that she won’t go to a water park like Wet ’n Wild. And the fact that she won’t – well you get the idea. If you do attend TechEd and you do swing by the Scripting Guys booth, could you wake Scripting Guy Greg Stemp up for us? Thanks.

Editor’s Note: In other words, Scripting Guy Jean Ross won’t go on any rides that might make her throw up, or go anywhere where she’d have to see her coworkers in bathing suits (which might also fall into that first category). And Scripting Guy Greg Stemp will be riding one ride over and over again, then sleeping through TechEd. You probably can’t wait to meet these people, can you?


How Can I Compact an Access Database?

Hey, Scripting Guy! How can I compact an Access database using a script?

-- DA

Hey, DA. We have to tell you the truth: we are thrilled and excited about getting the chance to answer your scripting question. To be honest, that isn’t always the case, most days we’d look at a question like this and think, “Compact an Access database? That sounds hard; why would we want to do that?’” But that was before last night, when the Scripting Guy who writes this column heard the most dreaded words in the English language: “Dad, can you help me with my math homework?”

Note. If you don’t have children, there are a few rough equivalents to “Can you help me with my math homework?” including “Could I see you in my office for a moment?” and “I hope you didn’t cut a piece of that cake that I had sitting on the counter.”

As it turns out, compacting an Access database is way easier that determining the real zeroes of polynomial functions. For one thing, the Scripting Guy who writes this column had only the vaguest memories of polynomial functions. For another, back when he took high school math the zero hadn’t even been invented yet; in fact, no one had ever found a use for zero, at least not until they had to start scoring the Scripting Guy’s math homework. Suddenly, there was a need for zero after all!

Note. Over time, of course, people found more uses for zero; for example, it turns out to be incredibly important when calculating the Scripting Editor’s batting average.

Somehow or another, the Scripting Guy and his son managed to slog through the assignment; in fact, as near as they could tell they even managed to come up with the right answers to boot. (The Scripting Guy who writes this column had forgotten that homework is much harder if you try to get the right answers.) Having experienced a taste of life outside the world of scripting, however, the Scripting Dad vowed that he would never again complain about having to answer scripting-related questions. Let’s face it: scripting questions are nothing compared to determining the real zeroes of polynomial functions.

If you don’t believe us (and it is hard to believe that anything could be easier than determining the real zeroes of polynomial functions), take a look at this piece of code, a script that compacts (and, if necessary, repairs) the Access database C:\Scripts\Test.mdb:

Const CreateLog = True

Set objAccess = CreateObject("Access.Application")

errReturn = objAccess.CompactRepair _
    ("c:\scripts\test.mdb", "c:\scripts\test2.mdb", CreateLog)

Wscript.Echo "Compact/repair succeeded: " & errReturn

Granted, it’s not as exciting as factoring x4 + 29x + 100, although it’s close. We start out by defining a constant named CreateLog and setting the value to True. When we compact the database we’ll use this constant to tell the script to create a log file if any corruption is detected in the database. As far as we know (and, to be honest, our knowledge here rivals our knowledge of synthetic division) if a database is corrupt the script will create a log file in the same folder as the corrupted database. If the database is not corrupt then no log file will be created.

Of course, the Scripting Guys would never have anything to do with a corrupted database; therefore we can’t verify that this is actually the case. But that’s what the documentation says, and when has Microsoft documentation ever been wrong?

Note. Don’t bother sending in examples of when Microsoft documentation has been wrong. After all, our email Inbox doesn’t have unlimited storage capacity.

After defining the constant we create an instance of the Access.Application object. We then use this line of code to compact (and, if necessary, repair) the database C:\Scripts\Test.mdb:

errReturn = objAccess.CompactRepair _
    ("c:\scripts\test.mdb", "c:\scripts\test2.mdb", CreateLog)

As you can see, the CompactRepair method takes three parameters: the path to the database we want to compact (C:\Scripts\Test.mdb); the path to the new, compacted database we’re about to create (C:\Scripts\Test2.mdb); and the constant CreateLog. When we execute this line of code the script attempts to compact the database, with the results (True if the operation succeeded, False if the operation failed) being stored in the variable errReturn. All we have to do now is echo back the value of errReturn and we’re done:

Wscript.Echo "Compact/repair succeeded: " & errReturn

That was easy, wasn’t it? However, there is one drawback to this script: if the file C:\Scripts\Test2.mdb already exists then the script will fail. That’s because CompactRepair will not overwrite an existing file. With that in mind we offer a slightly-modified version of the script, one that uses the FileSystemObject to check for the existence of Test2.mdb and, if necessary, deletes it. Here’s what that modified script looks like:

Const CreateLog = True

Set objAccess = CreateObject("Access.Application")
Set objFSO = CreateObject("Scripting.FileSystemObject")

If objFSO.FileExists("c:\scripts\test2.mdb") Then
    objFSO.DeleteFile("c:\scripts\test2.mdb")
End If

errReturn = objAccess.CompactRepair _
    ("c:\scripts\test.mdb", "c:\scripts\test2.mdb", CreateLog)

Wscript.Echo "Compact/repair succeeded: " & errReturn

Admittedly, it would be a bit of a stretch to say that the Scripting Guy who writes this column enjoyed being reacquainted with polynomial functions. However, he was intrigued by the opportunity to work with imaginary numbers again. When dealing with polynomial functions you sometimes have to take the square root of a negative number. That’s a problem: negative numbers don’t have square roots. But mathematicians aren’t deterred by that: if they can’t use real numbers then they simply use imaginary numbers instead. The Scripting Guys are currently working on a scripting equivalent: if you can’t use a real scripting method to carry out a task we’re trying to develop imaginary scripting methods that you can use instead. We’ll keep you posted on that.

Note. Of course, we’ll likely report the results in an imaginary scripting column, which means we aren’t totally sure how you’ll find that information. But we’ll worry about that later.


How Can I Determine Which Version of Access was Used to Create a Database?

Hey, Scripting Guy! How can I determine which version of Access was used to create an Access database?

-- TW

Hey, TW. You know, at least one of the Scripting Guys is a bit old-fashioned, to say the least. He doesn’t own a cell phone, and he won’t use drive-through windows. He wears his baseball hat forwards rather than backwards (!), and he absolutely refuses to buy books online.

That last one is particularly shocking: any time he wants a new book, he goes to a bookstore to buy it! (It is hard to believe that people still live that way, isn’t it?) But that’s because this Scripting Guy is a firm believer in serendipity, in the art of accidental discovery. He likes “brick-and-mortar” bookstores because, while wandering through the aisles, he might suddenly discover a really cool book, a book he would never have found online. (And, no, not even with all those new-fangled computer programs that can alert you to the fact that a book exactly like the last 30 books you bought has just been published.)

As it turns out, serendipity can be useful in scripting as well. When we first received this email, we immediately turned to the Microsoft Access documentation, figuring this would be an easy problem to solve. Unfortunately, though, we couldn’t find anything having to do with versioning. And so we set this question aside, chalking it up as one of those things that you’d think would be scriptable, but isn’t.

But then serendipity entered the picture. While looking up something else in the Access documentation (which we couldn’t find either; maybe we just don’t know how to use documentation) we ran across a property named FileFormat. You guessed it: FileFormat (rather than, say, Version) is the property that tells us which version of Access was used to create a database. Want to know which version of Access was used to create the database C:\Scripts\Test.mdb? Here you go:

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

intFormat = objAccess.CurrentProject.FileFormat

Select Case intFormat
    Case 2 Wscript.Echo "Microsoft Access 2"
    Case 7 Wscript.Echo "Microsoft Access 95"
    Case 8 Wscript.Echo "Microsoft Access 97"
    Case 9 Wscript.Echo "Microsoft Access 2000"
    Case 10 Wscript.Echo "Microsoft Access 2003"
End Select

Note.By default, Access will present you with a dialog box asking if you really want to open the database. To bypass that warning, set your macro security level to Low.

We begin by creating an instance of the Access.Application object, then we use the OpenCurrentDatabase method to open the file C:\Scripts\Test.mdb. (As you can see, the path is the sole parameter we pass to OpenCurrentDatabase.) We then use this line of code to get the value of the FileFormat property (which, technically, is a property of the CurrentProject object) and store that value in a variable named intFormat:

intFormat = objAccess.CurrentProject.FileFormat

All that’s left now is to set up a Select Case statement to examine the value of intFormat and echo back the corresponding version of Microsoft Access. For example, suppose intFormat is equal to 9. In that case, this line of code will be triggered and “Microsoft Access 2000” will be returned as the version of the application that created the database:

Case 9 Wscript.Echo "Microsoft Access 2000"

We’d like to see you find an answer like that online!

Oh, right: if you’re reading this column then we guess you did find the answer online, didn’t you? Hmmm ….

By the way, you might have noticed that we started an instance of Access but then never specifically quit that instance. Does that mean we have an orphaned copy of Microsoft Access running on our computer? No. As it turns out, Access works a bit different from Word or Excel: when you instantiate Access within a script, that instance will terminate at the same time the script does. There’s a way to work around that if need be, but that’s a topic we’ll have to address some other time.


How Can I Import a .CSV File into an Access Database?

Hey, Scripting Guy! How can I import a .CSV file into an Access database?

-- CS

Hey, CS. We’ll get to your question in just a minute. First, though, a few thoughts leftover from Super Bowl XLI:

Overall the Scripting Guy who writes this column thought that the game was OK, although it did buttress his belief that college football is far more entertaining than pro football. After all, the Colts completely dominated the game, yet could have easily lost, in large part to the fact that they always settled for field goals rather than going for it on, say, fourth-and-goal from the 2. At one point, the Colts even punted from the 36-yard line. The fact is, the NFL is just too conservative. The Scripting Guy who writes this column foresees the day when the Super Bowl will consist entirely of the two teams punting back and forth to one another until someone either runs the kick back for a touchdown or fumbles the ball away in the end zone.

Note. Of course, maybe that’s a good thing: after all, when the day comes that the team most afraid to take a chance is guaranteed to win then our very own Seattle Seahawks will finally be Super Bowl champions.

Let’s see, what else? Here’s something: the Scripting Guy who writes this column has never quite understood the hype surrounding Super Bowl TV commercials, and this year’s batch of ads didn’t help matters any. In several cases he watched a commercial and then thought, “Why would that make me want to buy their product?” In at least one case he watched a commercial and thought, “What exactly is their product?” And at least one commercial that aired was a year or two old. But we suppose that when you spend $2.5 million to buy air time, you probably don’t have enough money left over to pay for a new commercial.

Interestingly enough, there was one commercial that made a big impact on the Scripting Guy who writes this column, although perhaps not the impact that was intended. In this ad a robot on a car assembly line drops a bolt and is immediately fired. He tries to get another job, but it’s no good: he builds cars, he doesn’t work at fast-food restaurants. At the end he’s about to jump off a bridge when, fortunately, he wakes up and realizes that it was all a dream.

Admittedly, this was a well-made tear-jerker: you couldn’t help but feel sorry for the little robot. But, considering the fact that American car companies have been struggling in recent years, this definitely sends an interesting message to US autoworkers: make one little mistake and you’re fired. And guess what? You build cars, which means that you’re not going to be able to find another job somewhere; the only thing you’ll be able to do is jump off a bridge and end it all.

That should boost employee morale, shouldn’t it?

Finally, no, the Scripting Guy who writes this column didn’t watch the halftime show. He did, however, see a minute or two of the pre-game show. But once he saw the fake referees riding around on fake ostriches he decided to go see if there were any clothes that needed washing. And, if there weren’t, to go roll around in the mud and make some clothes that needed washing.

Note. For some reason, prior to Super Bowl LXI any time the Scripting Guy who writes this column thought about American football the words “Cirque de Soleil” never once popped into his head.

Now, does any of this help you import a .CSV (comma-separated values) file into an Access database? Probably not. But this should:

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const ForReading = 1

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = c:\scripts\test.mdb"

objRecordSet.Open "SELECT * FROM Employees", _
    objConnection, adOpenStatic, adLockOptimistic

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

Do Until objFile.AtEndOfStream
    strEmployee = objFile.ReadLine
    arrEmployee = Split(strEmployee, ",")

    objRecordSet.AddNew
    objRecordSet("EmployeeID") = arrEmployee(0)
    objRecordSet("EmployeeName") = arrEmployee(1)
    objRecordSet("Department") = arrEmployee(2)
    objRecordSet.Update

Loop

objRecordSet.Close
objConnection.Close

To be honest we don’t know of a straightforward method for importing a comma-separated values file into an Access database. (We aren’t saying that there isn’t one; we just don’t know about it.) However, it’s pretty easy to use a script to open the .CSV file and then “manually” add new records to the database. Which is exactly what we’ve done here.

We’re assuming, CS, that you already have the Access database created; for our sample script we’re using a database named C:\Scripts\Test.mdb. Inside that database we have a table named Employees; this table consists of three fields:

· EmployeeID

· EmployeeName

· Department

As you might guess, our text file (C:\Scripts\Test.txt) also consists of three fields. Thus:

1989,Ken Myer,Finance
1990,Pilar Ackerman,Research
1991,Jonathan Hass,Human Resources

In this case, we have three new employees to add to the database. And here’s how we do that.

We start out by defining three constants: adOpenStatic and adLockOptimistic, which are used for working with the database; and ForReading, which is used for working with the text file. We then use the following block of code to open Test.mdb and to create a recordset consisting of all the items in the Employees table:

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = c:\scripts\test.mdb"

objRecordSet.Open "SELECT * FROM Employees", _
    objConnection, adOpenStatic, adLockOptimistic

In case you’re wondering, almost all of the preceding code is boilerplate code that can be used as-is. About the only things you might need to change are the path to the database file and the name of the table.

After opening the database our next step is to open the text file. To do that we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

Now we’re ready to start adding records to the database. To begin with, we set up a Do Until loop that runs until we reach the end of the text file (that is, until the AtEndOfStream property is True). Inside that loop we use the ReadLine method to read the first line from the text file and store that value in a variable named strEmployee:

strEmployee = objFile.ReadLine

What does that mean? That means that, at the moment, strEmployee is equal to this:

1989,Ken Myer,Finance

That’s nice, but it doesn’t do us a lot of good: we need to parse the individual field values from that string. Fortunately, we can do that by using the Split function and splitting the string on the comma:

arrEmployee = Split(strEmployee, ",")

What does that give us? That gives us an array (named arrEmployee) consisting of the following three items:

· 1989

· Ken Myer

· Finance

And guess what? Those three items correspond quite nicely to the three fields in our database.

In turn, that means we can now add Ken Myer to the database. To do that we call the AddNew method to add a new record to the Employees table. We then specify the field values using this code:

objRecordSet("EmployeeID")
= arrEmployee(0)
objRecordSet("EmployeeName")
= arrEmployee(1)
objRecordSet("Department") = arrEmployee(2)

There’s nothing too complicated here. In line 1 we’re simply setting the value of the EmployeeID field to the value of item 0 in our array. (Remember, in a VBScript array the first item always has an index number of 0.) In line 2, we set the value of the EmployeeName field to the value of item 1 in our array (the second item). And so on and so on.

After assigning all the field values we then write the new record to the database by calling the Update method:

objRecordSet.Update

From there we loop around and repeat the process with the next line in the text file. And so on and so on.

Now, where were we? Oh, right, the Super Bowl. The truth is, for the Scripting Guy who writes this column the best part of the Super Bowl is listening to all the … expert .. commentators. For example, for the past two weeks everyone has remarked that Indianapolis quarterback Peyton Manning could not be considered a great quarterback until he won a Super Bowl. (Interesting side note: All these same commentators consider Hall of Famers Dan Marino and Warren Moon great quarterbacks. Try to guess how many Super Bowls these great quarterbacks won.) Having finally won his Super Bowl what did the commentators have to say about Peyton Manning? “Winning one Super Bowl is nice. But to be considered a truly great quarterback you have to win at least two Super Bowls.”

Maybe Peyton Manning should just jump off a bridge and get it over with.

Interesting side note 2. These same commentators all consider Brett Favre of the Green Bay Packers a great quarterback, maybe the best quarterback of all time. So, of course, that means that Brett Favre won at least two Super Bowls, right? Uh, probably. We’ll have to get back to you on that ….

Editor’s side note. Forget Super Bowl commercials. For the past few years the Daytona 500 commercials have blown away the Super Bowl commercials.


How Can I Import an Excel Spreadsheet Into an Access Database?

Hey, Scripting Guy! How can I import an Excel spreadsheet into an Access database?

-- IE

Hey, IE. Wait! Don’t tell us; let us guess. Just a moment … we’re picking up the vibrations now … the sensation is getting stronger and more distinct … OK, got it: you’d like to know how to import an Excel spreadsheet into an Access database, wouldn’t you? Are we right? That’s what we thought.

Granted, some of you don’t seem too impressed by that. “Big deal,” you say. “Anyone could ‘predict’ what question IE was going to ask; all you have to do is read the question at the start of the column.” Oh ye of little faith. Sure, it’s true that we could have simply read IE’s question ahead of time and then made a “prediction.” However, how do you explain this: in yesterday’s column we predicted that today we would cover importing an Excel spreadsheet into an Access database. It’s true; just take a look at this quote from the April 3rd column:

“Does that mean that we could use a similar script to import data? As a matter of fact it does, and in tomorrow’s column we’ll show you how to do that very thing.”

That’s OK, no need to apologize; we’re used to dealing with skeptics. And for those of you wondering how we were able to make such an uncanny prediction, well, to tell you the truth, we don’t know; it’s just a gift. But don’t worry: the Scripting Guys vow to use this gift only for good, never for evil.

Well, maybe a little evil. But not much.

Of course, throughout the ages seers and psychics have been a dime-a-dozen; does the name Nostradamus ring a bell with anyone? But there’s one thing that separates the Scripting Guys from their fellow prophets and soothsayers: as near as we can tell, Nostradamus never wrote a script that imports an Excel spreadsheet into an Access database. Score one for the Scripting Guys:

Const acImport = 0
Const acSpreadsheetTypeExcel9 = 8

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

objAccess.DoCmd.TransferSpreadsheet acImport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True

Guess what: we knew you were going to take one look at this and think “Wow; that’s easy.” And you’re right: it is. As you can see, we start out by defining a constant name acImport and setting the value to 0; this tells the script that we want to import data (as opposed to exporting it). We then define a second constant, this one named acSpreadsheetTypeExcel9, and set the value to 8; this tells the script that we’re planning to import an Excel 2003 worksheet. Are there other spreadsheet formats that we can import into Access? You bet there are:

Constant | Value
acSpreadsheetTypeExcel12 | 9
acSpreadsheetTypeExcel12Xml | 10
acSpreadsheetTypeExcel3 | 0
acSpreadsheetTypeExcel4 | 6
acSpreadsheetTypeExcel5 | 5
acSpreadsheetTypeExcel7 | 5
acSpreadsheetTypeExcel8 | 8
acSpreadsheetTypeExcel9 | 8
acSpreadsheetTypeLotusWJ2 | 4
acSpreadsheetTypeLotusWK1 | 2
acSpreadsheetTypeLotusWK3 | 3
acSpreadsheetTypeLotusWK4 | 7

After we define our constants we then use these two lines of code to create an instance of the Access.Application object and open the database C:\Scripts\Test.mdb (something we do by calling the OpenCurrentDatabase method):

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

At that point all we have to do is invoke the TransferSpreadsheet method and import the spreadsheet data into the database:

objAccess.DoCmd.TransferSpreadsheet acImport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True

Ah, yes, we had a feeling you were going to ask that; here’s what the parameters passed to the TransferSpreadsheet method represent:

· acImport, the constant that tells the script that we want to import data rather than export it.

· acSpreadsheetTypeExcel9, the constant that specifies the file format for the imported data.

· “Employees”, the name of the table in Test.mdb we want to add the spreadsheet data to. Suppose the database doesn’t contain a table named Employees; what then? Well, in that case the script will automatically create a new table (named Employees, of course) for you.

· “C:\Scripts\Employees.xls”, the full path to the Excel spreadsheet we want to import.

· True, the HasFieldNames parameter, which tells the script that the first row in the spreadsheet file consists of field names. As you might expect, the import is more likely to work if the field names in your database match the field names in your spreadsheet. But what if they don’t match? Well, in that case you might simply import the data to a new table. Alternatively, you can set the HasFieldNames parameter to False; when you do that the script will simply match the first column of data in the spreadsheet with the first field in the database table, then match the second column with the second field, etc. That works just fine, but you do need to make sure that the field order in the database is the same as the column order in the spreadsheet. If your database table has fields named FirstName, LastName, and Address, in that order, then your spreadsheet needs to have columns listing first names, last names, and addresses, in that same order.

We should also add that it’s important that the data types match as well. If you have a database field named EmployeeID, a field that only accepts numeric data, don’t try importing spreadsheet data that includes non-numeric data (e.g., an employee ID of 1234ABC). That’s not going to work.

Oh, and here’s another thing we should add: there’s actually an optional sixth parameter (following HasFieldNames) that enables you to limit your import to a specified range of data. For example, suppose your spreadsheet has 6 columns of data (columns A through F) and you only want to import the first 25 rows in that spreadsheet. Okey-doke. That means that the range of data to be imported runs from cells A1 through cell F25 (A1:F25 in Excel-speak). In turn, that means you can import just those 25 records by using this command:

objAccess.DoCmd.TransferSpreadsheet acImport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True, "A1:F25"

We’ve already thought of that: suppose you would like to import the spreadsheet into a brand-new database (as opposed to importing the data into an existing database). Is that possible? Not if you’re Nostradamus. But if you’re the Scripting Guys, well, then it’s a different story:

Const acImport = 0
Const acSpreadsheetTypeExcel9 = 8

Set objAccess = CreateObject("Access.Application")
objAccess.NewCurrentDatabase "C:\Scripts\New.mdb"

objAccess.DoCmd.TransferSpreadsheet acImport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True

You’re right: this does look a lot like the first script we showed you, doesn’t it? In fact, there’s only one difference: this time we don’t use the OpenCurrentDatabase method in order to open an existing database file. Instead, we use the NewCurrentDatabase method to create a brand-new database (C:\Scripts\New.mdb):

objAccess.NewCurrentDatabase "C:\Scripts\New.mdb"

At this point in time it’s hard to believe that anyone could possibly doubt the Scripting Guys and their power of clairvoyance. Just in case, however, we might note that the Scripting Guy who writes this column won the annual NCAA college basketball challenge by, among other things, correctly foreseeing a Florida victory over Ohio State. (Sorry; over the Ohio State University. Sheesh.) Furthermore, he already has a prediction for next year’s championship game: the University of Washington 78, North Carolina 65. Remember, you read it here first.

Unless it turns out that we’re wrong. In that case, you didn’t read it here at all, and we have no idea what you’re talking about.


How Can I List All the Access Database Files on a Computer?

Hey, Scripting Guy! How can I list all the Access database files (.mdb and .ldb files) on a computer?

-- MG

Hey, MG. You know, in this day and age most people don’t believe in Santa Claus. Well, we happen to know that these people are wrong: Santa Claus really is real. How do we know that? Because he’s spent the past couple days spamming the Scripting Guy who writes this column.

It’s true: over the past 24 hours or so the Scripting Guy who writes this column has gotten at least 50 emails from Santa. (Or possibly from his elves; the mails all seem to have come from different email addresses.) Each of these emails points him towards a Web site where he can register his child’s name; that way the little tyke (who, by the way, is 6’-1” and 180 pounds) can get a letter from Santa. Which, to be honest, seems a bit odd to us. After all, traditionally children have written letters to Santa; no kid that we know expects to get a letter from Santa.

Of course, the logical assumption is that Santa simply needs this information for other purposes. But that doesn’t make any sense, either. After all, he knows when you’ve been sleeping, he knows when you’re awake; he even knows if you’ve been bad or good. (So be good, for goodness sake!) Why would Santa need to know our email address and home phone number? Why would he need a credit card number? And surely Santa has no use for our Social Security Number. Does he?

If we didn’t know any better we’d think someone was using the Internet to try and scam innocent people. But that’s a bit far-fetched; surely no one would even think of using the Internet for evil rather than good.

Oops; there’s another one. Sorry, Santa, but that’s one email too many; no milk and cookies for you this year. However, we’ll still give you (and everyone else) a script that can locate all the Microsoft Access database files on a computer:

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Extension = 'mdb' OR Extension = 'ldb'")

For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

In the spirit of the holidays, this is a “traditional” script for locating Access database files; we’ll show you an alternate approach in a minute. For now, you can see that this is a pretty simple approach. We start out by connecting to the WMI service on the local computer. However, we could just as easily run the script against a remote machine: all we have to do is assign the name of that remote machine to the variable strComputer. We then use the following query to return a collection of all the files (that is, all instances of the CIM_DataFile class) that have an Extension equal to mdb or ldb:

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Extension = 'mdb' OR Extension = 'ldb'")

And before you ask, no, we didn’t make a mistake here: in WMI the period is not considered part of the file extension. That’s why we use the value mdb rather than .mdb.

That query returns a collection of all the .MDB and .LDB files on the computer. All we have left to do now is set up a For Each loop to loop through the collection, echoing back the Name of each file:

c:\scripts\restored_files.mdb\test.mdb
c:\scripts\test.mdb
c:\windows\shellnew\access9.mdb
c:\windows\system32\ias\dnary.mdb
c:\windows\system32\ias\ias.mdb

Ho, ho, ho, eh?

Now, this script works just fine; if there’s a drawback to it, it’s the fact that, depending on the size of your hard disks, the script could take a minute or two (or more) to complete. With that in mind here’s an alternative approach, one that takes only a second or two to complete. Is there a catch involved here? Of course there is; in fact, there are several catches involved. But let’s look at the code first:

On Error Resume Next

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';"

objRecordSet.Open "SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE System.FileExtension = '.mdb'" & _
    " OR System.FileExtension = '.ldb'", objConnection

objRecordSet.MoveFirst

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("System.ItemPathDisplay")
    objRecordset.MoveNext
Loop

Why is this script so much faster than the first script we showed you? That’s an easy one: this script takes advantage of Windows Desktop Search 3.0. So then why didn’t we just show you this script in the first place? That’s also an easy one. As it turns out, unless you’re running Windows Vista you don’t have Desktop Search 3.0; instead, you need to download and install it. (And unless you’re running Windows XP Service Pack 2 or Windows Server 2003 you can’t even do that; Desktop Search isn’t available for any other flavor of Windows, at least not at the moment.) On top of that the preceding script works only on the local machine; if you need to search for Access files on a remote computer you’ll need to use WMI.

That said, Desktop Search 3.0 is very fast and very cool: imagine being able to search your entire computer for emails or instant messages as well as for files. And imagine being able to search for files not just by file extension but by Title, by Keyword, by Song Artist, by Horizontal or Vertical Resolution, etc. etc. Trust us, it doesn’t just sound useful, it is useful. And, as a special holiday treat, the Scripting Guys have published an article that explains how you can start using Desktop Search 3.0 in your system administration scripts.

It was the least we could do.

At any rate, this is the last Hey, Scripting Guy! for 2006; we’ll be back with a brand-new column on January 2nd. Have a happy holiday season, and we’ll see everyone in a couple weeks.

Oh, and if you see Santa, could you tell him that our Social Security Number is 000-11-2222? We’re still not sure why he needs it, but seeing as how he is Santa, well, we can’t afford to get him upset with us.


How Can I List All the Fields and Data Types in an Access Database?

Hey, Scripting Guy! In a previous column you showed us how to list all the tables in an Access database. How can I list all the fields in those tables, along with the data type of each field?

-- MF

Hey, MF. As you may or may not know, there’s a Microsoft researcher named Gordon Bell who has devoted his professional efforts to “lifelogging,” an activity in which he uses cameras, scanners, tape recorders, and other gadgets to keep a digital record of everything he ever does. According to a recent article in The New Yorker:

“Bell’s archive now also contains a hundred and twenty-two thousand e-mails; fifty-eight thousand photographs; thousands of recordings of phone calls he has made; every Web page he has visited and instant-messaging exchange he has conducted since 2003; all the activity of his desktop (which windows, for example, he has opened); eight hundred pages of health records, including information on the life of the battery in his pacemaker; and a sprawling category he describes as “ephemera,” which contains such things as books he has written and books from his library; the labels of bottles of wine he has enjoyed; and the record of a bicycle trip through Burgundy, where he tried to eat in as many starred restaurants as he could (he averaged 2.2 stars per meal—“I do a lot of measuring,” he says).”

Before you ask, the Scripting Guys do not engage in lifelogging, mainly because none of them have lives worth logging. For example, the lifelog of the Scripting Guy who writes this column would consist of little more than an endless pile of receipts from the local doughnut shop. You can only log “two maple bars and a pair of cinnamon twists” so many times before it starts to seem a little pointless.

However, it’s also true that the Scripting Guys don’t need a lifelog, at least not as long as they have the Hey, Scripting Guy! column. For example, when the Scripting Guy who writes this column first read this question he thought, “List all the tables in an Access database, huh? That would be a useful script, wouldn’t it? But who knows how to write a script like that?” And then, as he read a little further, he realized that not only did he know how to write a script that lists all the tables in an Access database, but that he’d already done so. “Well, what do you know,” he thought to himself. “Maybe I should read this column more often.”

At any rate, thanks to the Hey, Scripting Guy! lifelog the Scripting Guy who writes this column was able to recreate that magic moment when he wrote a script that retrieved a list of all the tables in an Access database. From there it was relatively painless to take the next step and list the fields and data types for each of those tables:

Const adSchemaTables = 20
Const adSchemaColumns = 4

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = 'C:\Scripts\Test.mdb'"

Set objRecordSet = objConnection.OpenSchema(adSchemaTables)

Do Until objRecordset.EOF
    strTableName = objRecordset("Table_Name")
    Set objFieldSchema = objConnection.OpenSchema(adSchemaColumns, _
        Array(Null, Null, strTableName))
    Wscript.Echo UCase(objRecordset("Table_Name"))

    Do While Not objFieldSchema.EOF
        Wscript.Echo objFieldSchema("Column_Name") & ", " & objFieldSchema("Data_Type")
        objFieldSchema.MoveNext
    Loop

    Wscript.Echo
    objRecordset.MoveNext
Loop

Note. Feel free to add this script to your own lifelog: the Scripting Guys don’t have copyrights on their lives; we’ve never been all that concerned that people would try to copy our lives and claim them for their own. We also have plenty of doughnut shop receipts, you’re more than welcome to add those to your lifelog as well. Just let us know how many you need.

Let’s take a few moments out from our busy lives and see if we can figure out how this script works. We start out by defining a pair of constants, adSchemaTables and adSchemaColumns. Eventually we’ll use one of these constants to tell the script to retrieve table information from the database, then use the other to retrieve column (field) information from the database. See if you can guess which constant will be used for which task.

OK, apparently that wasn’t as hard as we thought it would be. Good thing we didn’t make that the basis of our recent TechEd Challenge.

After defining the constants we create instances of the ADODB.Connection and ADODB.Recordset objects, then use the following line of code to open the database C:\Scripts\Test.mdb:

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = 'C:\Scripts\Test.mdb'"

Once we’ve made the connection to the database we can retrieve a list of tables by using a single line of code and the OpenSchema method, passing the constant adSchemaTables as the only parameter for that method:

Set objRecordSet = objConnection.OpenSchema(adSchemaTables)

As soon as we’ve done that we’re ready to really spice up the old lifelog: we’re now ready to loop through the collection of tables and, for each one, retrieve not only a list of all the fields in the table, but also the data type of each field.

To that end we start out by setting up a Do Until loop that runs until we’ve finished iterating through all the records in the recordset (that is, until the recordset’s EOF – end-of-file – property is True). The first thing we do inside the loop is take a look at record 1, grab the value of the Table_Name property, then store that value in a variable named strTableName:

strTableName = objRecordset("Table_Name")

Once we know the name of the first table we can then use this line of code to retrieve a second recordset, this one consisting of all the fields (columns) in the table:

Set objFieldSchema = objConnection.OpenSchema(adSchemaColumns, _
    Array(Null, Null, strTableName))

As you can see, we’re once again calling the OpenSchema method in order to access the database schema. This time, however, we need to pass a few additional parameters to OpenSchema. For starters, we pass the constant adSchemaColumns, which simply tells the script that we want to return column information. We then pass an array of “constraint columns.” This array consists of the following values:

· A Null value representing the table catalog.

· A Null value representing the table schema.

· The variable strTableName, representing the name of the table we want to bind to.

For the time being, don’t worry too much about the table catalog and table schema. Just make sure your table name appears as the third item in the array and everything should work just fine.

After retrieving the second recordset (objFieldSchema) we can start displaying the data for the first table in the database. To kick off that process, we first echo back the name of the table, using the UCase function to make the table name appear in all uppercase characters:

Wscript.Echo UCase(objRecordset("Table_Name"))

We then set up a second Do Until loop, this one designed to loop through the collection of fields in table 1:

Do While Not objFieldSchema.EOF

And what are we going to do inside this loop? Two things. First, we’re going to echo back the values of the Column_Name and Data_Type properties:

Wscript.Echo objFieldSchema("Column_Name") & ", " & objFieldSchema("Data_Type")

Second, we’re going to call the MoveNext method to move us from the first record in the recordset to the next record. After we’ve echoed back information about each item in the recordset we echo a blank line to the screen and then call MoveNext a second time, this time to move to the next record (table) in our original recordset. When we’re finished we’ll have a report that looks something like this (depending on the tables and fields you have in your database):

COMPUTERS
ComputerName, 130
SerialNumber, 130

DISKDRIVES
Drive, 130
DriveSize, 2
SerialNumber, 130
Type, 130

EMPLOYEES
Bonus, 6
Department, 130
Division, 130
EmployeeID, 5
EmployeeName, 130
Salary, 6

Ah, good question: what are those numbers following the field names? Well, as it turns out, data types are stored as numeric values; for example, the field Salary has a data type of 6. If you look up the values list for the DatatypeEnum you’ll see that a 6 indicates a currency data type. And yes, we probably could have – and maybe should have – included all this information in our sample script. We didn’t do that simply because we were afraid the script would become too long and look far more complicated than it really is.

Besides, our lifelog was already beginning to fill up.

Incidentally, you don’t need to look at all the tables in the database. For example, suppose you were only interested in getting field information for the Employees table:

Const adSchemaColumns = 4

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
"Provider = Microsoft.Jet.OLEDB.4.0; " & _
"Data Source = 'C:\Scripts\Test.mdb'"

Set objRecordSet = objConnection.OpenSchema(adSchemaColumns, _
Array(Null, Null, "Employees"))

Do Until objRecordset.EOF
Wscript.Echo objRecordset("Column_Name") &
", " & objRecordset("Data_Type")
objRecordset.MoveNext
Loop

That should do it, MF. At least we hope it does, because we have to leave: it’s time for our mid-morning doughnut break.

Well, OK: time for our second mid-morning doughnut break. Just two more after that and it’ll be lunch time.


How Can I List All the Tables in an Access Database?

Hey, Scripting Guy! How can I list all the tables in an Access database?

-- KW

Hey, KW. You know, the best part about writing a daily column on system administration scripting is that people only see the finished result; they never see all the detours, dead-ends, and other frustrations we go through in order to produce that column. For example, people who read this column will think, “Man, those Scripting Guys are so smart: they even know how to list all the tables in an Access database!” Those people will never know that we had absolutely no idea how to list all the tables in an Access database, and that we stumbled upon the answer only after a long and fruitless search of the Access object model. But as far as the world knows, we Scripting Guys really are smart!

Well, unless we admitted how hard it was for us to answer this question. But even the Scripting Guys aren’t dumb enough to own up to something like that.

Now, where we were? Oh, right: how can you list all the tables in an Access database? That’s an easy one, KW; here’s how:

Const adSchemaTables = 20

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = 'C:\Scripts\Test.mdb'"

Set objRecordSet = objConnection.OpenSchema(adSchemaTables)

Do Until objRecordset.EOF
    Wscript.Echo "Table name: " & objRecordset.Fields.Item("TABLE_NAME")
    Wscript.Echo "Table type: " & objRecordset.Fields.Item("TABLE_TYPE")
    Wscript.Echo
    objRecordset.MoveNext
Loop

Admittedly, people without the Scripting Guys’ experience and expertise in the world of system administration scripting might think that the way to attack this problem would be to use the Microsoft Access object model. We Scripting Guys know better, however: instead, you get at this information using ADO (ActiveX Data Objects). And that’s actually a good thing: that means you can use this same basic approach to list the tables found in other kinds of databases. (Right: as if there even are databases other than Microsoft Access!)

Let’s take a closer look at the script and how it works. To begin with, we define a constant named adSchemaTables and set the value to 20; we’ll use this constant to tell the script what kind of database schema objects we’re interested in (needless to say, in this case, all we care about are tables). Are there other kinds of schema objects we can retrieve using a script? You bet: check out the Schema Enum documentation on MSDN for more information.

After defining the constant we then create a pair of objects: the ADODB.Connection object and the ADODB.Recordset object. If you’ve had some experience working with ADO then you should be very familiar with the Connection and Recordset objects. But what if you’re new to database scripting and have no idea what we’re talking about here? That’s OK: the Scripting Guys webcast Database Scripting for System Administrators will give you all the background information you need to understand this script and how it works.

As soon as we have our two objects in-hand we can then use the Open method to open the file C:\Scripts\Test.mdb:

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = 'C:\Scripts\Test.mdb'"

And once we’ve done that we can then call the OpenSchema method and retrieve information about the database itself. What kind of information are we retrieving? Well, in this case we’re passing OpenSchema the constant adSchemaTables; that means we’re going to get back a recordset consisting of information about all the tables found in the database.

Because we’re getting back a recordset we need to next set up a Do Until loop to walk through and echo back information about all the records (that is, all the tables) in the recordset. That’s what we do here:

Do Until objRecordset.EOF
    Wscript.Echo "Table name: " & objRecordset.Fields.Item("TABLE_NAME")
    Wscript.Echo "Table type: " & objRecordset.Fields.Item("TABLE_TYPE")
    Wscript.Echo
    objRecordset.MoveNext
Loop

As you can see, our Do Until loop is designed to run until the EOF (end of file) property is true; in other words, we want to keep looping around until we run out of records. To that end, take careful note of the last line of code within the Do Until loop: objRecordset.MoveNext. Is that important? You bet it is: that’s the line of code that tells the script, hey, after you finish with one record move on to the next. If you don’t include the MoveNext method the script will remain stuck on the first record. In turn, that means you’ll continue to echo back information about record 1 and will never, ever reach the end of the recordset. (For a more detailed explanation of this take a look at the answer to the scripting puzzle Yes, Ken, We See You Already!)

So do we do anything inside the loop besides call the MoveNext method? Of course we do; we also echo back the values of the TABLE_NAME and TABLE_TYPE properties:

Wscript.Echo "Table name: " & objRecordset.Fields.Item("TABLE_NAME")
Wscript.Echo "Table type: " & objRecordset.Fields.Item("TABLE_TYPE")

Is that important? You might say that; after all, that happens to be the list of tables (and their associated table types) found in the database C:\Scripts\Test.mdb:

Table name: UserInformation
Table type: TABLE

Table name: ComputerInformation
Table type: TABLE

Table name: MSysAccessObjects
Table type: ACCESS TABLE

Table name: MSysACEs
Table type: SYSTEM TABLE

Table name: MSysObjects
Table type: SYSTEM TABLE

Table name: MSysQueries
Table type: SYSTEM TABLE

Table name: MSysRelationships
Table type: SYSTEM TABLE

As you probably noticed, this script brings back information about all the tables in the database, including system tables you typically don’t interact with (or care about). Can you limit the returned data just to those tables (like UserInformation and ComputerInformation) that you typically do interact with? Sure; all you have to do is modify the script like so:

arrCriteria = Array(Empty, Empty, Empty, "Table")

Set objRecordSet = objConnection.OpenSchema(adSchemaTables, arrCriteria)

What we’ve done here is created an array named arrCriteria and told it to look just for tables where the TABLE_TYPE is equal to Table. (The three Empty parameters represent other criteria we could use in our filter; for more information, see the Schema Enum documentation.) We then add the variable arrCriteria as an optional second parameter to the OpenSchema method.

What will we get back when we run this modified script? We’ll get back this:

Table name: UserInformation
Table type: TABLE

Table name: ComputerInformation
Table type: TABLE

Here’s what the entire script looks like:

Const adSchemaTables = 20

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = 'C:\Scripts\Test.mdb'"

arrCriteria = Array(Empty, Empty, Empty, "Table")

Set objRecordSet = objConnection.OpenSchema(adSchemaTables, arrCriteria)

Do Until objRecordset.EOF
    Wscript.Echo "Table name: " & objRecordset.Fields.Item("TABLE_NAME")
    Wscript.Echo "Table type: " & objRecordset.Fields.Item("TABLE_TYPE")
    Wscript.Echo
    objRecordset.MoveNext
Loop

And, yes, all that was pretty easy, wasn’t it? Well, at least for the Scripting Guys ….


How Can I Print a Microsoft Access Report?

Hey, Scripting Guy! How can I print a Microsoft Access report?

-- RW

Hey, RW. You know the cool thing about being a Scripting Guy – aside from the opportunity to pal around with Paris and Nicole – is the fact that people think we know what we’re doing. Someone asks a question like, “How can I print a Microsoft Access report?” and the Scripting Guys respond with a script that does just that. Wow, you find yourself thinking. Is there anything those guys don’t know?

Just between us, RW, there are tons of things the Scripting Guys don’t know, things like, well, how you can print a Microsoft Access report. To be honest, we have no idea how to print a report from Microsoft Access; that means a question like this one results in a flurry of activity, with a gaggle of Scripting Guys checking SDKs, writing sample code, and running and debugging scripts. But no one ever sees that part of the process; instead, everyone sees the finished product and assumes that we knew the answer right off the top of our heads. That’s rarely the case, but we’re careful not to let anyone know that: after all, we’ve acquired a reputation as guys who know what they’re doing, and you’d have to be a really dumb Scripting Guy to spill the beans and let people know the sordid truth.

Editor’s Note: It should be pretty obvious at this point that at least one of three things is occurring here:

- The Scripting Guy who writes this column is the dumb Scripting Guy
- The Scripting Guy who writes this column is trying (most likely unsuccessfully) to convince his manager that he really does work
- The Scripting Guy who writes this column has nothing interesting to talk about today

We’ll let you decide.

So how do you print a Microsoft Access report? That’s an easy one, RW, at least for highly-competent, world-class experts like the Scripting Guys. Here’s how:

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

Set objCommand = objAccess.DoCmd

objCommand.OpenReport "EmployeeList"

objAccess.CloseCurrentDatabase

As you can see, we didn’t exactly have to knock ourselves out in order to print a report from Microsoft Access. We start out by creating an instance of the Access.Application object, then use the OpenCurrentDatabase method to open the database C:\Scripts\Test.mdb.

Note. You might have noticed that, with this script, we didn’t bother setting the Visible property to True and making Access visible onscreen. Why not? That’s simple: the report prints out so quickly there didn’t seem to be any reason to have Access appear onscreen and then, just as quickly, disappear. However, if you’d prefer to see Access onscreen while the report prints all you have to do is add this line of code immediately after the line that opens the database:

objAccess.Visible = True

That brings us to this portion of the script:

Set objCommand = objAccess.DoCmd

The DoCmd object is an unusual – albeit useful – little object. More often than not scripts that interact with Microsoft Access don’t use any Access-specific commands; instead, the scripts simply use Access as a data source and rely on generic database commands (like those found in ActiveX Data Objects) to perform their chores. On occasion, however (and this is one of those occasions), your script will need to do something Access-specific, like print a report or open a form. At times like that you need to use the DoCmd object to invoke those Access-only commands.

Which, needless to say, explains why we created that instance of the DoCmd object.

Once we have our very own DoCmd object we can then print the report (to the default printer) by calling the OpenReport method and passing the name of the report we want to print (in this case, that’s a report named EmployeeList):

objCommand.OpenReport "EmployeeList"

And from there all we have to do is close the database and we’re done:

objAccess.CloseCurrentDatabase

And yes, with this script you need to explicitly close the database. If you don’t, your invisible instance of Access will continue to run in the background forever and ever; we’re guessing you don’t really need to have invisible copies of Access running in the background using up system resources. But that’s up to you.

Incidentally, you can use a related script to print the data in an Access table; that provides a way to print information without having to create an Access report. For example, this script prints all the data in the Employees table:

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

Set objCommand = objAccess.DoCmd

objCommand.OpenTable "Employees"
objCommand.PrintOut

objAccess.CloseCurrentDatabase

You can see the similarities between this script and our report-printing script. In both scripts we start out by creating an instance of Access, opening the database C:\Scripts\Test.mdb, then creating an instance of the DoCmd object. In the table-printing script we then use these three lines of code to open the table, print the data (using the Printout method), and then close the database:

objCommand.OpenTable "Employees"
objCommand.PrintOut

objAccess.CloseCurrentDatabase

All in all, pretty darn easy.

We should also note that there are additional parameters you can employ when calling the PrintOut method; for example, you can specify a range of pages to print, you can specify a print quality (draft, low, medium, high), and even indicate the number of copies to print and whether or not those copies should be collated. For more information on these additional parameters take a look at the Microsoft Access VBA Language Reference.

Not that the Scripting Guys ever have to refer to outside resources like the VBA Language Reference; after all, we wouldn’t be Scripting Guys if we didn’t already know all that stuff. We just thought that other people might find it helpful.


How Can I Save a Table in an Access Database as a Spreadsheet?

Hey, Scripting Guy! How can I save a table in an Access database as an Excel spreadsheet?

-- OT

Hey, OT. You know, we appreciate the question, but where were you last week, when we really needed you? The truth is, last week was excruciatingly dull, at least for two of the Scripting Guys. While Scripting Guy Jean Ross was busy battling ninja warriors in San Diego and Scripting Guy Peter Costantini amused himself by climbing volcanoes in Nicaragua, the remaining Scripting Guys sat around the office all week having conversations like this:

“Sure is boring around here, isn’t it?”

“Yep.”

“I mean, really boring.”

“Yep.”

“Would you agree that it’s pretty boring this week?”

“Yep.”

In fact, the highlight of the entire week – at least for Scripting Guy Dean Tsaltas – occurred when the Microsoft cafeteria served broccoli with cheese sauce two days in a row!

We told you that it was a long week.

That’s why we could have used an interesting question like this one last week; that would have added a little excitement and pizzazz to our long and lonely days. Now that Jean is back we have all the excitement and pizzazz we could ever ask for; we don’t really need your question anymore. Sorry.

On the other hand, we can’t really say that this week is turning out to be any more exciting than last week did; in addition, Jean doesn’t seem to be anywhere near as interesting as we thought she was. So what the heck, OT: let’s see if we can figure out how to save an Access table as an Excel spreadsheet. Just be patient; this could take awhile.

Never mind; turns out that it won’t take very long after all:

Const acExport = 1
Const acSpreadsheetTypeExcel9 = 8

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

objAccess.DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True

Yes, we know: it’s great that this task can be accomplished by using just a few lines of code. However, we were hoping to kill a bunch of time by working on this one script; now what are we supposed to do?

Explain how the script works? Hmmm, interesting idea; maybe we’ll give that a try.

As you can see, we start out by defining a constant named acExport and setting the value to 1; that tells the script that we want to export data. Does that mean that we could use a similar script to import data? As a matter of fact it does, and in tomorrow’s column we’ll show you how to do that very thing. (Now we’re trying to give you something to look forward to!) After that we define a second constant – acSpreadsheetTypeExcel9 – and set the value to 8; this tells the script what kind of spreadsheet we want to create. The constant acSpreadsheetTypeExcel9 creates an Excel 2003 spreadsheet; alternatively, you might want to save your data in a different format. If so then use one of the following constants and their values:

Constant | Value
acSpreadsheetTypeExcel12 | 9
acSpreadsheetTypeExcel12Xml | 10
acSpreadsheetTypeExcel3 | 0
acSpreadsheetTypeExcel4 | 6
acSpreadsheetTypeExcel5 | 5
acSpreadsheetTypeExcel7 | 5
acSpreadsheetTypeExcel8 | 8
acSpreadsheetTypeExcel9 | 8
acSpreadsheetTypeLotusWJ2 | 4
acSpreadsheetTypeLotusWK1 | 2
acSpreadsheetTypeLotusWK3 | 3
acSpreadsheetTypeLotusWK4 | 7

That was all pretty easy, wasn’t it? And, as you’re about to see, it doesn’t get much harder, either. After defining our constants we next use these two lines of code to create an instance of the Access.Application object and then open the file C:\Scripts\Test.mdb (a feat we accomplish by calling the OpenCurrentDatabase method):

Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase "C:\Scripts\Test.mdb"

At this point we’re just one command away from exporting our data as an Excel spreadsheet. In fact, now that we mention it, we’re just this command away from exporting our data as an Excel spreadsheet:

objAccess.DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel9, _
    "Employees", "C:\Scripts\Employees.xls", True

What we’re doing here is using the DoCmd object (a child object of Access.Application) to call the TransferSpreadsheet method; this method enables you to export data to or import data from a spreadsheet. As you can see, we simply call the method, passing five parameters:

· acExport, the constant that tells the script that we want to export data rather than import it.

· acSpreadsheetTypeExcel9, the constant that specifies the file format for the exported data.

· “Employees”, the name of the table in Test.mdb to be exported.

· “C:\Scripts\Employees.xls”, the full path to the new Excel file being created.

· True, the HasFieldNames parameter, which tells the script to make the field names in the table the first row in the spreadsheet file.

Believe it or not, that’s all we have to do; if you run this script and then take a look at the folder C:\Scripts you should see a new spreadsheet file named Employees.xls.

OK, true; it’s not broccoli with cheese sauce. But it’s close.

We hope that helps, OT. And while we appreciate the offer, there’s no need to provide us with another exciting question for tomorrow. After all, tomorrow is Taco Salad Wednesday in the cafeteria, and that alone is more than enough excitement for the Scripting Guys.

Saving an Access Table as a Spreadsheet Using Windows PowerShell

Just for the heck of it, we’ve added a new feature to Hey, Scripting Guy!: whenever possible, we’ll provide a Windows PowerShell solution as well as our typical VBScript solution. In general we won’t be able to provide much explanation for the PowerShell version, but at least there will be some sample code for you to look at.

You know, sample code like this:

$acExport = 1
$acSpreadsheetTypeExcel9 = 8

$a = New-Object -Comobject Access.Application
$a.OpenCurrentDatabase("C:\Scripts\Test.mdb")

$a.DoCmd.TransferSpreadsheet($acExport, $acSpreadsheetTypeExcel9, "Employees",
`
"C:\Scripts\Employees.xls", $True)

$a.Quit()


How Can I Use Windows PowerShell to Pull Records From a Microsoft Access Database?

Hey, Scripting Guy! How can I use Windows PowerShell to pull records from a Microsoft Access database?

-- SB

Hey, SB. You know, you’re lucky to get this question answered; after all, this looks as though it’s going to be the very last Hey, Scripting Guy! column ever.

To be honest, that came as a bit of a surprise to us, too: we hadn’t planned on retiring this early. (Although our manager does suggest that to each of the Scripting Guys on a daily basis.) But that was before we received an email this morning informing us that we had won a prize of € 1 million (one million euros) in the Spanish Lottery Winners International E-mail Program. And as much as we like working here, well, it just doesn’t seem right for a millionaire to be getting up at 6:00 every morning in order to be to work by 7:00, does it?

Note. You think it’s lucky that we won the contest, even without entering it? That’s nothing. After all, this is the fourth or fifth time this year that the Scripting Guy who writes this column has won the Spanish lottery (or, to quote from the email, the “Spainsh” lottery). Amazingly enough, he’s managed to do this even though the lottery is only held once every three years. You want to talk lucky? That’s lucky!

To tell you the truth, we weren’t even going to write today’s column; it’s hard to focus on scripting when you now have to worry about where to buy your caviar and how many forks you need for a formal dinner. On the other hand, the email did note that there has been a “mix up of some numbers and names”, which means it will take a little bit of time to get everything sorted out. In turn, that means we have a little time to kill. With that in mind, we might as well tell you how to use Windows PowerShell to pull records from a Microsoft Access database. One last Hey, Scripting Guy! column, for old time’s sake.

Let’s take a look at the script, then see if we can figure out how it works:

$adOpenStatic = 3
$adLockOptimistic = 3

$objConnection = New-Object -comobject ADODB.Connection
$objRecordset = New-Object -comobject ADODB.Recordset

$objConnection.Open("Provider = Microsoft.Jet.OLEDB.4.0; Data Source = c:\scripts\sample.mdb")
$objRecordset.Open("Select * from TotalSales", $objConnection,$adOpenStatic,$adLockOptimistic)

$objRecordset.MoveFirst()

do {$objRecordset.Fields.Item("EmployeeName").Value; $objRecordset.MoveNext()} until
    ($objRecordset.EOF -eq $True)

$objRecordset.Close()
$objConnection.Close()

As you can see, we start out simple enough, assigning the value 3 to two different variables: $adOpenStatic and $adLockOptimistic. (If you’re new to Windows PowerShell and are wondering about those variable names there’s reason all our variable names start out with a $: that’s how variable names have to start.) We aren’t going to discuss the nuts-and-bolts of doing ADODB (ActiveX Database Objects) scripting today; for more information, see this classic Scripting Guys webcast. Instead, we’ll just say that we’ll use these two variables when we finally go out and retrieve data from the database.

After assigning values to the two variables we then use the New-Object Cmdlet to create a pair of object references, one for the ADODB.Connection object, the other for the ADODB.Recordset object:

$objConnection = New-Object -comobject ADODB.Connection
$objRecordset = New-Object -comobject ADODB.Recordset

As you can see, New-Object serves the same purpose as VBScript’s CreateObject method. In fact, this is the VBScript equivalent for creating a Connection object:

Set objConnection = CreateObject("ADODB.Connection")

A different syntax, obviously, but some definite similarities as well.

Once we have our two objects in hand we can connect to the database (C:\Scripts\Sample.mdb) and then query a table in that database. In our sample script, we’re selecting all the records from a table named TotalSales, something we do with this line of code:

$objRecordset.Open("Select * from TotalSales", $objConnection,$adOpenStatic,$adLockOptimistic)

And you’re right: this is very much like opening a recordset using VBScript. About the only real difference (aside from the variable names) is the fact that, in VBScript, we don’t put parentheses around the parameters passed to the Open method:

objRecordSet.Open "Select * From TotalSales", _
    objConnection, adOpenStatic, adLockOptimistic

But that’s in VBScript. In Windows PowerShell you must always use parentheses when calling a method, even if a method doesn’t require a parameter. For example, suppose we could just call the Open method without including query information. In that case our code would look like this, with the method followed by an empty set of parentheses:

$objRecordset.Open()

With the recordset open we call the MoveFirst method (what did we tell you: note the empty parentheses) to position the cursor at the beginning of the recordset. That brings us to this block of code:

do {$objRecordset.Fields.Item("EmployeeName").Value; $objRecordset.MoveNext()} until
    ($objRecordset.EOF -eq $True)

What we’re doing here is creating a Do Until loop that loops until we reach the end of our recordset. To explain how this works, note that we first employ the keyword do followed by the actions we want to perform, actions that need to be enclosed in curly braces. We want to do two things within our loop. First, we want to echo back the value of the EmployeeName field; that’s what this snippet of code is for:

$objRecordset.Fields.Item("EmployeeName").Value

And yes, unlike VBScript you must tack a .Value after the field name. Value is the default property when dealing with an item in a recordset; if you don’t explicitly specify a property name VBScript will automatically use the default property. That’s not the case with Windows PowerShell, however. Default property or not, you must still add the .Value.

After displaying the value for EmployeeName for the first record in the recordset, we then want to move on to the next record. Therefore, we call the MoveNext method using this snippet of code:

$objRecordset.MoveNext()

Notice that both our commands – one for echoing back the employee name, the other for moving to the next record – are enclosed within the curly braces. In addition, the two are separated by a semicolon. Needless to say, the semicolon is one way to separate commands in Windows PowerShell.

So much for the “do” portion; what about the “until” part? Well, after typing in the until keyword we use this code (enclosed in parentheses) to instruct the script to continue until the recordset’s EOF (end-of-file) property is true:

$objRecordset.EOF -eq $True

Two things to note here. First, we use the –eq comparison operator rather than the equals sign; that’s another Windows PowerShell convention you’ll need to get used to. Second, note the $ in front of the value True. Is that required, do we need to type Boolean values as $True and $False? You better believe it.

That’s really all we need to do. The script will loop through the recordset, displaying each employee name found in the table Total Sales. When the EOF property is true (that is, when we reach the end of the recordset) we’ll exit the Do loop, then use these two lines of code to close the recordset and the database connection:

$objRecordset.Close()
$objConnection.Close()

At that point, we’re done.

And when we say done, we mean that: after all, this is the last column. Well, unless it turns out that someone was – gasp! – trying to scam the Scripting Guys, and that we didn’t really win the Spanish lottery after all. But what do you suppose the odds are of that? We can’t imagine that anyone would ever use the Internet for anything but the most noble of purposes.


How Can I Add a Background Picture to an Excel Worksheet?

Hey, Scripting Guy! How can I add a background picture to an Excel worksheet?

-- GA

Hey, GA. You know, being the health-conscious type, each morning the Scripting Guy who writes this column eats some non-fat raspberry yogurt … right before he eats his daily doughnut. (But don’t worry: it’s a medically-proven fact that eating yogurt counteracts any potentially harmful or fattening effects of a doughnut.) This morning, as he was taking the lid off the yogurt container, he noticed the words Sell By, followed by this:

06-4902

Now, to be honest, the Scripting Guy who writes this column isn’t exactly sure how to interpret this; however, it sure looks as though the yogurt maker is suggesting that this product must be sold no later June, 4902. If that’s the case, then this Scripting Guy only has 2,895 years in which to finish off all the yogurt in his refrigerator. Needless to say, he’s beginning to regret having bought a 3,000-year supply of nonfat raspberry yogurt. What was he thinking?

Note. According to urban legend, Hostess Twinkies have a shelf life of anywhere from 50 to 100 years; pretty lame compared to nonfat raspberry yogurt, but pretty good for a pastry product. As a matter of fact, there are people who claim that Hostess stopped making Twinkies decades ago, and has simply been selling the old ones ever since. (The story is that Hostess terribly overestimated the number of Twinkies it would sell, and ended up making billions more than it needed.) Food experts say that, in reality, Twinkies have a shelf life of about 25 days, but there are unconfirmed reports of Twinkies surviving reasonably well for more than 30 years.

Which is something none of the Script Guys can claim to have done.

At any rate, we can’t be sure if yogurt really does have a shelf life of 2,895 years; most likely it doesn’t. However, we are sure that today’s Hey, Scripting Guy! column has a shelf life of at least, 2,895 years, especially when you consider that it deals with a subject like adding a background picture to an Excel worksheet:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.SetBackgroundPicture "C:\Scripts\Test.jpg"

Note. We really do guarantee that this column has a shelf life of at least 2,895 years. If, come June, 4902, you are unable to locate this particular column, well, email scripter@microsoft.com and we will refund your money, no questions asked.

We have to admit that we found this to be an interesting little exercise; to be honest, we didn’t even know you could add a background picture to an Excel worksheet. (We knew you could add background pictures to a graph, but didn’t know you could do that to a worksheet.) As you can see, however, not only can you add a background picture to a worksheet, but you can do so with just a few lines of code. And that’s good: after all, we only have 2,895 years to eat our yogurt and to add a background picture to a worksheet. At the typical Scripting Guys’ pace, that’s not all that much time.

As for the script itself, we start out by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Microsoft Excel that we can see onscreen. We then use the following two lines of code to add a new workbook to our instance of Excel and bind to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

From there, setting a background picture is as easy as calling the SetBackgroundPicture method, passing the path to the image file as the sole method parameter:

objWorksheet.SetBackgroundPicture "C:\Scripts\Test.jpg"

Is that really all we need to do? You bet it is:

Remember, this merely sets the picture in question as the background picture for the worksheet. If you just want to insert a picture into a worksheet (without making it the background), then you need to use code similar to this:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Pictures.Insert("C:\Scripts\Test.jpg")

What we’re doing here is referencing the Pictures collection and calling the Insert method; that adds the image file C:\Scripts\Test.jpg to the worksheet. By default, this positions the picture so that the graphic’s upper left-hand corner is in cell A1. Suppose you’d rather have the upper left-hand corner of the picture in cell B10. That’s fine; the following script makes cell B10 the active cell (objExcel.Range("B10").Activate) and then inserts the picture:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objExcel.Range("B10").Activate
objWorksheet.Pictures.Insert("C:\Scripts\Test.jpg")

Office Excel 2007: The scripts in this article will work just fine in Excel 2007. The only difference is that the active cell doesn’t seem to impact the position of the picture. So the preceding script, where we set B10 as the active cell, will simply make B10 the active cell; the position of the picture won’t be any different than it was when the active cell was A1. If you want to specifically set the position of the picture, you can use the AddPicture method of the Shapes object:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Shapes.AddPicture
"C:\Scripts\Sunset.jpg",1,0,0,0,100,200

The first parameter to AddPicture is (obviously) the path to the image. The second and third parameters have to do with how the picture is embedded. The final four parameters – in this case 0,0,100,200 – are the coordinates within the worksheet where the picture will appear and the size of the picture. See the Excel Developer Reference for more details.

That should do it, GA; let us know if you have any questions. Oh, and be sure to tune in again on June 1, 4092; on that day we’ll let everyone know whether or not nonfat raspberry yogurt really does have a shelf life of 2,895 years.

For now, however, we’ve got a doughnut to eat. How long is the shelf life of a doughnut? Let’s put it this way: if the Scripting Guy who writes this column is around, no doughnut is going to last more than a few minutes, max.


How Can I Add a Hyperlink to an Excel Spreadsheet?

Hey, Scripting Guy! How can I add a hyperlink to an Excel spreadsheet?

-- J B-N

Hey, J B-N. To begin with, we’d like to point out that we’re well aware of all the recent media scandals involving such prestigious organizations as The New York Times. As you know, many of those scandals revolve around writers who didn’t actually do the work they claimed they did. We wanted to take a moment to set everyone’s mind at ease: the Scripting Guys would never take shortcuts in an attempt to fool our readers into thinking we did work that we didn’t really do.

OK, OK, we admit it: we really didn’t do any work when it came time to answer this question. Awhile back we answered a similar question about adding a hyperlink to a Microsoft Word document. In order to answer this question, we pretty much took the script we wrote for that column, crossed out Microsoft Word and wrote in Microsoft Excel. We are suitably ashamed and embarrassed and we promise that something like this will never happen again.

Well, not unless someone wants to know how to add a shortcut to a PowerPoint presentation ….

Admittedly, we didn’t exactly knock ourselves out for today’s column; on the bright side, however, everyone gets a useful little script out of the deal. If you want to add a hyperlink to an Excel spreadsheet here’s what you need to do:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1) = "Script Center"

Set objRange = objExcel.Range("A1")
Set objLink = objWorksheet.Hyperlinks.Add _
(objRange, "http://www.microsoft.com/technet/scriptcenter")

As you can see, we begin by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Microsoft Excel that we can see onscreen. Next we use these two lines of code to create a new workbook and a new worksheet:

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

Once we have that worksheet we then use this line of code to add the term “Script Center” to cell A1 (that is, cell row 1, column 1):

objWorksheet.Cells(1, 1) = "Script Center"

As you probably figured out, we’re going to use Script Center as the hyperlink text.

OK, so what about the hyperlink? Well, to begin with, we need to create an instance of the Excel Range object, an instance that will encompass the hyperlink text. Because we want to hyperlink to appear in cell A1 we create a range that consists solely of cell A1:

Set objRange = objExcel.Range("A1")

All that’s left now is to call the Add method and add a new link to the Hyperlinks collection. Here’s how we do that:

Set objLink = objWorksheet.Hyperlinks.Add _
    (objRange, "http://www.microsoft.com/technet/scriptcenter")

Pretty simple, huh? We just call the Add method and pass it two parameters: the object reference for our Range object (objRange) and the URL for the hyperlink (http://www.microsoft.com/technet/scriptcenter). You’ll now be able to click on cell A1 and transport yourself to the Script Center.

Yes, exactly like the transporters on the old Star Trek TV show.

OK, OK, we admit it: it’s nothing at all like the transporters on the old Star Trek TV show. You guys are a tough audience; if this keeps up we might have to start telling the truth once in awhile. With that in mind, the Scripting Guys would like to issue the following corrections: Dean Tsaltas is from Canada, not Pluto. Peter Costantini isn’t 537 years old (not till April anyway) and Jean Ross is not a talking squirrel. Finally, Greg Stemp did not win the 1937 Miss Nebraska beauty pageant; in fact, he did not even place in the top 10.

We hope you’re satisfied now.


How Can I Add a Total Row to an Excel Spreadsheet?

Hey, Scripting Guy! How can I add a bunch of rows to an Excel spreadsheet and then stick a total row at the bottom?

-- MW

Hey, MW. Could we ask you a favor: is there any chance that you could write back later with this question? As you may or may not know, the University of Washington men’s basketball team is hosting a four-team, round-robin tournament this week. Sunday the Scripting Guy who writes this column and his Scripting Son went to the tournament’s opening day. Because they like watching both games, including the game that doesn’t involve the Huskies, they went over early and ate dinner beforehand: deli sandwiches (with tons of meat). Yesterday they went over early and had pulled-pork barbecue sandwiches. Tonight is the third and final day of the tournament, which means the Scripting Guy who writes this column is absolutely swamped trying to figure out what they should have for dinner this time around.

Anyway if you could get back to us, say when the college basketball season is over, that would really help a lot. Thanks, man!

Just a moment, MW ….

It seems that the Scripting Editor – who is becoming more and more of a killjoy by the moment (as if that was even possible) – has informed us that we were kidding when we suggested that we weren’t going to answer any more scripting questions until after the college basketball season was over. To tell you the truth, we didn’t think we were kidding; we thought we were serious. (Hey, the season will be over sometime in April.) On the other hand, maybe the Scripting Editor is right. After all, the law of averages suggests that sooner or later she’s bound to be right about something. Maybe today’s the day.

Well, just between us, we don’t think she is, either. But what the heck; we’ll humor the Scripting Editor and answer the question anyway:

arrValues = Array(1,5,7,9,11,13,15,17)

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.Workbooks.Add

i = 1

For Each intValue in arrValues
    objExcel.Cells(i, 1).Value = intValue
    i = i + 1
Next

objExcel.Cells(i, 1).Formula = "=SUM(A1:A" & i - 1 & ")"

Before we go much further we should point out that we’re working with a very simple, one-column spreadsheet here. That might not be especially realistic, but it should make it easier for you to follow what we’re doing. To make a fancier, multi-column spreadsheet all you need to do is take the basic recipe shown here and extend it a little.

OK. As you can see, we start out simply by assigning a bunch of numbers to an array named arrValues:

arrValues = Array(1,5,7,9,11,13,15,17)

Why do we do this? That’s easy: we need a bunch of values to add to the spreadsheet, and this seemed as good a way as any to do that. Do you need to put all of your values into an array before adding them to your spreadsheet? Probably not. We went this route simply because it helps us keep the code as short and sweet and possible.

After adding values to our array we then use the following block of code to create an instance of the Execl.Application object, make that instance visible, and then give that instance a new workbook:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.Workbooks.Add

With Excel up and running, and visible onscreen, we then set the value of a counter variable named i – a variable we’ll use to keep track of the current row in the spreadsheet – to 1:

i = 1

As soon as that’s done we’re ready to start adding data to our spreadsheet. To do that we simply set up a For Each loop that loops through each value in the array arrValues. Inside that loop we use this line of code to set the value of the cell in column 1 of the current row to the value we grabbed from the array:

objExcel.Cells(i, 1).Value = intValue

If you look closely at the code you can see that we use the counter variable i to represent the row. (Hey, just like we said we were going to do!) The first time through the loop i is equal to 1; as it turns out, the first value we grab from the array is also equal to 1. What does all that mean? That means, the first time through, cell row 1, column 1 (A1) will end up with a 1 in it. We then increment the value of i by 1, meaning that, the second time through the loop, i will be equal to 2. In turn, that means the next value in the array (3) will be plopped into cell row 2, column 1 (A2).

And you’re right: that part is pretty easy, isn’t it? each time we grab a value from the array we simply drop down another row and assign that value to column 1.

We have 8 values in our array; that means that, by the time we exit the loop, cells A1 through A8 will have values. That also means that we’re ready for the last step in the process: putting a formula into cell A9 that adds all the values in cells A1 through A8. How do we do that? Like this:

objExcel.Cells(i, 1).Formula = "=SUM(A1:A" & i - 1 & ")"

All we’re doing here is assigning a value to the Formula property for cell A9 (row 9, column 1). Why does the code look so weird? Well, the easiest way to sum several rows of cells is to use a formula similar to this:

=SUM(A1:A8)

That’s fine, except that it means we need to construct this formula ourselves. In particular, we need to concatenate the following items:

· =SUM(A1:A, which simply represents the beginning of the formula.

· i – 1, which represents the current row minus 1. Why minus 1? Well, remember, we’re currently in row 9; if we used the variable i we’d end up putting a formula in cell A9 that would then to add up all the values in cells A1 through A9. That’s right: the dreaded circular reference. (Not that anything like that happened the first time we tried this script, of course.) The mini-equation i – 1 is enough to prevent a circular reference.

· ), which simply closes out the formula.

In other words:

=SUM(A1:A
+                 8
+                 )
 __________________

        =SUM(A1:A8)

Will that give us the sum total for all the values in cells A1 through A8? Give it a try and see for yourself. And if you’d like to know a little more about working with formulas in Excel (as well as a quick tip for converting a cell address like row 8, column 1 to A8) then take a peek at this Office Space column.

As for us, it’s back to work: so far we’ve narrowed it down to Italian or Chinese. Although there is a diner nearby that has really good pie. We have to tell you the truth: when you’re a Scripting Guy you are under enormous pressure from the time you get up until the time you go back to bed. It’s a tough job, but someone has to do it.

Well, at least until we can get rid of the Scripting Editor. (Editor’s Note: The Scripting Guys need to get much more creative in their attempts to get rid of the Scripting Editor if they hope to succeed. Fortunately [for who, we’re not sure] they’re too lazy to put much effort into it. Hopefully they’ll never think to get tips from the Scripting Editor’s last team….)


How Can I Add Additional Worksheets to an Excel Workbook?

Hey, Scripting Guy! How can I add additional worksheets to an Excel workbook? By default you get 3 worksheets, but I have a scenario in which I need a workbook that has 12 worksheets.

-- AA

Hey, AA. You know, a couple years ago one the Scripting Guys helped put together an Excel spreadsheet for one of the local youth baseball leagues. Because there were 18 teams in the league, he was asked to create a .XLS file with 18 worksheets in it. And because he has Excel configured to use just one worksheet per new workbook (waste not, want not) that meant he had to click Insert, select Worksheet, and then click OK 17 times. Wouldn’t it have been faster and easier to do all that using a script? He’d rather not talk about it. (Editor’s Note: And, no, we can’t tell you which Scripting Guy did this: Greg would be terribly embarrassed if anyone ever found out.)

Fortunately, AA, you don’t have to do things the hard way. Assuming you have Excel configured to include three worksheets with each new workbook, here’s a script that adds nine more worksheets for you:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set colSheets = objWorkbook.Sheets

colSheets.Add ,,9

Yes, you’re right: except for a slightly-cryptic last line this is a pretty simple little script. We begin by creating an instance of the Excel.Application object and then setting the value of the Visible property to True; this gives us a running instance of Excel onscreen. We then use these two lines of code to add a new workbook (which, unless you’ve changed the default setting, will have three worksheets in it) and create an object reference to the Sheets collection:

Set objWorkbook = objExcel.Workbooks.Add()
Set colSheets = objWorkbook.Sheets

The rest is easy. We want to add nine additional worksheets to the Sheets collection, so we call the Add method, specifying the number of new worksheets (9) as the third parameter:

colSheets.Add ,,9

Cool, huh?

Good question: what about parameters 1 and 2? Well, by default new worksheets are added in front of the existing worksheets; that means sheets 1, 2, and 3 (the default worksheets) will actually be pushed to the tail-end of the collection. If you don’t like that you can use the optional parameters 1 and 2 to specify a particular worksheet before which (parameter 1) or after which (parameter 2) all the new sheets will be added. To do that, simply create an object reference to the appropriate worksheet and then use that object reference as the first or second parameter. For example, this script adds the nine new worksheets directly after worksheet 1 (thus pushing sheets 2 and 3 to the end of the collection):

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)
Set colSheets = objWorkbook.Sheets

colSheets.Add ,objWorksheet,9

As you can see, we use this line of code to create an object reference to worksheet 1:

Set objWorksheet = objWorkbook.Worksheets(1)

And then we use that object reference as the second parameter in order to specify that the new worksheets get added after worksheet 1:

colSheets.Add ,objWorksheet,9

That’s all there is to it. To add all the new worksheets directly in front of sheet 3, you’d use code like this, this time using the object reference as the first parameter:

objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(3)
Set colSheets = objWorkbook.Sheets

colSheets.Add objWorksheet,,9

One other little note: when you add new worksheets they always get added in “backwards.” That is, suppose you add three new worksheets after worksheet 3. You might expect to see the worksheets arranged in this order:

Sheet1
Sheet2
Sheet3
Sheet4
Sheet5
Sheet6

Nope. Instead, you’ll see this:

Sheet1
Sheet2
Sheet3
Sheet6
Sheet5
Sheet4

We knew you were going to ask that. In answer to your question, take a look at this Hey, Scripting Guy! column to learn how you can sort worksheets within a workbook.

And you’re right, you could also reconfigure Excel to give you 12 worksheets in every new workbook by default. For more information on that, see The Picture Book of Microsoft Office Management.

But if you’re wondering why someone who calls himself a Scripting Guy didn’t even use a script to save him a bunch of time and trouble, well, we don’t have the answer to that question.


How Can I Assign a Background Color to Cells in a Spreadsheet and Then “Sum” Those Cells?

Hey, Scripting Guy! How can I assign a background color (based on a specified value) to a spreadsheet cell and then “sum” those cells?

-- JC

Hey, JC. Thanks for the question. And yes, you heard correctly: in his last game the Scripting Son did indeed go 4-for-4, banging out three singles and a double. That outing raised his batting average for the season to an even .500 which, all things considered, isn’t bad at all.

Of course, as the Scripting Dad likes to remind him, a .500 batting average means that the Scripting Son still fails one out every two times he steps to the plate. Although come to think of it, failing only once every two tries is a way better percentage than the Scripting Dad has when it comes to writing this column. Hmmm ….

But you’re in luck, JC, and on two accounts. For one, this happens to be one of those rare occasions when we actually do have an answer to a question. For another, we have a number of other things we have to do today; that means we can’t spend as much time on meandering and meaningless anecdotes as we usually do. Instead, we’re going to cut to the chase and get right to the script.

Well, in just a minute, that is. Did you know that the phrase “cut to the chase” originated in Hollywood in the 1920s? In the movie world “cut to the chase” meant skipping past the boring parts of the film – like characters discussing their dreams and aspirations – and getting to the good stuff: the chase scene. As a general rule we don’t use the phrase “cut to the chase” in relation to Hey, Scripting Guy!; after all, if you skipped the boring parts there wouldn’t be much left to the column, would there?

With that in mind, here’s the scripting equivalent of a chase scene, a script that can assign a background color to cells in a spreadsheet and then “sum” those cells:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook
= objExcel.Workbooks.Add()
Set objWorksheet
= objWorkbook.Worksheets(1)

objExcel.Cells(1, 1).Value = "A"
objExcel.Cells(2, 1).Value = "B"
objExcel.Cells(3, 1).Value = "C"
objExcel.Cells(4, 1).Value = "D"
objExcel.Cells(5, 1).Value = "E"
objExcel.Cells(6, 1).Value = "F"
objExcel.Cells(7, 1).Value = "G"
objExcel.Cells(8, 1).Value = "H"

objExcel.Cells(1, 1).Interior.ColorIndex
= 7
objExcel.Cells(2, 1).Interior.ColorIndex
= 8
objExcel.Cells(3, 1).Interior.ColorIndex
= 9
objExcel.Cells(4, 1).Interior.ColorIndex
= 10
objExcel.Cells(5, 1).Interior.ColorIndex
= 7
objExcel.Cells(6, 1).Interior.ColorIndex
= 7
objExcel.Cells(7, 1).Interior.ColorIndex
= 8
objExcel.Cells(8, 1).Interior.ColorIndex
= 10

i = 1

Do Until objExcel.Cells(i, 1).Value = ""
intColor
= objExcel.Cells(i, 1).Interior.ColorIndex

Select Case intColor
Case 7 intSum = intSum + 5
Case 8 intSum = intSum + 10
Case 9 intSum = intSum + 15
Case 10 intSum = intSum +
20
End Select

i
= i + 1
Loop

Wscript.Echo intSum

To begin with we should note that we took a few liberties here. For example, JC has a script designed to enter a specific value into a cell and then set the background color of that cell based on the value entered. We didn’t bother setting up a fancy system like that; instead, we simply threw some data into some cells and then arbitrarily changed the background color of each cell. But that’s OK: after you see how easy it is to change the background color of a cell, and how easy it is to “read” these background colors, well, you won’t find it too terribly hard to adapt this script as needed.

Which reminds us: did we mention where the term “cut to the chase” came from? We did? In that case, let’s cut to the chase and discuss how the script works. We start out – in truly dramatic fashion – by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Excel that we can see on screen. We then use these two lines of code to create a new workbook and to bind to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

Next we have several lines of code that simply enter values into cells A1 through A8. For example, this command sets the value of cell A1 (row 1, column 1) to the letter A:

objExcel.Cells(1, 1).Value = "A"

And once we’ve assigned values to the cells we then use code similar to this to (again, arbitrarily) change the background color of each cell:

objExcel.Cells(1, 1).Interior.ColorIndex = 7

You can see how easy it is to change the color of a cell: all you have to do is assign the appropriate color integer to the Interior.ColorIndex property. The only tricky part here is knowing which integers correspond to which colors; for example, what color is 7? Fortunately, you can find out everything you need to know about colors and color values by taking a look at our Office Space article on that very topic.

So where are we? Turns out we’re right here, with a spreadsheet that looks like this:

What we want to do now is sum up the cells in the spreadsheet. However, we aren’t going to do that by adding the spreadsheet values; obviously adding A plus B plus C plus etc. wouldn’t make much sense. Instead, we want to determine the background color of the cell, assign that cell a value based on the color, and then add those values. Confusing? Don’t worry; we’ll explain it all in detail. And when we’re done with that we’ll talk about our dreams and aspirations.

Before we start reading the individual cells we have a line of code that assigns the value 1 to a counter variable named i; we’ll use this variable to keep track of our position (row) in the spreadsheet. We then set up a Do loop that runs until it encounters a blank cell in column A (at that point we’re assuming we’ve run out of data):

Do Until objExcel.Cells(i, 1).Value = ""

The first thing we do inside this loop is determine the background color of the cell and then assign the integer value to a variable named intColor; that’s what we do here:

intColor = objExcel.Cells(i, 1).Interior.ColorIndex

And once we know the background color of the cell we can start doing a little math:

Select Case intColor
    Case 7 intSum = intSum + 5
    Case 8 intSum = intSum + 10
    Case 9 intSum = intSum + 15
    Case 10 intSum = intSum + 20
End Select

What we’ve done here is set up a Select Case statement based on the value of intColor. In addition, we’ve assigned numeric values to each of the colors used in the spreadsheet. For example, the color 7 (a kind of pinkish-purplish sort of thing) has been assigned the value 5; the color 8 (sort of a cyan … we think) has been assigned the value 10; etc. Inside our Select Case statement we increment another variable (intSum). How much do we increment this variable by? Well, that depends on the cell color. For example, suppose we have a pinkish-purple cell. In that case intColor will be equal to 7, our script will execute this line of code, and intSum will be incremented by 5:

Case 7 intSum = intSum + 5

Again, nothing too terribly complicated here. If intColor equals 7 then we assign a new value to the variable intSum: the current value of intSum plus 5. The first time through the loop intSum is equal to 0; because of that, when we encounter that pinkish-purplish cell in row 1, column 1 our equation will be 0 + 5 = 5. As a result, intSum will be equal to 5.

Once we exit the Select Case statement we increment our counter variable by 1, then loop around and repeat the process with the cell in row 2, column 1. After we’ve looped through all the cells the script echoes back the dramatic conclusion:

90

Well, OK. But trust us, it’s more exciting to see it in person than it is to read about it.

And that should do it, JC. Like we said, we have several other things to take care of this afternoon, so we’re going to have to cut today’s column short. We apologize that you didn’t get to hear more about the Scripting Son. But don’t worry: as usual, he’ll be playing baseball on into the month of August. No doubt you’ll get to hear plenty of stories before the season is done.


How Can I Build an Array from a Column of Data in Excel?

Hey, Scripting Guy! How can I build an array from a column of data in Excel?

-- AH

Hey, AH. Obviously none of the Scripting Guys has any need to go on a diet (perish the thought!), but we do know that one time-honored dieting strategy is the theory of tiny bites: instead of gulping down your food in a few huge swallows, you nibble at it with scores of tiny little bites, the idea being that the more bites you take the more your body will assume that it’s full. To tell you the truth, we have no idea whether the tiny bites strategy works for dieting; however, it works quite well for building an array from a column of data in Excel.

So what is the tiny bites strategy in this case? Well, we don’t know of any way to grab all the data in a column and, with a single line of code, magically turn it into an array. But that’s OK, because it’s just as easy to grab a value from a cell, store that value in an array, grab the value in the next cell, store that value in the array, and so on. In other words, we build our array by taking tiny bites of data from the spreadsheet, like this:

Dim arrExcelValues()

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
objExcel.Visible = True

i = 1
x = 0

Do Until objExcel.Cells(i, 1).Value = ""
    ReDim Preserve arrExcelValues(x)
    arrExcelValues(x) = objExcel.Cells(i, 1).Value
    i = i + 1
    x = x + 1
Loop

objExcel.Quit

For Each strItem in arrExcelValues
    Wscript.Echo strItem
Next

Before walking through the code, we should note that we’re assuming you have a spreadsheet in which the data to be retrieved is in column A; in addition, we’re assuming that the data starts in cell A1 and continues downward, with no blanks cells until you reach the end of the data. In other words, a spreadsheet that looks like this:

If your spreadsheet doesn’t look like this (for example, if it has a header row or if it includes blank cells), then you’ll have to adjust the code accordingly.

Now, what about that code. Well, we begin by defining a dynamic array named arrExcelValues; as you might expect, this is the array that we’ll populate using the values grabbed from the spreadsheet:

Dim arrExcelValues()

We then create an instance of the Excel.Application object, use the Open method to open the file C:\Scripts\Test.xls, and then set the Visible property to True (enabling us to see our spreadsheet on screen).

Next we assign values to a pair of counter variables, i and x:

i = 1
x = 0

We’re going to use i to keep track of our position in the spreadsheet; because our data starts in row 1 we set the value to 1. Meanwhile, we’re going to use x to track our position within the array arrExcelValues. Because the first item in an array is always item 0, we set the starting value of x to 0.

Next we set up a Do Until loop that will continue looping until it encounters a blank cell in column A; that line of code looks like this:

Do Until objExcel.Cells(i, 1).Value = ""

Inside that loop, we use the ReDim Preserve method to resize the array arrExcelValues (and to preserve any data currently in that array). We pass ReDim Preserve the value x, which represents the size of the array. We then set the value of the current item in that array to the value of cell A1 (row 1, column 1) in the spreadsheet:

ReDim Preserve arrExcelValues(x)
arrExcelValues(x) = objExcel.Cells(i, 1).Value

In other words, the first time through the loop, we grab the value from cell row 1, column 1 and store it as array item 0. Make sense? We then increment the values of our two variables by one each:

i = i + 1
x = x + 1

Why? Well, the second time through the loop i will be equal to 2; that means we’ll be grabbing the value from row 2, column 1. Likewise, the value of x will be 1, which means we’ll be assigning a value to item 1 in our array (because the first item in an array is item 0, the second item in the array has to be item 1.) We then increment the variables, loop around, and repeat the process. This continues until we encounter a blank cell in column A.

Once we encounter a blank cell we call the Quit method to dismiss our instance of Excel, and then use a simple For Each loop to echo back the items in arrExcelValues, something we do just to show you that we really did build an array using the values found in the spreadsheet:

For Each strItem in arrExcelValues
    Wscript.Echo strItem
Next

Quick, easy, and guaranteed non-fattening. What more could you ask for?


How Can I Center Text in an Excel Cell?

Hey, Scripting Guy! How can I center text in an Excel cell?

-- MV

Hey, MV. You know, every now and then we get asked a question and we think to ourselves, “Wow, how many times do you suppose we’ve answered this question?” And, of course, when we go to double-check we find out that we’ve never answered that question; instead, it just seems like we should have answered the question somewhere along the line. We just didn’t remember.

Before you ask, this has nothing to do with the Scripting Guys getting old; in fact, thanks to hard work and clean living none of the Scripting Guys are getting old. And those are not grey hairs; they’ve just been bleached out by the sun.

Note. Sure it’s tough, but if you’re going to make your home in sun-drenched Seattle then having your hair bleached out – so much so that your hair begins to look grey – is just one of those things you have to learn to live with.

At any rate, it turns out that we’ve never told people how to center text in an Excel cell. Well, until now, that is:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add

Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1) = "A"
objWorksheet.Cells(1, 2) = "B"
objWorksheet.Cells(1, 3) = "C"

objWorksheet.Cells(1, 2).HorizontalAlignment = -4108

At the risk of spoiling the suspense, we’ll tell you that the only line of code you really need to worry about is the last one; the rest of the script simply creates an Excel spreadsheet and adds some data to it. To do that, the script first creates an instance of the Excel.Application object and then sets the Visible property to True; that gives us a running instance of Excel that’s visible on screen. We use the Add method to add a new workbook to our instance of Excel, then use this line of code to bind to the first worksheet in that workbook:

Set objWorksheet = objWorkbook.Worksheets(1)

Of course, a script that centers text in a cell isn’t all that impressive unless you actually have some text in that cell. Therefore, we use these three lines of code to put the letters A, B, and C into cells A1, B1, and C1, respectively:

objWorksheet.Cells(1, 1) = "A"
objWorksheet.Cells(1, 2) = "B"
objWorksheet.Cells(1, 3) = "C"

In case you’re wondering, that gives us a spreadsheet that looks like this:

Now how do we center the text in one of the cells? Well, this line of code centers the text for cell B1 (that is, cell row 1, column 2):

objWorksheet.Cells(1, 2).HorizontalAlignment = -4108

As you can see, all we have to do is set the value of the HorizontalAlignment property of the cell to -4108; that will center the text in the cell. If we wanted the cell to be right-aligned, we’d set HorizontalValue to -4152; set the value to -4131 to left-align the text.

Run the script, and the finished spreadsheet will look like this, with the text in cell B1 centered:

Another day, another question answered. Speaking of which, how many times have we answered a question about centering text in Excel?

Oh, right: once. The sun appears to be bleaching out our brains as well as our hair ….


How Can I Change All the Lowercase Letters in an Excel Worksheet to Uppercase Letters?

Hey, Scripting Guy! How can I change all the lowercase letters in an Excel worksheet to uppercase letters?

-- BC

Hey, BC. Have you ever heard the old expression, “If you want something done you have to do it yourself”? Well, to tell you the truth, we Scripting Guys had heard of that old expression, but we never paid much attention to it. Do something ourselves, when maybe someone else could do it for us? That’ll be the day! (Editor’s Note: Actually, the expression is “If you want something done right you have to do it yourself.” But the Scripting Guys don’t pay much attention to that either.)

Much to our surprise, however, it turns out that today is the day. When we first saw your question we figured, “Well, this should be easy: we’ll just get Excel to change the letter case for us.” Or so we thought. Try as we might, however, we couldn’t figure out a way to get Microsoft Excel to make this change for us. We assumed that Excel had a Change Case command similar to that in Microsoft Word; it doesn’t. We assumed that we could simply select all, change the font properties and make all the letters uppercase; we couldn’t. We assumed we could do a search-and-replace, changing the letter case along the way; no such luck.

Needless to say, by that time we were in a state of panic. But then Dean Tsaltas, the wisest of all the Scripting Guys, said, “If you need to change the letter case, why not just make that change yourself, without relying on a built-in Excel function to make the change for you.” Talk about profound; no wonder all the Scripting Guys look up to Dean and turn to him whenever we need advice.

Note. Do all the Scripting Guys really turn to Dean when we need advice? What do you think we are: crazy? Of course not. However, Dean thinks we pick on him all the time in this column, so we decided to say something nice about him for a change. He’ll never know whether we really mean it or not!

As usual, once we listened to Dean the rest was easy:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1,1) = "abcdef"
objWorksheet.Cells(1,2) = "ghijkl"
objWorksheet.Cells(1,3) = "mnopqr"
objWorksheet.Cells(1,4) = "stuvwx"

Wscript.Sleep 2000

Set objRange = objWorksheet.UsedRange

For Each objCell in objRange
objCell.Value = UCase(objCell.Value)
Next

Let’s talk about what we did, and why. We start off by creating an instance of the Excel.Application object and then set the Visible property to True; that simply gives us a running instance of Excel that we can see on-screen. After that we use these two lines of code to create a new workbook, and to bind to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

Note. You say that you’re new to scripting with Microsoft Excel, and none of this makes much sense to you? No problem: just visit the Office Space archive, where you’ll find all sorts of articles devoted to scripting with Microsoft Excel.

After all that initial setup we use these four lines of code to type some characters in cells A1 through D1:

objWorksheet.Cells(1,1) = "abcdef"
objWorksheet.Cells(1,2) = "ghijkl"
objWorksheet.Cells(1,3) = "mnopqr"
objWorksheet.Cells(1,4) = "stuvwx"

We then pause the script for two seconds (Wscript.Sleep 2000), just so we’ll have a chance to see the spreadsheet before we start changing letter cases. Speaking of which, that spreadsheet should look something like this:

This, of course, is the point where we figured Excel would change the letter case for us. As we found out, Excel isn’t going to be of much help here. Therefore, we did what Dean would do, and changed the letter cases ourselves.

Note. OK, we didn’t really do what Dean would do; that would have involved buying a latte and checking out used car prices on the Internet. But you know what we mean.

So what would Dean do in this situation (at least in theory)? Well, he’d no doubt begin by selecting all the cells on the worksheet that contained data. That sounds complicated, but it’s actually no harder than this:

Set objRange = objWorksheet.UsedRange

What we’re doing here is creating a Range object (with the object reference objRange) that’s mapped to the worksheet’s UsedRange property. The used range represents the range of contiguous cells that have data in them. For example, in our sample spreadsheet we have data in cells A1 through D1; thus the used range consists of cells A1 through D1. Suppose we had data in cell A1 and another piece of data in cell Z100. In that case the used range would consist of cells A1 through Z100, including all the blank cells in between.

Yes, a very handy little thing to know about.

Once we have a collection of cells we can set up a For Each loop to walk through each cell in the collection:

For Each objCell in objRange
    objCell.Value = UCase(objCell.Value)
Next

As you can see, inside the loop we do only one thing: we set the value of the cell to the uppercase version of whatever happens to be the value of the cell at the moment. (That’s what the VBScript function UCase is for: it converts letters to uppercase.) For example, the current value of cell A1 is this:

abcdef

After running cell A1 through the UCase function the value of cell A1 will be this:

ABCDEF

Etc., etc.

Here’s what we’ll see once we’ve run through the entire collection:

Which is just exactly what we wanted to see: all the letters on the worksheet have been converted to their uppercase equivalents. Best of all, we did it all by ourselves. Thanks, Dean: like we’ve always said, you’re the best!


How Can I Change the Background Color of a Cell Depending on the Date?

Hey, Scripting Guy! I have a Microsoft Excel spreadsheet I periodically analyze using a script. How can I change the color of a cell to red if the date stored in that cell has passed?

-- DM

Hey, DM. We were going to start today’s column off with some clever and witty remark about red-letter dates, but we couldn’t actually think of one. Therefore, we’ll pretend that we don’t have time for such nonsense and instead get right down to business.

Note. This column is written by the same Scripting Guy who, a short time ago, found himself unable to think of a word that started with a letter K. (Yes, yes: kite, kitten, kid, kindergarten; he’s well aware – now – that millions of words start with the letter K.) At the rate his mind is deteriorating this column will soon consist of a question and then a big blank area where the answer was supposed to go.

Hey: who said that would be an improvement?!?

In order to keep the explanation focused on the task at hand, let’s assume you have a very simple little spreadsheet, one that consists of a series of dates (all in column A). Our simple little spreadsheet looks something like this:

What we want to do is create a script that can open this file, check the date in each cell and, if that particular date has already passed, change the background color of the cell to red. In other words, after the script finishes we want our spreadsheet to look like this:

So can we do this? Well, while our Scripting Guy no longer seems capable of using the English language, he’s at least able to write scripts every now and then:

Set
objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

x = 1

Do Until objExcel.Cells(x,1).Value = ""
If CDate(objExcel.Cells(x,1).Value) < Date Then
objExcel.Cells(x,1).Interior.ColorIndex = 3
End If
x = x + 1
Loop

All things considered it is a short little script, isn’t it? The script starts out by creating an instance of the Excel.Application object and then setting the Visible property to True. We then use the Open method to open the file C:\Scripts\Test.xls. All that simply opens up our spreadsheet and makes it visible on screen.

After our spreadsheet pops up we assign the value 1 to a counter variable named x; we’ll use this variable to keep track of the current row as we work our way through the spreadsheet. Next we, well, start working our way through the spreadsheet, setting up a loop that continues until we encounter a blank value in column A. That’s what this line of code does:

Do Until objExcel.Cells(x,1).Value = ""

Note. Why do we do that? Well, for the sake of simplicity we’re assuming you have no blanks cells in column A. Therefore as soon as we encounter a blank cell we’ll know that we’ve run out of data.

Inside our Do Loop we check to see if the value of the current cell is less than today’s date. We do that by taking the value of the current cell – objExcel.Cells.(x,1).Value – and using the VBScript CDate (character-to-date) function to ensure that the value is treated as a date. We then compare that value to the current date (which we can get by using the Date) function. If the cell value is less then the current date, then we use this line of code to change the interior (background) color of the cell to red:

objExcel.Cells(x,1).Interior.ColorIndex = 3

Note. How did we know that setting the ColorIndex to 3 would give us a red background? We didn’t, at least not until we ran the script referenced here that shows us all the background colors and their corresponding values.

Got all that? If you’re not sure why we’re checking the value of objExcel.Cells(x,1) remember that x is a variable we created to keep track of the current row. Because x starts off with the value 1 what we’re doing the first time through the loop is checking cell (1,1): row 1, column 1. After making this first check we increment x by 1; thus the next time through the loop we’ll be checking cell (2,1): row 2, column 1. Etc.

Just that easy, just that quick. We guess you could say that…that…well, OK, we can’t think of anything to say at the moment. But you get the idea.


How Can I Change the Background Color of Spreadsheet Rows That Have a Date in a Specified Cell?

Hey, Scripting Guy! In an Excel spreadsheet, how can I change the color of a row if there is a date stored a particular cell in that row?

-- GL

Hey, GL. Some of you might have noticed that there was no Hey, Scripting Guy! column yesterday. Is that because the Scripting Guys are slacking off and shirking their responsibilities? Of course; after all, why would yesterday be different than any other day? However, the main reason there was no column yesterday is because yesterday was Memorial Day here in the U.S., and all Microsoft employees got the day off. (Yes, even those who don’t really deserve a day off.) Couldn’t one of the Scripting Guys have come in and published the column anyway? Sure, but at Microsoft we get very few days off, so we all decided to stay home while we had the chance.

Note. In case you’re wondering, the official holidays we get are pretty much the ones you would expect a company like Microsoft to observe: Christmas, Thanksgiving, Arbor Day, and Darth Vader’s Birthday.

No, we’re just kidding; we don’t really get Arbor Day off.

As we noted, at Microsoft we don’t get very many days off, which means that we don’t really need to keep track of which days are work days and which days aren’t. (Well, most of us don’t; there’s at least one Scripting Guy, who shall remain nameless, who once came in to work on a holiday, and actually worked for about 4 hours before realizing that he was the only one in the building.) But if you (like GL) have spreadsheets that include dates, then you might be interested in a script that can highlight any rows where those dates occur. You know, a script like this one:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

i = 1

Do Until objExcel.Cells(i, 1) = ""
    strValue = objExcel.Cells(i, 1)

    If IsDate(strValue) Then
        objExcel.Cells(i, 1).EntireRow.Interior.ColorIndex = 44
    End If

    i = i + 1
Loop

So how does this script work? Well, to begin with, we’re assuming we have a spreadsheet similar to this:

As you can see, this is a pretty simple little file. We have data in some of the cells in column A: a few dates, a number, a text string. Our task is to programmatically pick out the rows that have dates in column A, and then change the background color of those rows. Sound complicated? Trust us, it’s not.

We start out, as we so often do, by creating an instance of the Excel.Application object and setting the Visible property to True; that gives us a running instance of Excel that we can view onscreen. We then use these two lines of code to open the spreadsheet C:\Scripts\Test.xls and bind us to the first worksheet in that file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Once that’s done we assign the value 1 to a counter variable named i; we’ll use this counter variable to keep track of our position in the spreadsheet. (In particular, we’ll use this variable to keep track of which row we happen to be on.)

At this point we’re ready to start changing row colors. We’ve configured our sample spreadsheet to ensure that there are no blank rows in between rows that contain data. Why did we do that? Because that makes it easy to know when we’ve finished checking all the rows that contain data: if column A in a row is empty (“”) that means that we’ve checked everything that there is to check. With that in mind, we set up a Do Until loop that runs until we encounter a blank cell in column A:

Do Until objExcel.Cells(i, 1) = ""

Note. What if we did have blank rows in the spreadsheet? In a case like that we can employ the UsedRange property to help us determine where our data begins and ends. For a sample script that makes use of UsedRange see this Office Space article.

The first thing we do inside the loop is grab the value of cell i, 1; the first time through the loop, that will be row 1, column 1 (i, of course, being the variable that keeps track of the current row.) We then assign the value of that cell to a variable named strValue:

strValue = objExcel.Cells(i, 1)

Next, we use VBScript’s IsDate function to determine whether or not that value is a date:

If IsDate(strValue) Then

If IsDate returns False then we aren’t dealing with a date after all; therefore, we simply increment the value of i by 1, then loop around and repeat the process with the next row in the spreadsheet. (And yes, IsDate relies on your Regional and Language settings when determining if a given value is a date. Just something you should keep in mind.)

OK, but what happens if IsDate comes back True? Well, that can mean only one thing: there’s a date value in column A. In turn, that means we need to change the background color of this row, something we do using this line of code:

objExcel.Cells(i, 1).EntireRow.Interior.ColorIndex = 44

This is kind of an awkward-looking command, but it’s ultimately quite simple. What we’re doing here is selecting the current cell (cell i, 1), then extending the selection to encompass the entire row; that’s what the EntireRow property does. We then change the background color by setting the Interior.ColorIndex property to 44. When all is said and done, and when we’ve finished going through the entire file, that’s going to result in a spreadsheet that looks like this:

See how that works? If there’s a date in cell A then that row is going to be colored a … lovely … orangish-yellowish-brownish kind of color.

Note. No, you aren’t stuck with orangish-yellowish-brownish as your only background color. Take a peek at this Office Space article for a look at the different colors available to you, as well as their corresponding numeric values.

And there you have it, GL. Incidentally, if you’re trying to make holiday plans we have to confess that we don’t actually know the date of Darth Vader’s birthday; all we know for sure is that he was born in the year 41.9 BBY. And yes, that is significant; after all, the year 42 BBY is known as Year 7 before the Great Resynchronization and Year 958 after the Ruusan Reformation. The year 42 BBY is also the year Gilead Pellaeon entered the Raithall Academy and the same year that Omo Bouri died. (Bouri, of course, is best known for negotiating the Treaty of Trammis in 124 BBY.)

And no, we don’t have the slightest idea what any of that means. Fortunately, though, not having any idea what we’re talking about has never been, and never will be, a deterrent for Hey, Scripting Guy!


How Can I Change the Font Color in Excel If a Specified Condition is Met?

Hey, Scripting Guy! In an Excel spreadsheet, how can I change the color of the text if the value in column B minus the value in column A is greater than or equal to 10?

-- EP

Hey, EP. You know, before the Scripting Guy who writes this column started writing this column, he vowed that Hey, Scripting Guy! would have a laser-like focus on helping people learn system administration scripting; Hey, Scripting Guy! would not be a forum for inane rantings and ravings that no one else cared about. Throughout the years has he managed to stick to that vow? Let’s put it this way: have you seen the commercial featuring the Rock’em Sock’em Robots? The Scripting Guy who writes this column this column has, and he finds this commercial very distressing.

Very distressing.

If you haven’t seen the commercial, two life-size Rock’em Sock’em Robots (made of metal, oddly enough) are duking it out in a boxing ring. After defeating the red robot the blue robot crashes through the gymnasium door, terrorizes a couple of people on the street, and then proceeds to attack a pickup truck, a fight he ends up losing.

So what’s the big deal here? Well, let’s be realistic here: the Rock’em Sock’em Robots would never terrorize innocent people, nor would they pick a fight with a pickup truck that was just sitting there minding its own business. The Scripting Guy who writes this column had Rock’em Sock’em Robots when he was a kid; the Scripting Son had Rock’em Sock’em Robots when he was a kid. In all that time those robots never once left the ring, nor did they ever attack anyone other than the other robot. A Rock’em Sock’em Robot would never do anything like that; they have too much class. This is an outrage and something needs to be done about it.

What’s that? Are we finished ranting and raving? Sure … for now. Why do you ask?

Oh, right: a script that can do a little math and then, as needed, change the text color in Microsoft Excel. Hey, all you had to do was ask:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

i = 1

Do Until objWorksheet.Cells(i, 1) = ""
    intA = objWorksheet.Cells(i, 1)
    intB = objWorksheet.Cells(i, 2)
    If intB - intA >= 10 Then
        objWorksheet.Cells(i, 1).Font.ColorIndex = 3
        objWorksheet.Cells(i, 2).Font.ColorIndex = 3
    End If
    i = i + 1
Loop

According to EP he has a spreadsheet which – in simplified fashion – looks something like this:

As you can see, there’s a year (e.g., 2002) shown in column A and another year (e.g., 2004) shown in column B. Our task? Subtract column A from column B (2004 – 2002) and then, if the result is greater than or equal to 10, change the font color for that particular row. Can we do that? Assuming that we don’t have to stop for any further rants, you bet we can.

Our script starts in straightforward fashion: we create an instance of the Excel.Application object and then set the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We then use these two lines of code to open the file C:\Scripts\Test.xls and bind to the first worksheet in the file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Once that’s done we create a counter variable named i and assign it the value 1; we’ll use this variable to keep track of the current row in the worksheet.

Got all that? Good; now it’s time to do some real scripting. In order to keep matters as simple as we can, we’re assuming that the data begins in row 1, column 1; that is, there’s no header row. In addition, we’re assuming that there are no blanks rows in the data. Based on those assumptions, our next chore is to set up a Do Until loop that runs until we encounter a blank cell in column A; that’s what this line of code is for:

Do Until objWorksheet.Cells(i, 1) = ""

Remember, the counter variable i keeps track of the row for us. When our loop begins, that means we’re checking to see if cell row 1, column 1 is blank. If it is, then the script is finished.

If it’s not we then take the value of cell row 1, column 1 and assign it to a variable named intA; needless to say, this variable represents the value of cell A1 (row 1, column 1). We then take the value of row 1, column 2 and assign it to a variable named intB; in turn, this variable represents the value of cell B1. That’s what these two lines of code do:

intA = objWorksheet.Cells(i, 1)
intB = objWorksheet.Cells(i, 2)

As soon as we have the values in columns A and B we can then use this line of code to determine whether or not the result of subtracting column A from column B is greater than or equal to 10:

If intB - intA >= 10 Then

If the value is not greater than or equal to 10 then we don’t do anything at all. But what if the value is greater than or equal to 10 (as it will be in row 3, when we subtract 1992 from 2004)? In that case, we execute these two lines of code:

objWorksheet.Cells(i, 1).Font.ColorIndex = 3
objWorksheet.Cells(i, 2).Font.ColorIndex = 3

What’s going on here? Well, in the first line we’re simply setting the Font.ColorIndex property for the cell in column A to red. (A value of 3 means red; for more information on available colors and their values, see this Office Space article.) And in the second line – that’s right, in the second line we’re setting the Font.ColorIndex property for the cell in column B to red. When all is said and done our spreadsheet should look a little something like this:

And there you have it.

Now, getting back to the Rock’em Sock’em Robots, the Scripting Guy who writes this column recommends that, as a way to register our collective disappointment, we all agree to boycott sushi restaurants. That’s a tough thing to do and a tough sacrifice to make. But it’s the only way these companies will learn.

Note. OK, sure, if you want to get picky the commercial has nothing to do with sushi restaurants; instead, it’s for a car company. But the Scripting Guy who writes this column can’t boycott cars: he lives in Seattle, a city that’s pretty much devoid of mass transit. (In Seattle we don’t ever build mass transit systems; instead, we just vote on mass transit systems, year after year after year.) By contrast, sushi restaurants are something he can boycott, and without any inconvenience to himself whatsoever. And isn’t that the best kind of boycott?


How Can I Change the Footer in an Excel Spreadsheet?

Hey, Scripting Guy! How can I change the footer in an Excel spreadsheet?

-- JA

Hey, JA. Ah, a trick question, huh? As it turns out, there’s no such thing as the footer in Excel; instead, Excel has three different footers: the left footer, the center footer, and the right footer. And here’s a script that can change all three of them:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.PageSetup.LeftFooter = "Left footer"
objWorksheet.PageSetup.CenterFooter = "Center footer"
objWorksheet.PageSetup.RightFooter = "Right footer"

As you can see, there really isn’t all that much to this script. We begin by creating an instance of the Excel.Application object and then set the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We use the Add method to add a new workbook, then use this line of code to bind to the first worksheet in the Worksheets collection:

Set objWorksheet = objWorkbook.Worksheets(1)

All we have to do now is assign values to LeftFooter, CenterFooter, and RightFooter, all of which happen to be properties of the worksheet’s PageSetup object:

objWorksheet.PageSetup.LeftFooter = "Left footer"
objWorksheet.PageSetup.CenterFooter = "Center footer"
objWorksheet.PageSetup.RightFooter = "Right footer"

Cool.

OK, that’s a good point: it’s possible that you don’t want your center footer to simply read “Center Footer.” Obviously you can assign any text you want to these footers. In addition, you can also include some of Excel’s special formatting codes within a footer. For example, here’s a script that uses the current date as the center footer:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.PageSetup.CenterFooter = "&D"

There’s really nothing to it: we simply use the formatting code &D to insert the date. (Note that the formatting code must be enclosed in quotation marks, just like any other string value.)

Here’s a slightly fancier footer, one that gives you a Page 1 of 9 sort of thing. Notice that we can mix both regular old text and the special formatting codes in the same string value:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.PageSetup.CenterFooter = "Page &P of &N"

You can see the value we assigned to the footer: Page &P of &N. That simply means “Put the following in the footer: the word Page, followed by the current page number (&P) followed by the word of, followed by the total number of pages in the spreadsheet (&N).” Child’s play.

And now we have a trick question for you: If you spell "sit all day in the tub" S-O-A-K, and you spell "a funny story" J-O-K-E, how do you spell "the white of an egg?"

Nope, sorry: the white of an egg is spelled A-L-B-U-M-E-N. (Thank goodness you asked us an Excel footer question instead of a trick question like that.)

(Editor’s Note: You might be thinking that the editor should have stepped in and saved you from such a bad joke, but sometimes it’s easier to let the Scripting Guys (okay, Greg) think they’re (he’s) funny. You can just laugh politely and walk away like this editor tends to do.)


How Can I Color Every Other Row in an Excel Spreadsheet?

Hey, Scripting Guy! When I create a new Excel spreadsheet, I’d like to color every other row in that spreadsheet. How can I do that?

-- TM

Hey, TM. We know a lot of you have been wondering, “Gee, what’s the matter with the Scripting Guy who writes that column? He just doesn’t seem like his usual, jolly old self these days.” Sadly, that’s true. And there’s a reason for that: while in Orlando for TechEd 2007, the Scripting Guy who writes this column had both his confidence and his ego shattered in one crushing blow.

At one point during the festivities, the Scripting Guy who writes this column left the Scripting Guys’ booth in an attempt to find a place where the dynamic duo could store a box for a few hours. (How hard could it be to find a place to store one little box in a building the size of the Orlando Convention Center? Surprisingly hard. And yet, nowhere near as hard as it was trying to get that one little box back out of storage.) When the Scripting Guy who writes this column returned from his quest, Scripting Guy Jean Ross was busy chatting with someone. “Oh,” said this someone as he returned to the booth. “This must be the other Scripting Guy.”

The other Scripting Guy!?!? The Scripting Guy who writes this column single-handedly invented the Script Center, for goodness sake! How many Script Centers do you suppose Scripting Guy Jean Ross has invented? That’s right: zero. And yet Scripting Guy Jean Ross – who, we might point out, isn’t even a guy – has somehow become the Scripting Guy. And the Scripting Guy who writes this column? He has somehow become the other Scripting Guy.

The other Scripting Guy. Was George Harrison the “other” Beatle to John and Paul? Was Chico the “other” Marx Brother to Groucho? Was – well, never mind. You know what we mean.

Of course, people are truly defined by how they react to adversity. The Scripting Guy who writes this column reacted to adversity by getting very depressed and withdrawing into his office, his life pretty much over. What’s that? Write a really good column, one that, say, colors every other row in an Excel spreadsheet. You say that will show Jean Ross who the real Scripting Guy is? You know what? You’re right: that would show Jean who the real Scripting Guy is, wouldn’t it?

On the other hand, that sounds like a lot of work, and it sounds kind of hard, to boot. Sorry; we’ll pass on that.

Unless … Guess what? In rummaging through Jean’s desk, looking for a … paperclip … we just happened to run across a script that can color every other row in a spreadsheet. The other Scripting Guy? Not anymore, Jean Ross:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

For i = 1 to 5
    If i Mod 2 = 0 Then
        Set objRange = objExcel.ActiveCell.EntireRow
        objRange.Cells.Interior.ColorIndex = 37
    Else
        Set objRange = objExcel.ActiveCell.EntireRow
        objRange.Cells.Interior.ColorIndex = 36
    End If

    objWorksheet.Cells(i,1) = i

    intNewRow = objExcel.ActiveCell.Row + 1
    strNewCell = "A" &  intNewRow
    objExcel.Range(strNewCell).Activate

Next

OK, so how does this script work? (Good question; unfortunately, Jean didn’t seem to leave an explanation lying around anywhere. We’ll have to look through her purse the next time she goes to the bathroom.) As near as we can tell, the script starts out by creating an instance of the Excel.Application object, and then sets the Visible property to True; that gives us a running instance of Excel that we can see onscreen. Jean then – um, we then use the Add method to add a new workbook, then use this line of code to bind to the first worksheet in the workbook:

Set objWorksheet = objWorkbook.Worksheets(1)

That was easy enough, wasn’t it? OK. In our sample spreadsheet, we’re going to put data in the first five rows; therefore, we set up a little For Next loop that runs from 1 to 5:

For i = 1 to 5

Inside that loop, the very first thing we do is execute this line of code:

If i Mod 2 = 0 Then

What does this line of code do? Well, it … um, it – hold on second, we need to go ask Jean a question …..

OK, we’re back. As we were saying – and as any real Scripting Guy knows – the Mod function returns the remainder of a division problem. All we’re doing here is dividing the counter variable i by 2 and then checking to see if the remainder is 0. Why do we do that? Just a second … we do that as a way to keep track of alternating rows. Odd-numbered rows (1, 3, 5, 7, etc.) will not have a remainder of 0; that’s because 2 doesn’t divide evenly into an odd number. Even-numbered rows, by comparison, will have a remainder of 0. Even-odd, even-odd, even-odd: a simple way to keep track of alternating rows.

So what if the remainder is 0? In that case we’re going to execute this block of code:

Set objRange = objExcel.ActiveCell.EntireRow
objRange.Cells.Interior.ColorIndex = 37

As you can see, we’re doing two things here. First, we’re creating a range object that encompasses all the cells in row 1. How do we know that this range object encompasses all the cells in row 1? Hey, even the Scripting Guy who writes this column knows the answer to that one. With this line of code we’re referencing the EntireRow property for the row containing the ActiveCell (the cell that currently has the cursor in it). By default, any time you programmatically create a new spreadsheet the cursor gets placed in cell A1. And because cell A1 is in row 1, then that must mean we’re selecting all the cells in row 1.

Note. Take that, Scripting Guy Jean Ross!

In the second line of code we then set the background color (Interior.ColorIndex) of all the cells in row 1 to a light yellow. How did we know that the value 37 would result in a light yellow? That’s also an easy one: for some reason, Jean Ross has all the Excel color values tattooed to her forearm. We just waited until she rolled up her sleeves and then took a quick peek. If you can’t see Jean’s forearms from where you are, don’t panic: there is an alternative. For more information, see this Office Space article.

Now, what if the remainder isn’t 0? Well, in that case we could just leave the row uncolored. But that’s what someone like Scripting Guy Jean Ross would do. A real Scripting Guy would color the odd-numbered rows a light blue, using this block of code:

Set objRange = objExcel.ActiveCell.EntireRow
objRange.Cells.Interior.ColorIndex = 36

After coloring the row the appropriate shade, we then assign the value i to the cell in column A of the current row:

objWorksheet.Cells(i,1) = i

That’s no big deal; we just do that to make sure our spreadsheet has some data in it. We then run this block of code:

intNewRow = objExcel.ActiveCell.Row + 1
strNewCell = "A" &  intNewRow
objExcel.Range(strNewCell).Activate

Why this block of code? Well, according to Jean, programmatically adding data to a new row doesn’t cause the active cell to change; if we don’t explicitly make a change then cell A1 will remain the active cell forever and ever. That wouldn’t be very good; if that happened we’d end up doing nothing more than changing the background color of row 1 over and over again. Therefore, we do the following instead.

First, we assign the value of the active cell’s Row plus 1 to a variable named intNewRow. Why “plus 1?” Well, let’s assume that the active cell is currently in row 1; that means we need to move the active cell to row 2. And because 1 + 1 is – we’ll be right back; hey, Jean, what’s 1 + 1? Anyway, because 1 + 1 is 2, well ….

We then combine the new row number with the letter A, assigning that value to a variable named strNewCell:

strNewCell = "A" &  intNewRow

Needless to say, the first time through the loop that will make strNewCell equal to A2. That means that we can then call the Activate method and make cell strNewCell (A2) the active cell:

objExcel.Range(strNewCell).Activate

From there we loop around and repeat the entire process with the next row in the worksheet.

When all is said and done we’ll end up with a worksheet that looks remarkably like this:

And we did it without any help from Jean Ross.

Pretty much.

By the way, we should point out that we have nothing against Scripting Guy Jean Ross; heck, some of our best friends are named Scripting Guy Jean Ross. It’s just that we don’t see what Jean has done to merit the title of the Scripting Guy. OK, granted, in the first year after she joined the team our page views more than doubled; that’s definitely impressive. And sure, Dr. Scripto’s Fun Book, which was one of the most sought-after giveaways at TechEd, was entirely her idea. And, fine: she’s the only Scripting Guy capable of keeping the Script Center going from day-to-day. But other than that ….


How Can I Compare a List of Names in One Excel Column to a List of Names in Another Column?

Hey, Scripting Guy! In Excel, how can I compare a list of names found in one column with a list of names found in another column?

-- GS

Hey, GS. You know, in general we Scripting Guys are charged with presenting people with facts; in turn, we’re really supposed keep our opinions to ourselves. However, sometimes there are things that just have to be said, whether anyone likes it or not. And this happens to be one of those occasions.

On his way to work this morning the Scripting Guy who writes this column learned something absolutely mind-boggling: in most households (at least in the U.S.) dogs are forced to watch the same television shows as everyone else! Whoa, take it easy; we aren’t accusing any of you of being this heartless. But, like it or not, when people turn on the TV they almost always turn on a program designed for, well, people. Your poor dog is just plain out of luck.

Of course, we humans have a ready-made excuse for that behavior. “We just watch whatever happens to be on,” we say. “It’s not our fault if there aren’t any TV shows made for dogs.” Now, admittedly, that used to be true; at one time there weren’t any TV shows for dogs. But now there are.

Or at least there’s one: The Dog Tales - The Mailman DVD. To quote from the Web site, “Your dog will never be bored again after meeting Collin, Whistler, McLendon and the gang. In this episode hair stands on end when the dreaded Mailman pays a visit to Long Leash Lane!” So there you go, GS. If you – like so many of us – are concerned that your dog hasn’t been watching enough TV lately, well, your prayers have been answered.

Interesting Tidbit. There’s a sample clip from The Mailman on The Dog Tales Web site. What we found intriguing was the fact that, in several scenes, something happens – say, a dog is barking – and a caption appears at the bottom of the screen letting you know what the dog is saying. We assume that caption is meant as an aid to humans, but we don’t know that for sure. After all, the DVD is aimed specifically at dogs; it’s even filmed in “Dog-E-Vision.” (No, really, that’s what it’s called: Dog-E-Vision.)

Of course, maybe dogs have been able to read along. Seeing as how they can’t talk, though, we’d have no way of knowing that.

To be honest, we Scripting Guys don’t know if dogs are getting their fair share of scripts that can open an Excel spreadsheet and compare a list of names found in one column with a list of names found in another column, either. Because of that we’d appreciate it if everyone could pass today’s column on to Fido as soon as you’re done with it:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Set objRange = objWorksheet.Range("B1").EntireColumn
i = 1

Do Until objExcel.Cells(i, 1).Value = ""
    strName = objExcel.Cells(i, 1).Value
    Set objSearch = objRange.Find(strName)

    If objSearch Is Nothing Then
        Wscript.Echo strName & " was not found."
    Else
        Wscript.Echo strName & " was found."
    End If

    i = i + 1
Loop

Before we begin we’re assuming that you – or your dog – have a spreadsheet that looks something like this:

Our goal is to take a look at each name in column A and check to see if that name also appears in column B. How are we going to do that? Let’s find out.

Um, just as soon as “we” finish barking at that squirrel.

OK. As you can see, we start off in pretty straightforward fashion, creating an instance of the Excel.Application object and setting the Visible property to True; that gives us a running instance of Excel that we can see on screen. We then use these two lines of code to open the file C:\Scripts\Test.xls and bind to the first worksheet in that file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

With Excel up and running we then use this line of code to create a Range object that encompasses all the cells in column B:

Set objRange = objWorksheet.Range("B1").EntireColumn

Why do we do that? Because in Excel, you need to specify a range before you can do a search; your search then does its magic only within that set of cells. Because we’re only interested in names that appear in column B, we limit our search range to, well, column B.

After defining the search range we then assign the value 1 to a counter variable named i. That brings us to this line of code:

Do Until objExcel.Cells(i, 1).Value = ""

Here we’re setting up a loop that enables us to methodically check each and every value in column A. We begin the loop by looking at the value of cell 1, 1 (remember, we assigned the counter variable i the value 1). We’ll then continue looping until we encounter a blank cell in column 1.

Inside our loop we start off by grabbing the value of the first cell in column 1 and assigning it to a variable named strName. We then use this line of code to search the search range for the first instance of that name:

Set objSearch = objRange.Find(strName)

As you can see, we simply call the Find method, passing that method the name taken from column 1. If the name is found we’ll get back an object reference to the cell where the name was encountered; if the name can’t be found then, needless to say, we won’t get back an object reference. That means we can use this block of code to see whether or not we got back a valid object reference:

If objSearch Is Nothing Then
    Wscript.Echo strName & " was not found."
Else
    Wscript.Echo strName & " was found."
End If

Either way we then echo back the name taken from column 1, along with a message indicating whether or not that name also appears in column 2.

That takes care of the first cell in column 1. We increment the value of i by 1 (so that we’ll be working with the second row of data) and then loop around and repeat the process all over again. When we’re all done we should get back something that looks like this:

Jesper Aaberg was not found.
Jonathan Haas was found.
Ken Myer was found.
Lene Aalling was found.
Pilar Ackerman was found.
Syed Abbas was not found.

There you go, GS: we hope both you and your dog will find this script useful, and entertaining. You might find this of interest, too: for a few dollars more you can record a personalized message onto the DVD you buy for Spot. To quote from The Dog Tales Web site:

“Comfort your dog. Praise your dog. Even remind your dog to stay away from your favorite shoes.”

All kidding aside, this DVD is probably a pretty good thing for dogs; no doubt it beats doing … well, whatever it is dogs do all day when no one is home. Besides, if a personalized DVD could get Rover to make dinner or mow the lawn while he was out even the Scripting Guy who writes this column might buy one.

Interesting Tidbit. This holiday seasons Americans will spend an estimated $5 billion on Christmas presents for their pets. And those cheapskate pets probably won’t spend even half that much on us humans!


How Can I Configure Excel to Autosave Every 5 Minutes?

Hey, Scripting Guy! How can I configure Excel to autosave every 5 minutes?

-- KK

Hey, KK. You know, that’s a good question. As it turns out, the autosave capability in Microsoft Excel – hey, wait a minute: do you realize what day it is? Today is the first day of the 2007 Winter Scripting Games; why in the world are you reading the Hey, Scripting Guy! column?!? After all, even as we speak you should be writing a script that calculates the Chinese astrological year or unscrambling scrambled scripting terms. Quit wasting your time and get going on the Games!

Um, not that Hey, Scripting Guy! is a waste of time, mind you; it’s just that the Scripting Games are special: after all, they represent the opportunity for you to match your scripting skills with script writers from all around the world. That’s an opportunity that only comes once a year, which means you don’t want to miss it.

Besides, you aren’t going to win a Dr. Scripto bobblehead by reading this column, that’s for sure.

On the other hand, KK, you’re probably one of those people who have already finished every event in every division. Last year, during the inaugural Scripting Games, we worried about whether we had made the events too hard and whether we gave people enough time to write their scripts. Those concerns were answered soon enough: just a few hours into day 1 of the 2006 Games we’d already received 300 completed (and correct) entries. And no doubt many of you wrote those scripts with one hand tied behind your backs to boot.

Show-offs!

So what the heck: if you have time to compete in the 2007 Scripting Games and to configure Excel to autosave every 5 minutes, well, we’ll do what we can to help you out:

Set objExcel = CreateObject("Excel.Application")

objExcel.AutoRecover.Enabled = True
objExcel.AutoRecover.Time = 5

As you can see, this is a simple-enough little script, even for the Scripting Guys (who really did think that the events in last year’s Scripting Games were challenging). We start out by creating an instance of the Excel.Application object. In this script, however, we don’t bother setting the Visible property to True. Why not? Because configuring the autosave time is going to take only a split second; it just seemed silly to show Excel onscreen and then immediately make it disappear. But if you’d like to see Excel anyway, just so you know that something is happening, then make this the second line of code in the script:

objExcel.Visible = true

After creating our instance of Excel we then configure two different property values. To begin with, we set the value of the AutoRecover.Enabled property to True; that simply makes sure that autosave is actually enabled. What if you want to disable autosave? You got it: just set Autorecover.Enabled to False:

objExcel.AutoRecover.Enabled = False

Next we set the value of the AutoRecover.Time property to 5 which, needless to say, sets the autosave interval to 5 minutes. Want to autosave every 20 minutes? No problem; just use this code instead:

objExcel.AutoRecover.Time = 20

And that’s all we have to do; we don’t even have to call the Quit method and explicitly dismiss Excel. That’s because we didn’t add a workbook to our instance of Excel; in a case like that, when you don’t add a workbook, Excel automatically terminates at the same time the script terminates.

Incidentally, if you’d like to verify that your script worked you can use this code to retrieve the values of the Enabled, Time, and AutoRecover.Path properties:

Set objExcel = CreateObject("Excel.Application")

Wscript.Echo "AutoRecover enabled: " & objExcel.AutoRecover.Enabled
Wscript.Echo "AutoRecover interval: " & objExcel.AutoRecover.Time
Wscript.Echo "AutoRecover path: " & objExcel.AutoRecover.Path

In turn, that returns information similar to this:

AutoRecover enabled: True
AutoRecover interval: 10
AutoRecover path: C:\Documents and Settings\kenmyer\Application Data\Microsoft\Excel\

We hope that helps, KK. And we hope to see all of you take part in the Scripting Games: the Games are fun, they’re challenging, and there are some cool prizes to be won. The truth is, there’s really no reason not to take part.

Well, unless, of course, you’re too much of a chicken.

Note. The Scripting Editor has just informed us that we can’t call our readers chickens; please accept our apologies. Instead, today’s concluding sentence should have read as follows:

Well, unless, of course, you’re too much of a scaredy-cat.

Thank you. And good luck in the Games!


How Can I Convert a Number to a Date in Excel?

Hey, Scripting Guy! How can I convert a number in Excel to a date; for example, how can I convert 40806 to 4/8/2006?

-- JM

Hey, JM. You probably didn’t know this, but one of the Scripting Guys has a brother who’s an actor. As every actor knows, nepotism is pretty prevalent in the acting world. Well, it’s a good thing the Scripting Guys aren’t actors, because we’d never stoop to such favoritism. (There are other reasons it’s a good thing that the Scripting Guys aren’t actors, but that’s another story.) We randomly select our questions to answer from the hundreds we receive every day. So, little brother, uh, JM, what was it you wanted to know? Oh yes, numbers-to-dates in Excel.

From your email, it sounds like you have a spreadsheet with a column of numbers where the first one or two digits (depending on the month) represent the month, the next two (always two) digits represent the day, and the last two or four (either 06 or 2006) represent the year. Something like this:

Our first thought was to simply use the FormatDateTime method. This method accepts a string and outputs that string in date format:

strNew = FormatDateTime(strOld)

The problem we ran into is that the first date in our table, 40806, became 9/20/2011. Why? Well, the … interesting … thing about Excel is that when you try to turn a number into a date the program assumes that the number is a serial number representing the number of days that have occurred since Jan. 1, 1900. Turns out that 40806 days after Jan. 1, 1900, is Sept. 20, 2011. Back to the drawing board.

In the end, we decided to treat the number like a string: use the VBScript functions Left, Right, and Mid to take the string apart, and then put everything back together with the appropriate symbols that will convince Excel we want a real date, not a serial number. Here’s what we came up with:

Set objExcel =
CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\scripts\test.xls")

intRow = 1
intCol = 1
i = 0

Do Until objExcel.Cells(intRow,intCol).Value = ""
strDate = objExcel.Cells(intRow, intCol).Value
i = Len(strDate)
If i > 6 Then
dtYear = Right(strDate, 4)
i = i - 5
Else
dtYear = Right(strDate, 2)
i = i - 3
End If
dtDay = Mid(strDate, i, 2)
i = i - 1
dtMonth = Left(strDate, i)
newDate = dtMonth & "/" & dtDay &
"/" & dtYear

objExcel.Cells(intRow, intCol).Value = newDate
intRow = intRow + 1
Loop

objExcel.Visible = True

The first thing we do is create an Excel object and open our spreadsheet. We then set up a Do loop to loop through the entire column of dates. The loop will continue until we reach an empty cell in the column; if you don’t have a date in every column, you’ll have to modify this script.

Next, we read in the date from the first cell, which happens to be row 1, column 1:

strDate = objExcel.Cells(intRow, intCol).Value

We then use the Len function to retrieve the number of characters in our date string. Because our string won’t always be the same length we’ll use this number as we read through the string to keep track of how many characters we have left.

In reading our string, we start from the end of the string. Given the format of the dates, the longest a string with a two-digit year can be is six characters, while the shortest a string can be with a four-digit year is seven characters. So we check to see whether the string is greater than six characters long; if it is, that must mean this particular value uses four digits for the year. Consequently, we use the Right function to read the last four characters and save them as the year. If the string is six characters or less, that can only mean one thing: it uses two digits to indicate the year. Therefore, we read in only the last two characters and save those two characters as the year:

If i > 6 Then
    dtYear = Right(strDate, 4)
    i = i - 5
Else
    dtYear = Right(strDate, 2)
    i = i - 3
End If

As you can see, we also subtract the length of the date plus one from our remaining string length. We use that number as we continue to work backwards through the string to read the day (and yes, these are U.S. settings):

dtDay = Mid(strDate, i, 2)
i = i - 1

Here, we’re using the Mid function to say that we want to retrieve two characters starting at position i, which, as we said, is the length of the string minus the length of the year, minus one more because we’re retrieving two characters. In other words, our first string is five characters, so the value of i is 5. After reading the year, the value of i is 5 – 2 – 1, or 2. Starting at position 2, we take two characters, two characters which just happen to represent the day. We then subtract 1, and use that position to retrieve the month:

dtMonth = Left(strDate, i)

Here we use the Left function to retrieve the remaining number of characters (in this case, one) and save them as the month. Then we simply put the string together, separating the parts of the date with slashes (/):

newDate = dtMonth & "/" & dtDay & "/" & dtYear

After that we put the new string into the cell we read the date from, increment the row number, and loop back to start on the next row in the column. The last thing we do is make Excel visible so you can see the results.

So that’s it. Say hi to Mom for us. Um, say hi to your Mom for us ….


How Can I Convert a Tilde-Delimited File to Microsoft Excel Format?

Hey, Scripting Guy! How can I convert a tilde-delimited file to Microsoft Excel format?

-- JN

Hey, JN. You know, to tell you the truth we’re beginning to rethink our approach to this column. After all, usually we tell people how easy things are, and how – without much effort – they can write a script that will let them accomplish some seemingly-impossible task. However, yesterday the Scripting Son was flipping through the channels when he stumbled upon an infomercial for a knife – wait, did we say “knife?” This was no knife; in fact, it doesn’t even cut food, it “sonically separates food.” We’d like to see a knife do that.

Anyway, this actually looked like a pretty good product; however, what we thought was really interesting was the commercial itself. “You know how hard it can be to cut a sandwich in two?” asked the narrator. Now, to be honest, cutting a sandwich was always one of the few things the Scripting Guy who writes this column thought he was able to do. But it turns out he was just kidding himself: as the video plainly showed, it’s impossible to cut a sandwich with a regular knife.

OK, granted, this sandwich was about a foot high; even if you could cut it with a knife we have no idea how you’d go about eating it. Regardless, as the poor guy on the commercial tried cutting his foot-high sandwich everything went wrong: the sandwich fell apart, the tomatoes squirted out on to the floor, it was excruciating to watch a perfectly-good sandwich get destroyed like that. “Don’t feel bad,” said the narrator. “This has happened to all of us.” They then showed how, although you can’t cut a foot-high sandwich, you can sonically separate a foot-high sandwich. Hallelujah! Finally the Scripting Guy who writes this column can do something with all those foot-high sandwiches that have been piling up in his kitchen.

“And best of all: no more tired muscles from cutting food,” added the narrator. So there you go: if you’ve been painstakingly cutting your toast every morning then feeling worn-out and tired the rest of the day, we have good news for you: relief is at hand.

So what does any of that have to do with scripting? Well, it got us thinking. After all, if you’re like most people, you probably have hundreds of tilde-delimited files like this lying around in your computer:

Ken Myer~Fiscal Specialist~Accounting
Pilar Ackerman~Customer Service Representative~Accounting
Jonathan Haas~Manager~Accounting

No doubt you’ve tried to open these files using Microsoft Excel. But what happens when you do that? Disaster! Instead of seeing the tilde character (~) as a field separator, Excel simply throws all the data into column A:

Factor in the sore muscles you get from trying to open a tilde-delimited file and your entire day has been completely ruined. Worst of all, there’s absolutely nothing you can do about that! Your tilde-delimited files are useless, your muscles are sore and tired, and you’re doomed to live a life of squalor and despair.

Note. Welcome to the Scripting Guys world.

Or are you? Listen, don’t throw away those tilde-delimited files; instead, use the Scripting Guys patented Sonic Data Separation Technology to convert these tilde-delimited files into something that Excel can open properly:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForReading)
strContents = objFile.ReadAll
objFile.Close

strContents = Replace(strContents,
"~", vbTab)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForWriting)
objFile.Write strContents
objFile.Close

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.txt")

No doubt you’re saying, “Sonic Data Separation Technology? How does that work?” Let’s see if we can explain. Because JN specifically wanted to convert the tilde-delimited files, we need to get rid of the tildes and replace them with something Excel does see as a delimiter (namely, the Tab character). Here’s how we do that.

We start off by defining a pair of constants (ForReading and ForWriting) needed when opening the tilde-delimited text file (C:\Scripts\Test.txt). With the constants in tow we then use this line of code to create an instance of the Scripting.FileSystemObject:

Set objFSO = CreateObject("Scripting.FileSystemObject")

Once we have an instance of the FileSystemObejct we can then use this block of code to open the text file, read the entire contents into a variable named strContents, and then close the file:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)
strContents = objFile.ReadAll
objFile.Close

What’s the point of all that? Well, we can’t actually manipulate the text file directly; the FileSystemObject doesn’t allow for that. Therefore we need to open the text file, store a copy of the contents in memory, then manipulate that virtual copy of the file. After we’ve made our changes (that is, after we replace all the tilde characters with Tab characters) we’ll then reopen the file and write the modified contents back to Test.txt.

So let’s go ahead and do that right now. To replace the tilde characters we use the VBScript Replace function, replacing all instances of ~ with a tab (represented by the VBScript constant vbTab):

strContents = Replace(strContents, "~", vbTab)

That changes our virtual copy of the text file. We then reopen the actual file itself (this time for writing), use the Write method to replace the contents of the file, then close Test.txt:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strContents
objFile.Close

Believe it or not, it’s that simple: the patented Sonic Data Separation Technology has now converted Test.txt to a format Excel can open, and open properly. Hard to believe? We understand. That’s why we added a block of code to the end of the script that creates an instance of Microsoft Excel, makes that instance visible, and then opens the file C:\Scripts\Test.txt:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.txt")

Here’s what shows up on screen:

Is this the greatest thing that has ever happened in the history of the human race? Maybe not. But we can’t think of anything better.

You seem a little skeptical. “That is pretty cool, Scripting Guys. But what if I don’t want to convert my text file? What if I just want to open the file, leaving the tildes in place? I guess I’m out of luck, aren’t I?”

Out of luck? Au contraire: this is your lucky day. That’s because, at no extra charge, we’ve included our patented Sonic Data Import Filter attachment, enabling you to import tilde-delimited files into Excel, and without having to modify that file in any way! We don’t have time today to discuss how this all works, but simply attach the Data Import Filter and import away:

Const xlDelimited  = 1

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

objExcel.Workbooks.OpenText "C:\Scripts\Test.txt",,,xlDelimited,,,,,,,True,"~"

Yes, it’s a crazy looking little script, or at least the last line is. But that’s because the OpenText method includes a number of parameters that are irrelevant for our purposes. We don’t want to use them; however, we do have to leave an empty placeholder for each one. Consequently, we end up with a bunch of blank parameters. The important thing here is that we tell Excel that we are working with a delimited text file (xlDelimited); that we’re using a non-standard delimiter (True); and that non-standard delimiter happens to be the tilde character (~).

Did we mention that the Scripting Guys Sonic Data Separator is available now for just $29.95? Call today and we’ll throw in the Sonic Data Import Filter attachment absolutely free! This is a limited-time offer, so don’t delay: operators are standing by.

Note. The Scripting Guys Sonic Data Separator is not available in stores. For good reason.


How Can I Convert an Excel Spreadsheet to XML?

Hey, Scripting Guy! How can I convert an Excel spreadsheet to XML?

-- KE

Hey, KE. We apologize in advance if today’s column seems a little rushed; at the moment, we’re sitting in an office, surrounded by boxes and packing crates and waiting for the movers to come in and clear everything out. And no, the Scripting Guy who writes this column hasn’t been fired, he – just a second, we need to double-check something ….

No, we were right: the Scripting Guy who writes this column hasn’t been fired. Instead, it’s Moving Day, and he and his fellow Scripting Guys are about to move to new offices in a new building. For those of you who like to keep track of this sort of thing, for the Scripting Guy who writes this column that will make, in the past six years, 10 offices in 4 different buildings.

And, sadly, no, that’s not a record for Microsoft. Not even close.

At any rate, the reason we’re feeling a little rushed is because we don’t exactly know when the movers will show up. Adopting the same highly-beloved customer service strategy employed by the TV cable companies, today’s move will start promptly at noon. Unless, of course, it starts later. Or maybe even earlier; after all, we were required to have everything packed up a day in advance. If today’s column suddenly breaks off in the middle of a sentence, well, then you’ll know the movers came a little earlier than expected.

And because we may (or may not) be pressed for time today we decided to pick an easy question to answer. How easy? This easy:

Const xlXMLSpreadsheet = 46

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

k = 1

For i = 1 to 5
    For j = 1 to 3
        objWorksheet.Cells(i,j) = k
        k = k + 1
    Next
Next

objWorkbook.SaveAs "C:\Scripts\Test.xml", xlXMLSpreadsheet

Sure, we can explain how this all works; we have lots of time. (Unless we don’t.) As you can see, we start out by defining a constant named xlXMLSpreadsheet and setting the value to 46; we’ll use this constant to tell Excel that we want to save the spreadsheet as XML. After defining the constant we create an instance of the Excel.Application object and then set the Visible property to True; that gives us a running instance of Excel that we can see onscreen.

Of course, it’s no fun to have a running instance of Excel unless we actually do something with that instance. Consequently, we use the following two lines of code to add a new workbook and a new worksheet to our running instance of Excel:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

We then use this block of code to add some (in this case, meaningless) data to the worksheet:

k = 1

For i = 1 to 5
    For j = 1 to 3
        objWorksheet.Cells(i,j) = k
        k = k + 1
    Next
Next

Now we have an Excel spreadsheet filled with all sorts of interesting data. That’s nice, but how do we save that data as XML? Believe it or not, that takes only a single line of code:

objWorkbook.SaveAs "C:\Scripts\Test.xml", xlXMLSpreadsheet

As you can see, all we’re doing here is calling the SaveAs method and passing the method a pair of parameters: the complete path to our new XML file (C:\Scripts\Test.xml) and the constant xlXMLSpreadsheet. Will that really give us a file that can be opened not only by Excel but also by Notepad or by an XML editor? Of course it will; here’s an excerpt from the data section of the file we created:

<Worksheet ss:Name="Sheet1">
  <Table ss:ExpandedColumnCount="3" ss:ExpandedRowCount="5" x:FullColumns="1"
   x:FullRows="1">
   <Row>
    <Cell><Data ss:Type="Number">1</Data></Cell>
    <Cell><Data ss:Type="Number">2</Data></Cell>
    <Cell><Data ss:Type="Number">3</Data></Cell>
   </Row>
   <Row>
    <Cell><Data ss:Type="Number">4</Data></Cell>
    <Cell><Data ss:Type="Number">5</Data></Cell>
    <Cell><Data ss:Type="Number">6</Data></Cell>
   </Row>

Yes, we know; what you’re really curious about is this: after years of working out of tiny, windowless offices is the Scripting Guy who writes this column finally going to get the working environment he deserves? Let’s put it this way: the last thing the Scripting Guy who writes this column wants to get is what he deserves. If you know what we mean.


How Can I Copy a Worksheet to a New Spreadsheet?

Hey, Scripting Guy! How can I copy a worksheet in an existing Excel spreadsheet to a new spreadsheet?

-- TY

Hey, TY. Before we tackle your question we wanted to note that, as we write this, the University of Washington football team has a winning record. Sure, we’re only three weeks into the college football season, and the UW’s record is only 2-1, but the way the last two years have gone (3 wins and – it hurts to type this – 19 losses) a 2-1 record and a 21-20 victory over Fresno State is definitely something worth mentioning. So we’re mentioning it.

And, let’s face it: if we’re going to talk about having a winning record we need to do it now, without waiting. Not that we lack faith in the Huskies or anything, but, still ....

By the way, have we mentioned that basketball season is just two months away, and that the UW has been to the men's Sweet 16 each of the past two seasons? That's good; after all, mentioning something like that would sound like bragging, and the Scripting Guys never brag.

Note. OK, granted, that’s because the Scripting Guys never actually have anything to brag about. But no one has to know that.

If you’re a college football fan (and you should be), then you know that winning football games is tough; the Huskies, for example, had to block a extra-point try in the closing minutes in order to beat Fresno State. Fortunately, copying a worksheet from an existing Excel spreadsheet to a brand-new spreadsheet is far easier.

Anyway, the important thing is that the Huskies finally started to – what’s that? Show you how to copy a worksheet from an existing Excel spreadsheet to a brand-new spreadsheet? Well, we’re not sure what that has to do with college football, but, hey, why not:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.Activate

objWorksheet.Copy()

See, we told you it was easy. The script starts out by creating an instance of the Excel.Application object, and then sets the Visible property to True; that gives us a running instance of Excel that we can see onscreen. With Excel up and running we then use the Workbooks.Open method to open the file C:\Scripts\Test.xls. With the spreadsheet open we use these two lines of code to create an object reference to Sheet1 (the worksheet we want to copy), then use the Activate method to make Sheet1 the active worksheet:

Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.Activate

Believe it or not, all we need to do now is add one more line of code and we're done. (Hey, it's Monday; surely you weren't expecting us to write a long and complicated script on a Monday, were you?) After we’ve connected to Sheet1 and made this the active worksheet all we have to do is call the Copy method:

objWorksheet.Copy()

That’s it: any time you call the Copy method without any parameters the worksheet will be copied and pasted into a brand-new spreadsheet. You'll now have two spreadsheets up and running: Test.xls, and a new, untitled spreadsheet that includes an exact copy of Sheet1, including formatting, formulas, and anything else you can cram into an Excel worksheet.

As long as we’re on the subject, you might find it useful to know that this new spreadsheet is actually opened up as the second workbook in your running instance of Excel. That means it’s possible to use your script to control this new workbook. For example, these two lines of code first create an object reference name objNewWorkbook that binds to the new spreadsheet, then use the SaveAs method to save thie file as C:\Scripts\New_Workbook.xls:

Set objNewWorkbook = objExcel.Workbooks(2)
objNewWorkbook.SaveAs("c:\scripts\new_workbook.xls")

That could come in handy.

In case you’re wondering, TY, it’s only the Scripting Guy who writes this column who is a rabid follower of the University of Washington; in fact, he’s also the only Scripting Guy who attended the UW. But does that somehow make him better than his colleagues, colleagues who attended such diverse universities as Dalhousie (Dean), Western Washington (Jean), and the University of Northern Jupiter (Peter)? Well, now that you mention it ….


How Can I Copy All the Comments From an Excel Worksheet to a Word Document?

Hey, Scripting Guy! How can I copy all the comments from an Excel worksheet to a Word document?

-- ED

Hey, ED. You know, one of the drawbacks to calling yourself a “Scripting Guy” is that people assume you’re really more of a Scripting Fanatic. Every now and then we get emails that say, “Hey, Scripting Guy! I read your column on X. Well, I do X all the time, and I can do it faster and easier by using the GUI/a command line tool/magic. What do you have to say about that?”

Well, to tell you the truth, this is what have to say about that: “Good for you.” Believe it or not, we don’t think you should script everything you do; in fact, depending on what it is you do, maybe you shouldn’t even use scripts at all. If you find that the GUI or a command-line tool (or, yes, magic) works better for you, well, then that’s probably the approach you should take. Do whatever works best for you.

Of course, there will be times when there is no easy way to carry out a task using the GUI or a command-line tool. (We’re not sure about magic; that falls into kind of a gray area.) Those are the times when scripting can be a real life-saver. And, by amazing coincidence, the chore of copying all the comments from an Excel worksheet to a Word document falls into that very category. Short of methodically copying and pasting each individual comment is there any way to accomplish this task using the GUI? Not as far as we know. And don’t even ask about the command-line; needless to say, Microsoft Office doesn’t exactly ship with a boatload of command-line tools.

Note. In all fairness, we should point out that magic did work in this case; however, the spell we cast had the unfortunate effect of turning Peter into a chicken. To put a new spin on an old joke, we’d turn him back, except we need the eggs.

With a script, however, copying all the comments from an Excel worksheet to a Word document is no more difficult than this:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

Set colComments = objWorksheet.Comments

For Each objComment in colComments
    strComments = strComments & objComment.Text & vbCrlf & vbCrLf
Next

objExcel.Quit

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.TypeText strComments

Note. In case you’re wondering, it took us just two minutes to create this script. Granted, we had an advantage because we copied some boilerplate code; in addition we also had a pretty good idea of how to carry out the task before we started. Still, you can see that scripting doesn’t have to be a vast, time-consuming enterprise. After all, we were able to toss this script together in no time, even though Peter ate our first draft.

So how does the script work? Well, we begin by creating an instance of the Excel.Application object and then set the Visible property to True. The latter is actually optional: the script works just fine if Excel is never visible on screen. We like to make the application visible simply so you can see what’s going on. (Of course, if you have only a handful of comments in the spreadsheet then Excel will flash onto and off of the screen so quickly you might miss it anyway.)

After creating an instance of Excel we then use these three lines of code to open the spreadsheet C:\Scripts\Test.xls and make Sheet1 (the worksheet where our comments reside) the active worksheet:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

All of that is a prelude to this line of code:

Set colComments = objWorksheet.Comments

As it turns out, all the comments for a worksheet are stored in the Comments collection. To retrieve this collection all we have to do is create an object reference (here named colComments) to the worksheet’s Comments property. It’s that easy.

Of course, that poses a new problem: what do we do with those comments once we have them? Turns out that this is no problem at all: because the comments are stored in a collection, we can set up a For Each loop to walk through all the items in that collection:

For Each objComment in colComments
    strComments = strComments & objComment.Text & vbCrlf & vbCrLf
Next

As you can see, we don’t do anything particularly complicated within that For Each loop. In fact, all we do is assign a value to a variable named strComments. The first time we run through the loop strComments gets assigned the existing value of strComments (which is nothing, seeing as how we haven’t assigned a value to it yet) plus the Text of the first comment in the collection plus a pair of carriage return-linefeeds (vbCrLf). That’s easy enough, right?

Now, what happens the second time through the loop? Pretty much the same thing: strComments gets assigned the existing value of strComments (which, the second time around, will be the Text of comment 1) plus the Text of the second comment in the collection plus a pair of carriage return-linefeeds (vbCrLf). By the time we finish with the loop the Text of all of the comments will be stashed in the variable strComments.

In other words, without resorting to copying, pasting, or magic spells we can easily grab all the comments from the worksheet.

After we’ve grabbed the comments we have no further use for Excel, so we simply call the Quit method to dismiss the application.

Now we turn our attention to Microsoft Word. To begin with, we create an instance of the Word.Application object and set the Visible property to True. We then use the Add method to create a new, blank document to work with.

Note. We know: if you’ve never written scripts that interact with Microsoft Office applications much of this column likely sounds like gibberish. (Sadly, even if you have written scripts that interact with Microsoft Office applications this column occasionally sounds like gibberish. But that’s a different story.) If you’re new to Microsoft Office scripting you might find it useful to take a peek at the articles in our Office Space archive.

As soon as we have a blank document we create an instance of Word’s Selection object, then use this one line of code to write the extracted comments into the Word document:

objSelection.TypeText strComments

Is this really that easy? Give the script a try and see for yourself.

Oh: and if anyone needs eggs, please let us know. We always considered Peter a so-so Scripting Guy, but we have to admit, he’s one heckuva chicken!


How Can I Copy and Rename a Worksheet in the Same Workbook?

Hey, Scripting Guy! I’d like to copy one of the worksheets in an Excel workbook and then paste that copy into the very same workbook; once it’s pasted it, I’d like to rename it. Can I do that?

-- JG

Hey, JG. Of course you can do this; after all, if you can do something in Excel it’s almost 100% guaranteed that you can do it using a script. This is actually pretty easy, with one little catch we’ll talk about right away.

To begin with, it’s very easy to copy a worksheet in Excel; all you have to do is bind to the worksheet and call the Copy method. For example, this script will copy Sheet1 in a workbook named C:\Scripts\Testsheet.xls:

Set
objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Testsheet.xls")
objExcel.Visible = TRUE

Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.Copy

As you can see, there’s nothing too fancy here. We create an instance of the Excel.Application object and then use the Open method to open the workbook C:\Scripts\Testsheet.xls. We set the Visible property to TRUE (just so we can watch the drama unfold) and create an object reference to Sheet1, the worksheet to be copied:

Set objWorksheet = objWorkbook.Worksheets("Sheet1")

We then call the Copy method to create a copy of the worksheet.

This is the tricky part. The preceding script works, but there’s one problem: by default, the Copy method copies the worksheet into a brand-new workbook. We’ll end up with two copies of Sheet1 alright, but one copy will be in Testsheet.xls and the other copy will be in a new workbook. Not what you had in mind. (If you aren’t sure what we’re talking about, give the script a try and you’ll see what we mean.)

So how do we fix this? Well, what have to do is specify an insertion point for the new worksheet. One way to do that is to indicate which sheet the copied worksheet should be inserted after. To do that, we need to create a second object reference. For example, if we want to insert the new worksheet after Sheet3, we need to create an object reference to Sheet3. Thus:

Set objLastSheet = objWorkbook.Worksheets("Sheet3")

When we have this second object reference we then pass it as a parameter to the Copy method. This line of code copies Sheet1 and tells Excel to insert the copied worksheet after Sheet3:

Set
objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\testsheet.xls")
objExcel.Visible = TRUE

Set objLastSheet = objWorkbook.Worksheets("Sheet3")
Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.Copy, objLastSheet

Note what happens when we do this; we get a copy of Sheet1 -- named Sheet1 (2) -- inserted immediately after Sheet3, just the way we wanted:

Having copied and inserted the new worksheet all that’s left is to rename Sheet1 (2). That takes two lines of code: we create an object reference to the new worksheet and then assign it a new name. Or, to put it programmatically:

Set objWorksheet = objWorkbook.Worksheets("Sheet1 (2)")
objWorksheet.Name = "My New Worksheet"

Like we said, once we got past the tricky part, this was almost too easy. Here’s a completed script that carries out both tasks for you: it copies Sheet1, and then renames it to My New Worksheet:

Set
objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\testsheet.xls")
objExcel.Visible = TRUE

Set objLastSheet = objWorkbook.Worksheets("Sheet3")
Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.Copy, objLastSheet

Set objWorksheet = objWorkbook.Worksheets("Sheet1 (2)")
objWorksheet.Name = "My New Worksheet"

If you’re looking for more tips and tricks for scripting Microsoft Office check out Office Space, published every Tuesday and Thursday in the Script Center.


How Can I Copy Column C of One Worksheet to Column A of a Second Worksheet?

Hey, Scripting Guy! How can I copy column C of one worksheet to column A of a second worksheet?

-- JP

Hey, JP. Well, that’s easy: you just open Excel, highlight column C, press Ctrl+C, put the cursor in column A1 of the second worksheet, and then press Ctrl+V. There you go, and thanks for your question.

Oh, right: you wanted to know how to do this using a script. (What were we thinking?) But that’s OK; it’s just as easy to do this using a script as it is to do it manually:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

Set objWorksheet = objWorkbook.Worksheets(2)
objWorksheet.Activate

Set objRange = objWorkSheet.Range("C1").EntireColumn
objRange.Copy

Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

Set objRange = objWorkSheet.Range("A1")
objWorksheet.Paste(objRange)

The script starts out by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us an instance of Excel that we can see on screen. We then use the Open method to open the file C:\Scripts\Test.xls. For this sample script, we’re assuming that this file has at least two worksheets, and that there is data in column C of Sheet 2 that we want to copy.

To do that we first create an object reference to Sheet 2 (the second worksheet in the Worksheets collection), and then call the Activate method to make this the active worksheet. That’s what these two lines of code are for:

Set objWorksheet = objWorkbook.Worksheets(2)
objWorksheet.Activate

To select column C, we create an instance of the Range object, specifying cell C1 as the starting point, then using the EntireColumn property to select the rest of column C. That’s what we do here:

Set objRange = objWorkSheet.Range("C1").EntireColumn

After we’ve selected the column, we then call the Copy method to copy the data to the Clipboard.

Now we need to switch to Sheet 1; we do that by creating an object reference to the first worksheet in the Worksheets collection and then calling the Activate method to make Sheet 1 the current worksheet. We then use this line of code to create a Range object that points to cell A1:

Set objRange = objWorkSheet.Range("A1")

Because we’re pasting data there’s no need to select the entire column; all we have to do is pick the starting cell and then call the Paste method, passing this method the object reference that indicates where we want the data pasted:

objWorksheet.Paste(objRange)

That should do the trick, and in fully automated fashion to boot. Like we always say (sometimes a bit prematurely): there you go, and thanks for your question.


How Can I Copy Data From One Spreadsheet to Another?

Hey, Scripting Guy! How can I copy data from one Excel spreadsheet to another Excel spreadsheet?

-- AC

Hey, AC. You know, we’re going to let you in on a little secret here: the Scripting Guy who writes this column is a tad bit tired this morning. That’s because he arrived home late last night (well after midnight) following a flight from Rome to Seattle. (A flight, in case you’re wondering, that takes just a shade under forever to complete.) And yet, despite being tired from a full day of travel, and despite the effects of jet lag, he still got up at 6:00 this morning and came into work. Why did he come in to work rather than take one more day off? Because he’s a dedicated Microsoft employee who absolutely loves his job, that’s why.

Well, that and the fact that he’s trying to stockpile enough vacation days to enable him to take the entire month of December off. But no one needs to know about that, least of all his manager or anyone he works with.

Actually, today’s article should be an interesting one. After all, the Scripting Guy who writes this column often wanders through the hallways of Microsoft, telling anyone who has the misfortune to run into him, “A script that can copy data from one Excel spreadsheet to another? Why, I could write a script like that in my sleep!”

So can he really write a script like that in his sleep? We’re about to find out:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("c:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets("Sheet1")

Set objRange = objWorksheet.Range("A1:A20")
objRange.Copy

Set objExcel2 = CreateObject("Excel.Application")
objExcel2.Visible = True

Set objWorkbook2 = objExcel2.Workbooks.Add
Set objWorksheet2 = objWorkbook2.Worksheets("Sheet1")

objWorksheet2.Paste

OK, not too bad; now let’s see if he can explain a script that can copy data from one Excel spreadsheet to another in his sleep. To begin with, the script creates an instance of the Excel.Application object and then sets the Visible property to True; that gives us a running instance of Microsoft Excel that we can see onscreen. We next use the Open method to open the file C:\Scripts\Test.xls, then use the following line of code to bind us to the first worksheet in the file:

Set objWorksheet = objWorkbook.Worksheets("Sheet1")

That was pretty easy, wasn’t it?

Let’s see, what’s next? Well, in order to copy data to the Clipboard, we need to first specify the range of data to be copied (that is, the cells we want to copy), then call the Copy method. That’s what these two lines of code are for:

Set objRange = objWorksheet.Range("A1:A20")
objRange.Copy

As you can see, in line one we create an instance of Excel’s Range object, in this case a Range object encompassing the cells A1 through A20 (although, needless to say, this could be any range of data we want). In line 2, we call the Copy method, a command that copies cells A1 through A20 (all the cells in our range) to the Clipboard.

You know what? That was pretty easy, too.

But wait; we’re not done yet. After all, we still have to paste this data into a second spreadsheet. For our sample script, we’re going to paste this information into a brand-new spreadsheet; however, we could also open and paste the data into another pre-existing spreadsheet if we so desired. With that in mind, we thus create a new instance of Microsoft Excel, giving this instance the object reference objExcel2 and setting its Visible property to True:

Set objExcel2 = CreateObject("Excel.Application")
objExcel2.Visible = True

Déjà vu, huh?

After creating our second instance of Excel, we use the Add method to add a new, blank workbook to objExcel2, then use this line of code to bind to the first worksheet in that new workbook:

Set objWorksheet2 = objWorkbook2.Worksheets("Sheet1")

At this point all we need to do is call the Paste method and, by extension, call it a day:

objWorksheet2.Paste

Good observation: Paste is actually a method that belongs to the Worksheet object (unlike the Copy method, which belongs to the Range object). By default, Excel will paste the data into cell A1 (and adjoining cells as needed). What if we want to paste this data somewhere else? Well, the only way to do that is to make a cell other than cell A1 the active cell. For example, here’s a line of code that makes cell D5 the active cell on our worksheet:

objWorksheet2.Range("D5").Activate

Now when we call the Paste method the data will be pasted into cell D5 (and any adjoining cells).

By the way, thanks in advance for not telling anyone how tired the Scripting Guy who writes this column really is. The fact of the matter is that if anyone knew the truth, they’d make him go home and, in turn, use up one of his precious vacation days. As it is, right now the Scripting Guy who writes this column is wandering around in a daze, eyes half-closed, and muttering incoherently to himself. Which, considering the fact that this is pretty standard behavior for him anyway, means that no one suspects a thing.


How Can I Copy Selected Columns From a CSV File to an Excel File?

Hey, Scripting Guy! How can I copy selected columns from a CSV file to an Excel file?

-- MdP

Hey, MdP. One thing that people always worry about (and rightly so) is that one of these days the Scripting Guys will do something that tears a hole in the space-time continuum. Some of you might be concerned that this has finally happened: after all, this is the column for Monday, June 4th, and yet you might have actually read it on Sunday, June 3rd! Has it finally happened? Have the Scripting Guys irrevocably warped time and space?

No, we haven’t. (Well, OK, we might have dented it a little, but it’s nothing that can’t be pounded out.) We published the June 4th column on June 3rd for one simple reason: on June 3rd the Scripting Guys set sail for Orlando and TechEd 2007. Scripting Guy Jean Ross left early on Sunday, June 3rd, but we couldn’t be sure that she would be able to connect to the Microsoft network from her hotel room; therefore, we couldn’t rely on her to publish this column Monday morning. (We also couldn’t be sure that she wouldn’t fall down and crack her head open. But that’s another story.)

Meanwhile, Scripting Guy Greg Stemp couldn’t publish the column on Monday because he opted to watch his son’s baseball game Sunday evening and then take the red-eye special, leaving Seattle around 11:00 PM. His itinerary gets him to Orlando just in time to head to the convention center, and no time to publish Monday’s Hey, Scripting Guy! (And no time to sleep, either, which is a bit of a problem seeing as how he has real trouble sleeping on a plane.)

At any rate, in addition to today’s Hey, Scripting Guy! we had several other important things we wanted to publish, including the first installments of the Windows PowerShell Owner’s Manual as well as the TechEd Challenge, an opportunity for those of you who didn’t get to go to TechEd to win a Dr. Scripto bobblehead doll. And so we published all of this on Sunday. If the discrepancy between the date on the column and the date the column was actually published has driven you insane and you will now be forced to spend the rest of your life in a mental institution, well, we do apologize.

Note. One more thing: if you do end up in the institution, could say hi to Peter for us? Thanks!

In the meantime, we have a question to answer:

Const ForReading = 1

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

objExcel.Workbooks.Add

i = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    arrLine = Split(strLine, ",")

    objExcel.Cells(i, 1).Value = arrLine(0)
    objExcel.Cells(i, 2).Value = arrLine(2)

    i = i + 1
Loop

objFile.Close

Dum-dee-dum-dum …. Sorry; we were hoping that we really had warped the space-time continuum and that, by doing so, we’d have already written the explanation for this script. Unfortunately, though, that didn’t work. Therefore, we’ll just have to figure this script out on our own.

To begin with, we should note that we’re cheating a little bit: we’re not directly importing the text file into Excel. (Well, we’ll double-check, but, no, we don’t believe we can be arrested for that.) It looks like it might be possible to use Excel’s OpenText method to open a text file and specify which columns should be imported and which ones shouldn’t. However, that approach looked a little complicated, especially in light of how easy it is to create a script that opens the text file and then just writes the specified columns to an Excel spreadsheet. And so, as always, the Scripting Guys took the easy way out.

So what exactly do we do here? Well, we begin by defining a constant named ForReading and setting the value to 1; we’ll need this constant when we open our text file. We then use these two lines of code to create an instance of the Excel.Application object and make that instance visible onscreen:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

We then use the Add method to add a new, blank workbook to our instance of Excel; while we’re at it, we also assign the value 1 to a counter variable named i. In a few minutes, we’ll use this counter variable to keep track of the current row in the spreadsheet.

Now it’s time to turn our attention to the text file. For our sample script, we’re assuming we have a comma-separated values file that looks like this:

Ken Myer,Finance,Senior Accountant,New York
Pilar Ackerman,Research and Development,Consultant,Des Moines
Jonathan Haas,Research and Development,Scientist II,Des Moines

As you can see, this sample file has four “columns” (fields):

· Name

· Department

· Job Title

· Location

In our spreadsheet, we want to important just the first and third columns: Name and Job Title. And here’s how we do that.

For starters, we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

We then set up a Do Until loop that runs until the file’s AtEndOfStream property is True; that’s just a fancy way of saying that we want the loop to keep going until we’ve read the entire file. Inside the loop, the first thing we do is use the ReadLine method to read line number 1 in the file and store that value in the variable strLine:

strLine = objFile.ReadLine

The first time through the loop that means that strLine will be equal to this:

Ken Myer,Finance,Senior Accountant,New York

Admittedly, that doesn’t do us all that much good, at least not in its current format. Therefore, our next step is to use the Split function to turn this value into an array of individual values. By splitting strLine on the comma we end up with an array (named arrLines). That array has the following elements:

Ken Myer
Finance
Senior Accountant
New York

Believe it or not, this does help us, immensely. We want to write the Name and Job Title to our spreadsheet. And what do you know: the Name just happens to be item 0 in our array. (Remember, the first item in a VBScript array is always item 0.) As if that wasn’t exciting enough, the Job Title happens to be item 2 in the array. (If the first item is item 0, then the second item is item 1, and the third item – Job Title – is item 2.) Cool; all we have to do now is write the values of array items 0 and 2 to the appropriate spots in the spreadsheet:

objExcel.Cells(i, 1).Value = arrLine(0)
objExcel.Cells(i, 2).Value
= arrLine(2)

In the first line we’re setting the value of cell row 1, column 1 (i.e., cell A1) to Ken Myer, the user Name. How do we know that this is cell 1, 1? That’s easy: because we assigned the value 1 to the counter variable i. In the following line, we then set the value of cell row 1, column 2 (B1) to Senior Accountant. From there we increment the value of i by 1 (so that, on the next iteration, we’ll be writing data to cells in row 2), then loop around and repeat the process with the next line in the text file. It might not be fancy, but it works:

At any rate, if you happen to be at TechEd please stop by the CMP Media booth (booth number 1301 in the Partner Expo Hall) and say hi. And while you’re there, don’t forget to pick up your copy of Dr. Scripto’s Fun Book. And to enter the drawing for a Dr. Scripto bobblehead doll.

Incidentally, if you have problems finding the CMP Media booth just listen for the sound of someone snoring; that will likely be Scripting Guy Greg Stemp. And no, he won’t be snoring because he had problems sleeping on the plane; he’ll be snoring because Scripting Guy Jean Ross is telling yet another story about her Scripting Dog. Either way, though, they should be easy to find. (Look for the giant Dr. Scripto; that’s usually a good sign that there are Scripting Guys nearby.)


How Can I Create a Custom Date Format in Microsoft Excel?

Hey, Scripting Guy! In Microsoft Excel, how can I display a date like 01/20/2006 using this format: 20060120?

-- CM

Hey, CM. Who do we make the check out to again? And for how much?

Oh, sorry. After attending a parent meeting for the Scripting Son’s new baseball team the Scripting Guy who writes this column naturaly assumes that anyone who asks him a question is asking for money. You need $1,800 to cover the Scripting Son’s expenses for winter workouts and for the season? OK. The Scripting Family is expected to buy 6 tickets to the annual fund-raising auction? Uh, all right. The coach has decided to play in a wood bat league, meaning that the Scripting Son will need another bat on top of the one he got just a few months ago? Sure; why not?

Anyway, just let us know how much we owe you, CM; we didn’t bring the checkbook to work today. In the meantime, here's a script that can apply a custom date format to a cell in a Microsoft Excel spreadsheet:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

objExcel.Workbooks.Add

objExcel.Cells(1,1).Value = "01/01/2006"
objExcel.Cells(1,1).NumberFormat = "yyyymmdd"

As you can see, applying a custom date format is as easy as spending money. We start out by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Microsoft Excel that we can see on screen. After we have Excel up and running we then use the Add method to add a new workbook. And once we have a workbook, we're ready to get down to business.

To begin with, we need to put the value 01/01/2006 into the spreadsheet. This line of code does just that, setting the Value of cell A1 (row 1, column 1) to 01/01/2006:

objExcel.Cells(1,1).Value = "01/01/2006"

That’s going to give us a spreadsheet that looks like this, with Excel applying its default date-time format to cell A1:

Of course, we don’t want the default date-time format; we want to use a custom format, one that displays the year using four digits, the month using two digits, and the day using two more digits. How much is that going to cost us? For once, at least, absolutely nothing:

objExcel.Cells(1,1).NumberFormat = "yyyymmdd"

Turns out that there's nothing to this: we simply configure the cell’s NumberFormat property, assigning it a value consisting of four y’s (representing the four digits in the year); two m’s (representing the digits in the month); and two d’s (representing the two digits in the day). That’s all we have to do; like magic, the value in cell A1 will now be formatted like this:

We should probably point out that you can create pretty much any custom date-time format you want. For example, suppose you'd like to display the date like this:

01.01.2006

Okey-doke:

objExcel.Cells(1,1).NumberFormat = "mm.dd.yyyy"

For more information on formatting cells, open up Excel, click Format, click Cells, and then, on the Number tab of the Format Cells dialog box click on the Custom category. Any of the formatting symbols you can see in the dialog box can be applied to the NumberFormat property using a script.

At any rate, consider that a down payment, CM and don’t worry: we’ll get the balance to you as quick as we can. How do you spell CM again?


How Can I Create a New Excel Spreadsheet at Midnight Each Night?

Hey, Scripting Guy! How can I create a new Excel spreadsheet at midnight each night? I’d like each spreadsheet to have a file name similar to this: 6-1-06.xls.

-- DH

Hey, DH. You know, often-times the simplest approach is also the best approach. For example, the other night the Scripting Son was having a frustrating baseball game: in his first two at-bats he hit the ball about as hard as anyone could hit it, yet he also hit it right at an outfielder. Two great at-bats and nothing to show for it. So what did he do? He simplified matters. “This time I’m hitting it where no one can catch it,” he muttered as he went to the plate for at-bat number 3. Seconds later he smashed a mammoth, three-run, game-winning home run.

Note. Hey, who said that, in just one at-bat, the Scripting Son did something – hit a home run – that the Scripting Dad couldn’t do in all his years of baseball? We are shocked that someone could even think something like that. Not that it isn’t true; we just don’t like to be reminded of it.

Besides, the Scripting Dad was a leadoff hitter, and leadoff hitters never hit home runs. And no, we haven’t heard of Rickey Henderson. Or Craig Biggio. Or Alfonso Soriano. Or – say, shouldn’t we be talking about scripting?!?

OK, so maybe the Scripting Dad never hit a home run in his life. He did, however, come up with a script that can automatically create a new Excel spreadsheet based on and named after the current date. And he also came up with a simple way to make sure that script runs at midnight each night, something we’ll talk about in a few minutes.

But first things first:

dtmDate
= Date

strMonth = Month(Date)
strDay = Day(Date)
strYear = Right(Year(Date),2)

strFileName = "C:\Scripts\" & strMonth & "-" &
strDay & "-" & strYear & ".xls"

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
objWorkbook.SaveAs(strFileName)

objExcel.Quit

Like we said, simplicity is a virtue. The script starts out by using the VBScript Date function to store the current date in a variable named dtmDate. We then use the functions Month, Day, and Year to grab individual pieces of that date and store them in the variables strMonth, strDay, and strYear. We should point out that, when extracting the year, we also use the Right function and grab only the two rightmost characters. Why? So that the variable strYear will be equal to 06 instead of 2006. If you’re OK with having all four digits of the year in the file name then that particular line of code can be simplified to look like this:

strYear = Year(Date)

Next we need to construct the full path to the new Excel file, something we do with this line of code. If the date happens to be June 1, 2006 the value of the variable strFileName (and thus the new file name) will be C:\Scripts\6-1-06.xls:

strFileName = "C:\Scripts\" & strMonth & "-" & strDay & "-" & strYear & ".xls"

If you’re having problems seeing how we came up with C:\Scripts\6-1-06.xls it might help to take a look at the individual items we’re combining in that line of code:

· C:\Scripts\

· 6 (the value of strMonth)

· -

· 1 (the value of strDay)

· -

· 06 (the value of strYear)

· .xls

Or, to put it pseudo-mathematically:

C:\Scripts\ + 6 + - + 1 + - +06 +.xls = C:\Scripts\6-1-06.xls

To tell you the truth, deriving the path name is the most difficult part of the entire script (and even that wasn’t too-terribly hard). Once we have the path name we then create an instance of the Excel.Application object and set the Visible property to True. (Incidentally, you don’t have to set the Visible property to True. We did it in this sample script simply so you’d have a visual cue as to whether or not the script was actually working.)

With Excel up and running we then use this line of code to create a new workbook:

Set objWorkbook = objExcel.Workbooks.Add()

And as soon as we have a new workbook we can call the SaveAs method, passing the path name we just created as the sole parameter:

objWorkbook.SaveAs(strFileName)

Believe it or not, that’s all we need to do. SaveAs will save a new, blank spreadsheet file for us, and we can then use the Quit method to terminate Excel. Simple, yet effective.

Of course, that’s all well and good, except for one thing: how do we make sure that the script runs every night at midnight? Admittedly we could write some fairly complicated code that runs in the background, periodically checking the date and time, and then creating a new spreadsheet each time the clock strikes midnight. Like we said we could do that, but: 1) that sounds like a lot of work; and, 2) you have the problem of figuring out what to do if the script ever stops for some reason. (Not to mention figuring out how’d you even know that the script has stopped.) Believing whole-heartedly in the concept of simplicity, we decided to sidestep all those problems and instead schedule the script to run as a Scheduled Task.

What’s that? You say you’ve tried running scripts as Scheduled Tasks and they never worked? That’s probably because you listed the script itself as the program to run. For example, in the Scheduled Tasks wizard you might have entered something similar to this in the Select Program to Schedule dialog box:

And you’re right: that probably won’t work. What you need to do instead is pick one of the script hosts (either Cscript.exe or Wscript.exe) as the program to run, and then specify your script as a command-line parameter for that program. To do that, select one of the script hosts (for example, C:\Windows\System32\Cscript.exe) as the program to be scheduled:

When you get to the last page of the Scheduled Tasks Wizard don’t just click Finish. Instead, select the checkbox labeled Open advanced properties for this task when I click Finish checkbox and then click Finish:

Once you’ve done that, wait for the dialog box to appear and then add the path to your script (for example, C:\Scripts\Test.vbs) as a command-line parameter to Cscript.exe:

In other words, the Scheduled Task should show something like this in the Run box:

C:\WINDOWS\system32\cscript.exe c:\scripts\test.vbs

That should take care of any problems you’ve had trying to run scripts as scheduled tasks.

Note. Sure, you can write scripts to create, delete, and otherwise manage scheduled tasks, although there are some limitations. For more information, check out this portion of the Microsoft Windows 2000 Scripting Guide.

And no, the Scripting Dad never hit an inside-the-park home run, either. And his teams never won a single championship, another sore spot seeing as how the Scripting Son has won everything from league championships to city championships to district all-star championships. Of course, the Scripting Dad is one of the Microsoft Scripting Guys. We’d like to see the Scripting Son top that!

Yes, yes, you’re right: hitting a game-winning home run does top being a Scripting Guy, hands-down. But, hey, what doesn’t?


How Can I Delete All the Worksheets in a Spreadsheet Except for the First Worksheet?

Hey, Scripting Guy! How can I delete all the worksheets in an Excel spreadsheet except for the first worksheet?

-- NN

Hey, NN. Please excuse any bloodstains that might appear on your screen; as it turns out, the Scripting Guy who writes this column was involved in an … incident … this past weekend. Ah, but dedicated Microsoft employee that he is, he’s already back at work, and back to writing this column.

In case you’re wondering, this weekend the Scripting Guy who writes this column and the Scripting Son were playing wiffle ball in the backyard. The Scripting Guy who writes this column somehow managed to hit three home runs on three consecutive pitches. The Scripting Son, who was none-too-pleased with this surprising turn of events, decided it would be … funny … to throw the next pitch as hard as he could. (That’s something you typically don’t do, seeing as how, in this particular game, the pitcher stands maybe 20 feet from the hitter in the first place.) The Scripting Guy who writes this column managed to duck out of the way of the pitch, but the ball hit the storage shed, bounded off into the dense jungle in back of the house, and rolled down a tiny – but steep – ravine.

That’s where the fun began. Down to their last wiffle ball, the Scripting Dad and Son stood at the top of the ravine. “Hey, I see the ball,” said the Scripting Dad. “I think I can get it.” And with that he began inching his way down the hill, balanced precariously on a tangle of dead branches and dead blackberry bushes.

“Just let it go, Dad,” said the Scripting Son. “That stuff you’re walking on doesn’t look very stable. Let’s just go buy another ball.”

“Don’t worry,” said the Scripting Dad. “I know what I’m doing.”

As it turned out, what he was doing was walking on a pile of dead branches and dead blackberry bushes, a pile that soon gave way, pitching the Scripting Dad headfirst down the hill and into the blackberry bushes below.

You might think that landing face first in a bunch of blackberry bushes would hurt. You would be right about that.

Fortunately, other than a number of blackberry thorns that had to be removed from his hands, legs, and face, the Scripting Guy who writes this column was fine. In fact, once he stopped bleeding, the whole thing was actually pretty funny.

Or at least the Scripting Son thought so.

But all’s well that ends well, right? After all, the Scripting Guy who writes this column is still alive (or at least as alive as he’s ever been), and everyone else still gets a script that deletes all the worksheets in an Excel spreadsheet except for the very first worksheet:

Set objExcel = CreateObject("Excel.Application")

objExcel.Visible = True
objExcel.DisplayAlerts = False

Set objWorkbook
= objExcel.Workbooks.Open("C:\Scripts\Test.xls")
i = objWorkbook.Worksheets.Count

Do Until i =
1
objWorkbook.Worksheets(i).Delete
i = i - 1
Loop

Provided we don’t fall down another ravine in the process we’ll take a few minutes here to explain how this script works. As you can see, we begin by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Excel that we can view on screen. That brings us to this line of code:

objExcel.DisplayAlerts = False

What’s this line for? Well, if you try to delete an Excel worksheet and that worksheet contains data, a dialog box will pop up asking if you really want to delete this worksheet. (This happens regardless of whether you use a script or you try to delete the worksheet from within Excel itself.) We don’t really want to see that dialog box; after all, that would cause our script to stop each time we attempt to delete a worksheet, and, even worse, to stay stopped until we answer the dialog box. However, if we set the DisplayAlerts property to False that prevents the dialog box from appearing; instead, the script will act as though the dialog box had appeared and, in turn, as though we had clicked the default button. In this case, at least, that’s perfect: that’s because the default button happens to be Yes (meaning that we do want to delete the worksheet).

After configuring the DisplayAlerts property we next call the Open method to open the workbook C:\Scripts\Test.xls:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

And once the file is open we use the Count property to determine the number of worksheets in Test.xls, storing that value in a counter variable named i:

i = objWorkbook.Worksheets.Count

Now we’re ready to start deleting worksheets.

Of course, deleting items from Microsoft Office collections can be tricky; that’s due to the way Office renumbers items any time something gets deleted from a collection. One way to work around this problem is to start at the tail-end of the collection and work your way back towards the beginning. With that in mind we set up a Do Until loop that looks like this:

Do Until i = 1
  objWorkbook.Worksheets(i).Delete
  i = i - 1
Loop

Sure, we’ll explain how this loop works. Let’s say we have 5 worksheets in our collection. What we’re going to do is start off by deleting the last worksheet in the collection (which has an index number of 5). That’s what this line of code does:

objWorkbook.Worksheets(i).Delete

This works because we’ve assigned the number of worksheets to the counter variable i; as a result, the value of our counter variable (5) is equal to the index number of the last worksheet in the collection (also 5). After deleting worksheet 5 we decrement i by 1; if you do the math, that means i is now equal to 4. We loop around, delete worksheet 4, decrement i by 1, then return to the top of the loop and do the whole thing all over again. This continues until i is equal to 1. When i is equal to 1 that means we only have one worksheet in the collection; thanks to a little planning and foresight we also know that this lone worksheet is the very first worksheet in the collection. And because we’ve now deleted all the worksheets except for the very first one, that means we can go ahead and exit the loop and let the script terminate.

And there you have it: as easy as falling off a log. Or at least as easy as falling off a pile of dead branches and dead blackberry vines.

Speaking of which, if you’re waiting for the punch line to today’s story, well here it is. As long as he was lying at the bottom of the ravine, the Scripting Guy who writes this column figured he might as well retrieve the wiffle ball. When he did, he discovered that there was now a big crack in the ball and it was no longer usable. In other words, he ended up having to go out and buy another ball anyway; he just took a slightly more circuitous (and painful) route than he needed to.


How Can I Delete Data from a Spreadsheet Yet Keep All the Formatting?

Hey, Scripting Guy! How can I delete data from a spreadsheet yet keep all the formatting?

-- TB

Hey, TB. From your email it sounds like you have a script that periodically replaces data in a spreadsheet with new, up-to-date values. The spreadsheet has been preformatted, and what you’d like to do is delete all the old values – thus making room for new values – but without deleting any of the formatting. That way you can simply plug in the new values without having to redo the fancy formatting.

We hope it doesn’t look exactly like this, but for now we’ll pretend your spreadsheet has the same kind of nifty formatting shown here:

So can we delete the data yet leave all this…interesting…formatting behind? Of course we can:

Set
objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.UsedRange.ClearContents

This simple little script starts off by creating an instance of the Excel.Application object and then sets the Visible property to True (we set Visible to True just so we can see the changes take place on screen). Next we use these two lines of code to open the file C:\Scripts\Test.xls and create an object reference to Sheet1, the first worksheet in the file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

To delete the values without affecting the formatting takes just one line of code:

objWorksheet.UsedRange.ClearContents

All we’re doing here is taking UsedRange (which represents all the cells that have data in them) and calling the ClearContents method. As you might have guessed, ClearContents will delete the data but leave the formatting alone. When we run the script we’ll end up with a spreadsheet that looks like this:

Admittedly it’s not the most impressive spreadsheet we’ve ever seen, either. But look what happens when you add data to a cell:

As you can see, all the old formats have been retained. All we have to do is add the data and our spreadsheet will be just as pretty as ever. And as Martha Stewart would say, that’s a good thing.

OK, so she probably wouldn’t say that if she saw the way we formatted this spreadsheet. But you know what we mean.

By the way, if you’re interested in Microsoft Office Scripting be sure and check out the Office Space column.


How Can I Delete Specified Rows in an Excel Spreadsheet?

Hey, Scripting Guy! How can I delete a row in an Excel spreadsheet if there is a specific value in column A of that row?

-- OI

Hey, OI. Before we get rolling on today’s column, have we mentioned that time is beginning to run out on the Scripting Guys TechEd Challenge? We have? Oh. Well, we’re going to mention it again anyway, because time really is running out on the TechEd Challenge: you only have until 11:59 PM on June 30th to submit your entry. And why would you even want to submit your entry? That’s easy; after all, the top five scorers are going to win a Scripting Guys gift pack consisting of:

· A Dr. Scripto bobblehead doll

· A Scripting Guys coffee mug

· A Scripting Guys T-shirt

· A copy of Dr. Scripto’s Fun Book

· Anything else we have lying around the office that we’re hoping to get rid of

Now, admittedly, the competition is fierce: we’ve actually had people submit perfect scores. (Much to our surprise, and somewhat to our chagrin: when we first issued the challenge we said that we would rename the Script Center, for one day, after anyone who received a perfect score. If you happen to run into anyone from TechNet, don’t tell them that we’re going to be renaming the Script Center soon. We’d kind of like to … surprise … them with that. No doubt they’ll be thrilled.)

However, in keeping with Script Center tradition, you don’t have to be a winner to be a winner. All the entries we receive (even from people who somehow fail to earn a single point) will go into a hat; from there we’ll draw the names of 10 lucky souls who will receive their very own Dr. Scripto bobblehead doll. Does it get any better than that? In a word: no.

Of course, that does lead to an obvious question: how do you even begin to prepare yourself to undergo something as rigorous and demanding as the TechEd Challenge? That’s also an easy one. How do you prepare for the TechEd Challenge? The same way you prepare for any of life’s challenges: by learning how to write a script that can delete a row in an Excel spreadsheet if there is a specific value in column A of that row.

Fortunately, we just happened to have a script that does that very thing:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook
= objExcel.Workbooks.Open("C:\Scripts\Test.xls")

i = 1

Do Until objExcel.Cells(i, 1).Value = ""
If objExcel.Cells(i, 1).Value =
"delete" Then
Set objRange = objExcel.Cells(i, 1).EntireRow
objRange.Delete
i = i - 1
End If
i
= i + 1
Loop

Let’s see if we can figure out how this script works. (At the risk of spoiling the suspense, we’ll note that there is one plot twist that you’ll need to pay special attention to.) We start out by creating an instance of the Excel.Application object and setting the Visible property to True; that gives us a running instance of Excel that we can see on screen. After that we call the Open method to open the file C:\Scripts\Test.xls, then assign the value 1 to a counter variable named i (a variable we’ll use to keep track of the current row in the worksheet). At that point, we’re ready to start whacking rows in the spreadsheet.

Speaking of which, for educational purposes our spreadsheet looks something like this:

As you can see, we have 9 rows of data; in column A of each of these rows we have either the word delete or the word keep. Our script is going to methodically look at the value of column A in each row. If it finds the word delete the script will delete that row; if it finds the word keep the script will keep that row. If everything goes according to plan (and, with the Scripting Guys, everything always goes according to plan … sort of) when the script finally comes to a halt the spreadsheet should look like this:

All we have to do now is figure out how to get from Point A to Point B.

As highly-trained professionals, the Scripting Guys know that you’re unlikely to get from Point A to Point B unless you start out by doing something. Therefore, our first step is to set up a Do Until loop that runs until we encounter a blank cell in column A:

Do Until objExcel.Cells(i, 1).Value = ""

As you can see, all we’re doing here is checking to see if cell i, 1 (row i, column 1) is equal to an empty string (“”). If it is, then we’re done: that means we’ve looked at all the rows in the spreadsheet (or at least all the rows that contain data). If it’s not empty, well, then we have some work to do.

In particular, what we have to do is determine the actual value of the cell in question: we need to know if the value is equal to delete. If it’s not, well, no big deal: we simply increment the counter variable i by 1, then loop around and repeat the process with the next row in the spreadsheet. But suppose the value is equal to delete? What do we do then?

Here’s what we do then, we execute this block of code:

Set objRange = objExcel.Cells(i, 1).EntireRow
objRange.Delete
i = i - 1

In the first line we create a Range object that begins in cell i, 1 and then proceeds to gobble up the rest of the row as well. (How do we know that this range encompasses the entire row? Because we used the EntireRow property when we created the range.) In line 2 we then call the Delete method and delete the row in question. That’s pretty east, and pretty straightforward.

The third line in our little code block is the tricky part. Why i = i - 1? Having deleted one row in the spreadsheet our thoughts should logically turn to the next row in the spreadsheet. Shouldn’t this line of code be i = i + 1 instead?

Believe it or not, no; it shouldn’t be. And here’s why.

Suppose we’re on row 2 of the spreadsheet. What happens if we delete this row? That’s right: row 2 disappears, but what used to be row 3 in the spreadsheet moves up and becomes the new row 2. In other words, each time we delete a row the remaining rows in the worksheet renumber themselves. What used to be row 3 is now row 2.

Of course, there’s nothing wrong with that, but it does lead to one problem: we haven’t checked the value of this new row 2 yet. That’s why we decrement the value of our counter variable: if i was equal to 2, we subtract 1 from that value and make i equal to 1. That way, when we encounter the line of code that increments the counter variable, i will be set to 2. Doesn’t that mean we’re going to check row 2 again? Yes, and that’s the point: remember, this is a brand-new row 2, one that we haven’t checked yet.

Yes, we know: it is a little confusing, isn’t it? However, do this: run the script once with a sample spreadsheet and see what happens. When the script finishes, comment out the line i = i - 1 and re-run the script. At that point it should be clear why we need to perform this little trick.

And that, believe it or not, is all we have to do. When we’re all done the rows marked delete will be deleted, and the rows marked keep won’t be deleted. Which means that, for once, at least, things really did go according to plan.

And remember: enter the TechEd Challenge while there’s still time. Things are still a bit up in the air, but, for better or worse, this might be the last chance you ever have to get a Dr. Scripto bobblehead doll that you can call your own. As it stands right now, the Scripting Guys budget for the upcoming fiscal year is – let’s see, divide by 12, carry the 2, round up to the nearest dollar … 0 dollars and 0 cents. Needless to say, that means that this might very well be a once-in-a-lifetime opportunity.

Oh: and don’t forget that, if you get a perfect score, for one day we’ll rename the Script Center after you. That’s also likely to be a once-in-a-lifetime opportunity, especially when TechNet finds out what we’re doing ….


How Can I Determine the Background Color of a Spreadsheet Cell?

Hey, Scripting Guy! We have an Excel spreadsheet that tracks the status of all our projects. In that spreadsheet the status for a project is indicated by a color assigned to a cell. How can I use a script to determine the color of these cells?

-- AE

Hey, AE. Just a second; we need to catch our breath. Sorry. Last night the Scripting Guy who writes this column and the Scripting Son went to the gym. As they sat down on the exercise bikes the Scripting Son set’s Dad’s difficulty level to 20, twice what they normally do. “Don’t worry, I’ll set it back,” he said. “There’s no way an old guy like you could do level 20, not even for one minute.”

Of course, a mature, grown-up adult would have replied, “Thank you, son. After all, one should never take foolish risks simply to win a dare or otherwise ‘prove oneself.’” Unfortunately, though, there weren’t any mature, grown-up adults around at the time. “Listen,” said the Scripting Dad. “I’ll not only do level 20 for one minute, I’ll do level 20 for five minutes. And I’ll do it fast, too.”

To his … credit … he did just that: he rode the bike , at level 20, for 5 minutes, fast. At the end of those 5 minutes, he was sweating hard and breathing even harder. This morning, a good 13 or 14 hours later, he’s stopped sweating. But he’s still breathing hard.

And sure, maybe that was kind of a dumb thing to do. But the important thing is that the Scripting Dad put the Scripting Son in his place. Which he’ll remind him of, assuming that he ever catches his breath long enough to speak again.

Note. Turns out that oxygen might be a little more important than the Scripting Guy who writes this column may have given it credit for.

Fortunately, it doesn’t take a lot of oxygen to write a Hey, Scripting Guy! column. (Well, assuming the Scripting Editor hasn’t set the keyboard difficulty level to 20, that is.) Hold on a second … OK, here goes:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

i = 2

Do Until x = 1
    If objWorksheet.Cells(i,1) = "" Then
        Exit Do
    End If

    intColor = objWorksheet.Cells(i,2).Interior.ColorIndex

    Select Case intColor
        Case 3 strStatus = "Behind schedule"
        Case 4 strStatus = "Project complete"
        Case 6 strStatus = "Project on schedule"
        Case Else strStatus = "No information"
    End Select

    Wscript.Echo objWorksheet.Cells(i,1).Value & " -- " & strStatus
    i = i + 1
Loop

Whew; does it seem hot in here to anyone else? Oh, well. Before we go any further we should note that, without knowing exactly what your spreadsheet looks like, AE, the best we can do is provide a generic answer to your question. For our sample script we’re using a simple little spreadsheet that looks like this:

As you can see, the spreadsheet merely lists the project names in column A and the status of each project in column B. In this sample spreadsheet status can be interpreted like so:

· Red means that the project is behind schedule.

· Yellow means that the project is in progress and on schedule.

· Bright Green means that the project has been completed. Coincidentally, this is also the same color the Scripting Dad was by the time he finished his bike ride.

As for the script itself, we start out in pretty straightforward fashion, creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We then use these two lines of code to open the file C:\Scripts\Test.xls and bind to the first worksheet in that file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Wow; most days typing in two lines of code wouldn’t tire us out like that. (Sure, it tires us out some, but usually not this much.) But we’ll solider on the best we can. With our spreadsheet open we next create a counter variable named i and assign this variable the value 2; we’ll use i to keep track of our current row in the spreadsheet. That brings us to this block of code:

Do Until x = 1
    If objWorksheet.Cells(i,1) = "" Then
        Exit Do
    End If

    intColor = objWorksheet.Cells(i,2).Interior.ColorIndex

    Select Case intColor
        Case 3 strStatus = "Behind schedule"
        Case 4 strStatus = "Project complete"
        Case 6 strStatus = "Project on schedule"
        Case Else strStatus = "No information"
    End Select

    Wscript.Echo objWorksheet.Cells(i,1).Value & " -- " & strStatus
    i = i + 1
Loop

What we’ve done here is set up a Do Until loop that’s designed to run until x is equal to 1; of course, because we never do anything to make x equal to 1 that means that the loop is really designed to run forever and ever. Fortunately, there’s a, well, loophole that we can use to break out of this never-ending story (something we’ll discuss in a minute). But the loop isn’t all that interesting; what’s interesting is what we do inside that loop.

Of course, the very first thing we do inside the loop – depending on the circumstances – is exit the thing. Remember the loophole we told you about? That’s what this is for:

If objWorksheet.Cells(i,1) = "" Then
    Exit Do
End If

What we’re doing here is checking the value of cell A2. How do we know it’s cell A2? Because we’re looking at cell row i, column 1, and, at the start of the loop, i is equal to 2. The cell located at row 2 column 1 is more-commonly known as cell A2. If the value of cell A2 is equal to nothing (“”) we call the Exit Do statement to exit the loop. Checking for a blank cell is how we know whether or not we’ve reached the end of the data. Because we have no blank rows in the spreadsheet a blank cell in column A can only mean one thing: there’s no more data to read.

Let’s assume, however that there is a value in cell A2. In that case, we use this line of code to determine the color of cell B2 (because we track status in column B) and then store the integer value of that color in a variable named intColor:

intColor = objWorksheet.Cells(i,2).Interior.ColorIndex

Nothing too fancy here: we simply grab the value of the cell’s Interior.ColorIndex property. Like we said, this will always be an integer value: 3 represents the color red; 4 represents the color green; and 6 represents the color yellow.

Note. Are there more colors available to you than just red, green, and yellow? You bet; take a look at this Office Space article for details, and for more information about the Interior.ColorIndex property.

After we’ve determined the color of the cell we then set up the following Select Case statement to interpret the integer value:

Select Case intColor
    Case 3 strStatus = "Behind schedule"
    Case 4 strStatus = "Project complete"
    Case 6 strStatus = "Project on schedule"
    Case Else strStatus = "No information"
End Select

Again, this shouldn’t cause you any undue problems. The first time through the loop (at least with the sample spreadsheet we showed you) intStatus will be equal to 6; that’s because the cell has a yellow background. In turn, that means we set the value of a variable named strStatus to Project on schedule. That’s what this line of code does:

Case 6 strStatus = "Project on schedule"

With the value of strStatus set we can then echo back the name of the project (found in cell A2) plus the project status (strStatus):

Wscript.Echo objWorksheet.Cells(i,1).Value & " -- " & strStatus

Yes, that was easy, wasn’t it? And trust us: no one is happier about that than the Scripting Guy who writes this column.

After echoing back status information for Project 1 we increment the value of i by 1; that means that, the second time through the loop, we’ll be working with row 3 in the spreadsheet. This process continues until we encounter a blank row; at that point the script automatically ends.

And what kind of output can we expect to get here? Output like this:

Project 1 -- Project on schedule
Project 2 -- Behind schedule
Project 3 -- Project complete
Project 4 -- Project complete
Project 5 -- Behind schedule

We hope that helps, AE. If not, write back to us – tomorrow. We have a feeling we won’t be doing much of anything today.

And no, that has nothing to do with being tired; in fact, we’ve fully recovered by now. We just don’t feel like working today.


How Can I Determine the Last Row in an Excel Spreadsheet?

Hey, Scripting Guy! How can I determine the last row in an Excel spreadsheet? I can’t append data to the file unless I know which row to start with.

-- TGW

Hey, TGW. You know, this is the kind of thing that separates the Scripting Guys from the rest of the world. Most people would do the same thing you’re doing: looking at their spreadsheet and thinking, “OK, so how do I know where to start? After all, I want to make sure I do this right.” That’s not the kind of question you’ll ever hear a Scripting Guy ask. Instead, a Scripting Guy is more likely to go ahead and run the script and then say something like this: “Uh-oh: I wonder if I really should have done that.”

Note. And, yes, we are thinking of the time one of the Scripting Guys wrote a script that opened a text file and replaced the existing contents of that file with a bunch of new data. The problem? Without thinking – and without bothering to check his work – this Scripting Guy inadvertently typed in the name of his script as the text file to open. His script ran, dutifully opened the .vbs file, and replaced all of his unbacked-up script code with a bunch of data that was supposed to be stored in a separate text file. Bye-bye .vbs file.

And, no, he wasn’t particularly upset. After all, for that Scripting Guy it was just another day at the office.

Ironically, even though the Scripting Guys tend to be a bit impulsive and irresponsible we actually know how to do things the right way; it’s just that we don’t bother to do things the right way. But you don’t have to be like us (thank goodness!). Instead, you can use a script like this one, which will position the cursor in the next available row in an Excel spreadsheet:

Const xlCellTypeLastCell = 11

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

Set objRange = objWorksheet.UsedRange
objRange.SpecialCells(xlCellTypeLastCell).Activate

intNewRow = objExcel.ActiveCell.Row + 1
strNewCell = "A" & intNewRow

objExcel.Range(strNewCell).Activate

Before we explain this all let’s show you what our sample spreadsheet looks like:

Now, we didn’t show you this sample spreadsheet because we’re so proud of it. (Although, come to think of it, it is pretty nice, isn’t it?) Instead we wanted to point out that it’s a spreadsheet that includes some blank rows. Is that important? It can be. For example, it’s possible to mimic the use of the Ctrl+Down Arrow using a script. That can help you determine the last row in a spreadsheet … provided, of course, that you don’t have any blank rows. In our sample, spreadsheet, however, a Ctrl+Down Arrow script won’t work: that’s because Ctrl+Down Arrow is designed to stop at the first blank row, meaning it will think row 5 is the last row in the spreadsheet. That’s why we didn’t use a script that mimicked Ctrl+Down Arrow.

Incidentally, that’s also why we didn’t just check to see if the value of cell A-whatever was equal to nothing and call it good. In a spreadsheet like this one, some rows simply have no value in column A.

So what does our script do? Well, it starts out by defining a constant named xlCellTypeLastCell and setting the value to 11; we’ll talk about what this is for in a moment. We then have several lines of code that create a running (and visible) instance of the Excel.Application object, open the file C:\Scripts\test.xls, and then create an object reference to Sheet 1:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

Now it’s time to roll up our sleeves and get to work. The first thing we do is issue this command:

Set objRange = objWorksheet.UsedRange

Why? We’re glad you asked that. The UsedRange is a worksheet property that represents the entire range of cells that have data in them. For example, suppose we had a spreadsheet that had some data in cell A1 and some data in cell Z37. What’s the used range going to be? It’s going to be a range that starts in cell A1, ends in cell Z37, and includes all the cells in between.

The key here is that the used range includes the very last row in the spreadsheet that contains data; as you might recall, that’s exactly the row we’re looking for. The problem now is this: how do we figure out which row is the last row?

Here’s how. We start off by using the SpecialCells and Activate methods to move the cursor to the very last cell in the range and make that cell the active cell. This is where the constant xlCellTypeLastCell gets used; that tells the script to go to the last cell in the range:

objRange.SpecialCells(xlCellTypeLastCell).Activate

All that puts the cursor in the very last cell in the range; however, we still don’t know the row number of that cell, which means we still don’t know which is the first available row in the spreadsheet. But then, that’s what this code is for:

intNewRow = objExcel.ActiveCell.Row + 1
strNewCell = "A" & intNewRow

In the first line we get the value of the active cell’s Row property and add 1 to it. Why? Well, in our sample spreadsheet the last cell in the range (which is now the active cell) lies in row 16. If we add 1 to that we’ll get row 17, which just happens to be the next available row in the spreadsheet. In the next line we construct a cell address consisting of the letter A and the row number; in this example, that’s cell A17, which represents the location we should start at when we append data to this spreadsheet.

Finally we create a new range object consisting of cell A17 and use the Activate method to make A17 the active cell. In turn, that moves the cursor into cell A17, and we’re ready for business. (And, for once, at least, without having to say “Uh-oh: I wonder if I really should have done that?”)


How Can I Format an Excel Spreadsheet So It Retains Leading Zeroes?

Hey, Scripting Guy! How can I format an Excel spreadsheet so that it retains leading zeroes?

-- AS

Hey, AS. You know, one of the Scripting Guys still has nightmares about the number zero. Several years ago he got a phone call from a friend of his wondering if he’d be interested in helping coach a youth league basketball team (10- and 11-year-olds). Never being one to look before leaping, this Scripting Guy said, “Sure.”

As it turned out, this team was put together at the last minute, and consisted of all the players none of the other coaches in the league wanted. (They were all 10-year-olds, too, in a league dominated by 11-year-olds.) After losing the opening game 14-12 – and sparking a naïve bit of optimism – the team was shut out in each of the next three games.

Yes, you heard correctly: shut out. Shut out in basketball, the same game in which Wilt Chamberlain once scored 100 points all by himself. No points, nothing, nada, zip. In other words, all zeroes.

Incidentally, if you’re ever looking for a way to knock yourself down a peg or two, we’d suggest being the coach of a basketball team that goes three consecutive games without scoring a single point. That humbles you in a hurry.

Fortunately, AS, leading zeroes in Excel are less likely to damage your self-esteem. We’re assuming you want to add a few numbers – say, the numbers 1 through 10 – to a spreadsheet, and then have those numbers formatted something like this:

001
002
003
004
005
006
007
008
009
010

Can you do that? Well, like we said, when it comes to putting up zeroes, at least one of the Scripting Guys is a world-class expert:

Set
objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

For i = 1 to 10
objExcel.Cells(i, 1).Value = i
Next

Set objRange = objWorksheet.UsedRange
objRange.NumberFormat = "000"

As seems to always be the case with Excel, this is actually a very simple little script. The first four lines of code, for example, are boilerplate: all they do is create a visible instance of Excel, and then give us a blank workbook and a blank worksheet to work with. We then have this block of code that simply runs through cells A1 through J1 and places a number (1 through 10, corresponding to the row number) in each cell:

For i = 1 to 10
    objExcel.Cells(i, 1).Value = i
Next

In other words, our spreadsheet will have numbers that look like this:

1
2
3
4
5
6
7
8
9
10

That’s a good point: we don’t want our numbers to look like that, do we? That’s why the script includes these last two lines of code:

Set objRange = objWorksheet.UsedRange
objRange.NumberFormat = "000"

This is where all the fun takes place. In the first line, we create a Range object that encompasses all the cells on the spreadsheet that contain data (that’s what the UsedRange property gives us). After we’ve created this range we then set the value of the NumberFormat property to 000. That will give us our leading zeroes. In other words:

· The number 1 will be displayed as 001.

· The number 37 will be displayed as 037.

· The number 442 will be displayed as, well, 442

That’s all there is to it. If we wanted to display numbers using more zeroes then all we have to do add a few extra zeroes to the NumberFormat. For example, this line of code displays numbers using a maximum of six zeroes:

objRange.NumberFormat = "000000"

It’s that easy.

Incidentally, we have a confession to make. At the beginning of this column we said that the youth basketball team lost its first game 14-12. That’s not entirely true. The Scripting Guy’s friend received a phone call on a Friday asking if he’d be willing to coach the team; the caller said, “Oh, and your first practice is tomorrow.”

“Great,” said the friend. “When’s our first game?”

“Last night. But because you didn’t show up you had to forfeit.”

In other words, the team was 0-1 before anyone even knew it was a team. That tells you everything you need to know about that season.


How Can I Import a Fixed-Width File into Microsoft Excel?

Hey, Scripting Guy! How can I import a fixed-width file into Microsoft Excel?

-- JP

Hey, JP. We have a confession to make here, something we’re terribly ashamed of: for a moment – just for a moment, mind you – we doubted Microsoft Excel. Yes, we know: how could we do such a thing? All we can say is that we made a mistake, and we’re sorry. For some reason, though, when we first read this question we assumed that this would be a really hard thing to do; in fact, we were already thinking about ActiveX Data Objects (ADO) and other fancy scripting techniques that would make up for what we assumed would be Excel’s difficulty in handling fixed-width text files.

Excel: how can we ever make this up to you?

Personal Note to Excel. Actually, we’re not sure if we should recommend that people uninstall all the other software and use only you from now on; that might get Microsoft Word users and the 11 people who still use WordPerfect a little upset. We’ll have to think that over and get back to you.

OK, it’s time to put the mistakes of the past behind us and get back to work. We’re assuming, JP, that you have a simple little fixed-width text file similar to this:

First         Last              Department
Gail          Erickson          Human Resources
Ken           Myer              Accounting
Pilar         Ackerman          Research
Jonathan      Haas              Accounting
Syed          Abbas             Accounting

Interestingly, enough, we knew that Excel had a method named OpenText that could be used to open a text file as a spreadsheet. We also knew, from previous experience, that this method worked really well with comma- or tab-separated values files. What we weren’t sure of was how to make it work with a fixed-width file such as yours. We looked at the Excel documentation, tried following the instructions, and failed miserably.

Usually that’s about the time we give up. Because we thought this was going to be really challenging, however, we decided to persevere; after all, we assumed that we might end up with a Nobel Prize or something if we could somehow answer a question that required this level of expertise and sophistication.

Having failed at writing a script that used all the requisite parameters we decided to approach the problem methodically: we’d start with a script that used only a single parameter – the path to the text file – and go from there. We knew that this script would fail, but we figured that the error message would help us determine which of the other parameters we needed and how we would have to configure those parameters.

With that in mind, we started out with this little three-line script:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

objExcel.Workbooks.OpenText "C:\Scripts\Test.txt"

To make a long story short, the script worked perfectly: it opened the text file and put each field and record in the proper place. We were a bit surprised, but then we reasoned that this was probably due to the simplistic nature of our file. So we added more records and more fields, and we mixed in dates and numbers as well as text. We tried our simple script again and it worked flawlessly. If you have a fixed-width file in which there’s always at least one blank space between the end of one field and the beginning of the next then all you need to do is call the OpenText method and let Excel take care of the rest.

In fact, the only time we had to get a little fancier was when one field ran directly into the next. For example, consider this file (note the name Erickson-Wilkerson):

First         Last              Department
Gail          Erickson-WilkersonHuman Resources
Ken           Myer              Accounting
Pilar         Ackerman          Research
Jonathan      Haas              Accounting
Syed          Abbas             Accounting

In this case Excel stumbled when it came to Erickson-Wilkerson. Because there was no blank space to delineate the end of one field and the beginning of the next Excel simply treated the file as if it consisted of a single field; in other words, it dumped all the data into column B. Not exactly what we were hoping for.

Fortunately it was right about then that we finally figured out how to add additional parameters to the OpenText method. If you have a “messy” text file like the one shown above then you can import that file by using a script similar to this:

Const
xlFixedWidth = 2

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.Workbooks.OpenText _
"C:\Scripts\Test.txt",,,xlFixedWidth,,,,,,,,,Array(Array(0,1),Array(14,1),Array(32,1))

Yes, this one is a bit more complicated, so let’s walk you through how it works. We begin by defining a constant named xlFixedWidth and setting the value to 2. As we saw a moment ago, Excel has problems identifying a file like this as being a fixed-width file; therefore, we’ll use this constant to make that fact explicit.

Next we create an instance of the Excel.Application object and set the Visible property to True; this gives us a running instance of Excel that we can see on screen. Finally we call the OpenText method, passing a crazy looking parameter string:

objExcel.Workbooks.OpenText _
    "C:\Scripts\Test.txt",,,xlFixedWidth,,,,,,,,,Array(Array(0,1),Array(14,1),Array(32,1))

We won’t bother detailing all the empty parameters in this string; if you’re interested in that check out the Excel VBA Language Reference on MSDN. For now, we’ll just note that we pass along the path to the text file and then, after a pair of blank parameters, pass the constant xlFixedWidth to let Excel know what type of file we’re dealing with.

That brings us to this:

Array(Array(0,1),Array(14,1),Array(32,1))

What we’re doing here is defining each field in the text file (and thus each column in the spreadsheet). The entire set of field definitions must be passed as an array, which is why we have the outer Array function. But what about the individual elements within the array, elements like this: Array(0,1)?

We’re glad you asked that. Each individual column definition must also be passed as an array; in this case, a two-dimensional array. The first number in the array represents the starting character position for the field. The very first field starts in character position 1 so – well, so we set the starting position in the Array as 0. That’s because arrays tend to start at 0; because of that, the starting position in the array will always be one less than the character position in the file. Our second field starts off at character position 15; hence the second element in our array shows 14 (15 – 1) as the starting point.

We don’t really like it, either. But to quote the immortal John Prine, that’s the way that the world goes round.

The second number in each element – that is, the 1 in Array(0,1) – indicates the data type for the field. We used the value 1 (representing generic data) for each field. However, you can specify any of the values found in the following table:

Constant | Value
xlDMYFormat | 4
xlDYMFormat | 7
xlEMDFormat | 10
xlGeneralFormat | 1
xlMDYFormat | 3
xlMYDFormat | 6
xlSkipColumn | 9
xlTextFormat | 2
xlYDMFormat | 8
xlYMDFormat | 5

Like we said, this approach is a tad bit more complicated, but it will guarantee that Excel will correctly open and parse your text file, even if the data is a bit messy. And even here we’re talking only a few lines of code, and without having to rely on ADO or any other technology. In other words, we’re sorry, Excel: we’ll never doubt you again.


How Can I Insert a Column into a Spreadsheet?

Hey, Scripting Guy! How can I insert a column into an Excel spreadsheet?

-- HE

Hey, HE. Let’s assume you have a spreadsheet that looks something like this:

And, yes, it is a very nice little spreadsheet, except for one problem. As you can see, we goofed, and we forgot to insert Dataset 3; instead, we only have Datasets 1, 2, and 4. Even worse, Dataset 4 is in the wrong spot; it needs to be moved over to column D. In other words, we need to insert a new column between columns B and C. That will move Dataset 4 over to column D (where it belongs), and give us a blank column in column C where we can enter the missing information for Dataset 3.

Make sense? The only question is this: how do we insert a column in a spreadsheet?

Ah, but don’t worry, that’s what the Scripting Guys are here for. Here’s a script that writes a column header to columns A, B, and C, then inserts a new, blank column between columns B and C:

Const
xlShiftToRight = -4161

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1,1) = "Dataset 1"
objWorksheet.Cells(1,2) = "Dataset 2"
objWorksheet.Cells(1,3) = "Dataset 4"

Set objRange = objExcel.Range("C1").EntireColumn
objRange.Insert(xlShiftToRight)

The script begins by defining a constant named xlShiftToRight and setting the value to -4161 (yes, minus 4161; go figure). To insert a column (or a row) in Excel you select an existing column or row and then move all those cells either to the right (in which case you use the constant xlShiftToRight) or down (in which case you use the constant xlShiftDown, which has a value of -4121). After defining the constant we create an instance of Excel, set the Visible property to True, then add a new workbook and a new worksheet.

Next we add a header to cells A1, B1, and C1; this sets the stage for us to actually insert the new column. And we do that using just two lines of code:

Set objRange = objExcel.Range("C1").EntireColumn
objRange.Insert(xlShiftToRight)

To begin with, we create a Range object representing all the cells in column C, the column we need to shift over. We do that by creating a range consisting of a single cell in column C (in this case, cell C1) and then using the EntireColumn property to extend this range to all the cells in column C. All we have to do then is call the Insert method, passing the constant xlShiftToRight as the sole parameter. This simply tells Excel that we want to take the specified range – column C – and move those cells over to the right. And look what happens:

We’ve successfully inserted a new, blank column, just like we wanted to do.

And before you ask, here’s a similar that scripts that adds a new row across the top of a spreadsheet:

Const
xlShiftDown = -4121

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1,1) = "Dataset 2A"
objWorksheet.Cells(1,2) = "Dataset 2B"
objWorksheet.Cells(1,3) = "Dataset 2C"

Set objRange = objExcel.Range("A1").EntireRow
objRange.Insert(xlShiftDown)

Cool, huh? If this whets your appetite for Microsoft Office scripting be sure and check out the Office Space column, which features new Office scripting tips and tricks every Tuesday and Thursday.


How Can I Keep the Screen From Updating While Running an Excel Script?

Hey, Scripting Guy! I seem to recall that there’s a way to “freeze” the display in Microsoft Excel any time you carry out a lengthy operation; that way the user doesn’t see anything change until the operation is complete. Do you have any idea how I can do that?

-- IW

Hey, IW. Do we have any idea of how you can do that? Oh, IW, you’ve cut us to the quick; after all, we’re the Scripting Guys, for Pete’s sake! How can you even ask if we have any idea of how you can do that? To tell you the truth, IW, you’ve hurt our feelings – oh, look: you made Dean cry. It’s OK, Dean; we’re sure IW didn’t mean that the way it sounded. Come on now; Scripting Guys don’t cry.

Note. Admittedly, the Scripting Guy who writes this column came close after the University of Washington football team lost in overtime for the second straight week. But he didn’t actually cry; he just had something in his eye.

Sorry, folks; trying to get Dean calmed down could take awhile. In the meantime, do we have any idea of how you can freeze the display in Microsoft Excel any time you carry out a lengthy operation? Well, now that you mention it, no, we don’t. But we can look it up:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook
= objExcel.Workbooks.Add()
Set objWorksheet
= objWorkbook.Worksheets(1)

objExcel.ScreenUpdating = False

For i = 1 to
100
For j = 1 to 100
objExcel.Cells(i,j) = i * j
Next
Next
objExcel.ScreenUpdating = True

What we have here is a script that starts Excel and then fills a big range of cells (row 1, column 1 through row 100, column 100) with the product of the row number and the column number. You say that sounds boring, that nobody wants to sit there and watch all those numbers get added to an Excel spreadsheet? We couldn’t agree with you more; that’s why we’ve “frozen” the screen so that you don’t see each number get tediously added to the worksheet. Instead you just sit quietly for half a minute or so and then, like magic, watch as the fully-updated spreadsheet springs to life.

Oh for the love of – no, Peter, they didn’t say that you were boring; they said that Excel was boring. No, Peter, not on your sleeve; you’ll have to borrow the handkerchief from Dean as soon as he’s finished with it. Good heavens.

Kind of makes you wonder if maybe the Scripting Guys are a little too sensitive, doesn’t it?

Now, where we were? Oh, right: the script. As you can see, we start things out in fairly typical fashion, at least when it comes to scripts that use Microsoft Excel. After creating an instance of the Excel.Application object we set the Visible property to True; that gives us a running instance of Excel that we can see on screen. Next we use the Add method to add a new workbook to our instance of Excel, then use this line of code to bind to the first worksheet in that workbook:

Set objWorksheet = objWorkbook.Worksheets(1)

We’re now ready to start adding numbers to the worksheet, something we’ll do using this block of code (which we won’t bother explaining):

For i = 1 to 100
    For j = 1 to 100
      objExcel.Cells(i,j) = i * j
    Next
Next

Of course, as we’ve already decided, we aren’t interested in watching all these numbers get added to the worksheet. That’s why, just before we launch into the preceding block of code, we set Excel’s ScreenUpdating property to False:

objExcel.ScreenUpdating = False

What’s going to happen when we set ScreenUpdating to False? Well, depending on how you want to look at it, nothing. Our script will go ahead and run its calculations and add the results to the designated cells in the spreadsheet; however, you won’t see any of that happen. Instead, Excel will just sit there, seemingly frozen. As soon as we’ve finished adding values to the spreadsheet, however, we’ll set ScreenUpdating back to True:

objExcel.ScreenUpdating = True

At that point you’ll see everything that took place while the screen was frozen. In other words, you’ll go directly from a spreadsheet that has no data on it to a spreadsheet that has data in row 1, column 1 through row 100, column 100.

Give it a try and you’ll see what we mean.

Incidentally, temporarily disabling screen updating will also make your scripts run a tad bit faster. For example, on our test computer – with screen updating disabled – it took 17 seconds for this script to complete. With screen updating enabled, it took 13 seconds. Not a huge savings, but a savings nonetheless.

At any rate, we – hold on a second. Well, if we do take you out for ice cream will you two stop pouting?

Sorry, we have to go. We’ll see everyone tomorrow.


How Can I List the Addresses in My Sent Items Folder in an Excel Spreadsheet?

Hey, Scripting Guy! How can I get a list of all the email addresses in my Sent Items and then write those addresses to an Excel spreadsheet?

-- RD

Hey, RD. As a matter of fact, yes, we are having problems focusing on work today. Granted, that’s true most days. However, at least today it’s understandable, coming on the heels of a very exciting (and very enjoyable) opening to the college football season. The fact that our very own Washington Huskies thumped Syracuse 42-12 would have, by itself, made this past weekend a good one. However, when you couple that with the fact that Michigan was upset by Appalachian State, Washington State was thumped by Wisconsin, and the fact that Florida State lost, well, could the weekend have been any better?

OK, true. But it’s unlikely that the University of Oregon will be shut down any time soon.

Note. This is about the time the Scripting Editor slips in an editor’s note stating that the Scripting Guy who writes this column is just joking, and that he really has nothing against the University of Oregon. Don’t listen to her! This is also about the time when she says something like, “Remember, despite what you might think, very few people in the world care about college football.” We repeat: don’t listen to her. After all, the Scripting Editor thinks that car racing is a sport. Car racing?!? Next she’ll be telling us that soccer is a sport! (Editor’s Note: It is.)

Of course, the one drawback to having really good things happen (did we mention that Florida State got beat?) is that everything that happens after that tends to pale by comparison. And that’s a problem: how can we ever find anything to equal the excitement of this past weekend? That’s a tough question to answer. Fortunately, though, we did think of one thing that comes awfully close to generating that same level of excitement: a script that can retrieve a list of all the email addresses in Outlook’s Sent Items folder!

What’s that? You say that’s nowhere near as exciting as Appalachian State beating Michigan? That’s because we forgot to mention one thing: this script also writes those email addresses to an Excel spreadsheet!

Ah, yes; we figured that would get your heart pounding:

Const olSentMail = 5

Set objDictionary = CreateObject("Scripting.Dictionary")
Set objExcel = CreateObject("Excel.Application")

objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olSentMail)

Set colItems = objFolder.Items

For Each objItem in colItems
    Set colRecipients = objItem.Recipients
    For Each objRecipient in colRecipients
        strAddress = objRecipient.Address
        If Not objDictionary.Exists(strAddress) Then
            objDictionary.Add strAddress, strAddress
        End If
    Next
Next

i = 1

For Each strKey in objDictionary.Keys
    objWorksheet.Cells(i, 1) = strKey
    i = i + 1
Next

Let’s see if we can figure out how this script works. (If this gets too exciting for you, please stop reading and consult your doctor before finishing the article.) To begin with, we define a constant named olSentMail and assign this constant the value 5; we’ll use olSentMail to tell the script which Outlook folder we want to work with. After defining the constant we then create two objects: the Scripting.Dictionary object and the Excel.Application object. It should be fairly obvious what we need the Excel.Application object for; after all, we are planning to write the retrieved email addresses to an Excel spreadsheet. But why in the world do we need an instance of the Dictionary object? Well, sit tight; we’ll explain that in just a moment.

And no, that’s not because we have to go ask the Scripting Editor why we need the Dictionary object. This is just a literary device designed to heighten the suspense and keep you on the edge of your seat.

With our first two objects in hand we turn our attention to a pair of setup chores. First, we set the Visible property of our Excel object to True; that’s done simply so we can see our spreadsheet onscreen. After that, we then execute these two lines of code, lines of code that add a new workbook to our instance of Excel and then bind us to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

Once Excel is ready to go we then need to get Microsoft Outlook prepped and configured. To that end, we create an instance of the Outlook.Application object, then use the GetNamespace method to bind to the MAPI namespace. (As we’ve noted before, this is the only namespace you can bind to. Nevertheless, we still have to include code that calls the GetNamespace method and explicitly does this binding for us.) As soon as we’re connected to the MAPI namespace we then bind to the Sent Items folder by calling the GetDefaultFolder method, passing the constant olSentMail as a way to tell the script which Outlook folder we want to bind to:

Set objFolder = objNamespace.GetDefaultFolder(olSentMail)

Granted there’s a little typing involved here, but the first 9 or 10 lines of code in this script should be pretty straightforward.

Now it’s time to roll up our sleeves and get down to business. To begin with, we use this line of code to retrieve a collection of all the items (that is, all the email messages) found in the Sent Items folder:

Set colItems = objFolder.Items

From there we take the next logical step: we set up a For Each loop to loop us through each of the items in that collection:

For Each objItem in colItems

This is where matters get a tiny bit tricky. When we first sat down to tackle this problem, we assumed that each email message (each instance of an Outlook MailItem object) had some sort of property that indicated who the message was sent to; you know, some property like SentTo or SentToAddress, something like that. Interestingly enough, we were wrong: the MailItem doesn’t include a property that indicates who the message was sent to.

Note. So is that the first time the Scripting Guys were ever wrong about something? To the best of our recollection, yes, it is.

As it turns out, in order to find out who we sent an email message to we need to access the email’s Recipients collection, something we do with this line of code:

Set colRecipients = objItem.Recipients

And because this is a collection (after all, an email can be, and often-times is, addressed to more than one recipient), we need to set up a second For Each loop in order to loop through each recipient in the collection; that’s what this line of code is for:

For Each objRecipient in colRecipients

So then what do we do inside this loop? Well, the first thing we do is grab the value of the Address property and assign it to a variable named strAddress:

strAddress = objRecipient.Address

Note. Incidentally, when you try to access the Address property Outlook will pop up a security dialog box that says, hey, someone is trying to access email addresses, is that OK with you? You must check the Allow Access for box and then click Yes in order for the script to grab those email addresses; if you click No or if you try to ignore the dialog box your script will fail. This means that you can’t run this script in fully-automated fashion; if you start the script but don’t stick around long enough to address the dialog box the script will fail.

The next part of the script is optional, but – in our opinion – kind of handy. The odds are pretty good that you’re going to have duplicate recipients in your Sent Items folder; if you’ve sent 10 messages to kenmyer@fabrikam.com then you’re going to end up with 10 instances of kenmyer@fabrikam.com in your Excel spreadsheet. To avoid duplicate entries like that we decided to use –drumroll, please – the Dictionary object!

Note. OK, so maybe revealing the use of the Dictionary object isn’t quite as suspenseful and exciting as we thought it would be. But, then again, maybe that’s your fault for assuming that there could be suspense and excitement in a daily column about system administration scripting.

So how are we going to use the Dictionary object? Like this:

If Not objDictionary.Exists(strAddress) Then
    objDictionary.Add strAddress, strAddress
End If

What we’re doing here is using the Exists method to determine whether the recipient’s email address (represented by the variable strAddress) already exists as a Dictionary key. If the address is already in the Dictionary then we don’t do anything; that’s because, well, because the address is already in the Dictionary. (Besides, the Dictionary object doesn’t allow for duplicate keys.) If the address doesn’t exist in the Dictionary, then we use the Add method to add the address, using the variable strAddress as both the key and the item value:

objDictionary.Add strAddress, strAddress

Note. You say you don’t know what we mean when we throw out terms like “key” and “item value?” Then you need to take a look at this section of the Microsoft Windows 2000 Scripting Guide, or the Sesame Script article on using the Dictionary object.

After we’ve disposed of all the recipients for the first email message, we then loop back around (to the beginning of our first For Each loop) and repeat the process with the next email in the collection. When we’re all done, the Dictionary object will contain a list of unique recipient email addresses, as pulled from all the emails in our Sent Items folder. That means that all we have to do now is write that information to our Excel spreadsheet:

i = 1

For Each strKey in objDictionary.Keys
    objWorksheet.Cells(i, 1) = strKey
    i = i + 1
Next

As you can see, the process of writing this data to Excel begins with assigning the value 1 to a counter variable named i; we’ll use this variable to keep track of the current row in the spreadsheet. We then set up yet another For Each loop, this one designed to walk us through all the keys in our Dictionary object:

For Each strKey in objDictionary.Keys

So then what is this loop for? That’s easy: in this loop we assign the value of the Dictionary key (which, of course, is one of our recipient email addresses) to cell i, 1. The first time through the loop, that means that the email address gets written to cell row 1, column 1; remember, the value of i starts off as 1. We increment i by 1 (ensuring that the next email address will be written in cell row 2, column 1), then loop around and repeat the process with the next key in the Dictionary. When all of this is done – well, you can probably figure out for yourself what we’ll end up with when all of this is done: a spreadsheet containing all the email addresses extracted from messages in the Sent Items folder.

That should do it, RD. If you’re not totally obsessed with college football (although we don’t consider watching a game on Friday, two games on Saturday, another game on Sunday, and two more on Monday to be any kind of “obsession”) we should note that this weekend promises to be another exciting one, starting with the fact that the University of Washington hosts Boise State. (University of Idaho trivia note: If you go to the University of Idaho bookstore, you can buy a T-shirt that says “Boise is not a state.”) That should be good news for those of you who aren’t obsessed with college football: if the Huskies get thumped, the Scripting Guy who writes this column is unlikely to bore you with a play-by-play recap of the game. But if the Huskies should happen to win that one ….


How Can I Locate and Replace Information for a Specific Item in a Spreadsheet?

Hey, Scripting Guy! In an Excel spreadsheet, how can I locate and replace information for a specific computer?

-- BB

Hey, BB. You know, there’s an old saying that goes, “If all you have is a hammer then everything looks like a nail.” Well, all we Scripting Guys have at the moment is some code for modifying a record in a database. So guess what: to us everything looks like a database, including your Excel spreadsheet.

Fortunately, though, you actually can use database techniques to work with an Excel spreadsheet. And while we don’t have to use this approach to answer your question, using some basic database commands seemed a lot easier than writing a bunch of code to open the spreadsheet, search for the computer in question, figure out what row and column we’re in, change the data, save the spreadsheet, etc. etc. How much easier did it seem? This much easier:

On Error Resume Next

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=C:\Scripts\Inventory.xls;" & _
        "Extended Properties=""Excel 8.0;HDR=Yes;"";"

objRecordset.Open "Select * FROM [Sheet1$]", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

strSearchCriteria = "Name = 'atl-fs-01'"
objRecordSet.Find strSearchCriteria

objRecordset("IPAddress") = "192.168.1.100"
objRecordset.Update

objRecordset.Close
objConnection.Close

Before we talk about the script itself we should mention that, for this sample script, we’re assuming you have a very simple spreadsheet, one that looks something like this:

As you can see, we have only two fields here: Name and IPAddress. Needless to say you aren’t limited to just two fields; we kept our spreadsheet small in order to keep the script as simple as possible (and to enable us to keep the screenshot as small as possible). But you can have as many fields as you want (or at least as many fields as there are columns in Excel).

We should also note that we won’t explain today’s script in minute detail; that’s because we have an Office Space article that already does that for us. We’ll focus primarily on the code that updates the spreadsheet, and simply refer you to the Office Space article for more information on the ADO (ActiveX Data Objects) code that lets you treat Excel as a database.

The script begins by defining three constants we need to use in order to make the ADO connection to the spreadsheet. After creating instances of the ADODB.Connection and ADODB.Recordset objects we then use this clunky-looking line of code to actually make the connection to the Excel spreadsheet:

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=C:\Scripts\Inventory.xls;" & _
        "Extended Properties=""Excel 8.0;HDR=Yes;"";"

Like we said, there’s no need to worry about things like Provider and Extended Properties, at least for now; instead, just note that you set the Data Source to the path to your Excel spreadsheet and let it go at that.

After connecting to the spreadsheet we then use this line of code to select all the records (that is, all the rows in the spreadsheet):

objRecordset.Open "Select * FROM [Sheet1$]", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

Notice that, in our Select statement, we select the worksheet where the data is stored; in this case, that’s Sheet1, so we specify [Sheet1$]. What if we needed to get information from a worksheet we renamed Computers? No problem; just change the query accordingly:

objRecordset.Open "Select * FROM [Computers$]", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

This gives us a recordset containing information about all the computers listed in the spreadsheet. That’s fine, except that we’re interested in only one computer: atl-fs-01. Therefore, we use these two lines of code to define a search criteria and to locate the record for atl-fs-01:

strSearchCriteria = "Name = 'atl-fs-01'"
objRecordSet.Find strSearchCriteria

When we execute the Find method the cursor gets positioned at the record for atl-fs-01. (Incidentally, an error will be generated if no computer named atl-fs-01 can be found in the database. Because of that you might want to implement error-handling of some kind somewhere in the script.) What do we do now that we’ve found the record we were looking for? Why, we do this, of course:

objRecordset("IPAddress") = "192.168.1.100"
objRecordset.Update

All we’re doing here is assigning a new value to the IPAddress property; note that we don’t need to worry about specifying which computer we’re giving the new value. Why not? That’s easy: the Find method has already located the computer for us. After assigning the new IP address we then call Update to write the changes to the database. Which, in this case, means changing the appropriate cell in the spreadsheet.

Will this really work? Judge for yourself:

Yes, we know: we thought we were crazy, too when we first suggested using ADO to update an Excel spreadsheet. But as you can see, ADO works pretty well, and with minimal effort on our part. In fact, if we could just use ADO to pound in all these nails we’d be home free. Anyone want to trade a database for a hammer?


How Can I Make Changes to and Then Re-Save an Existing Excel Spreadsheet?

Hey, Scripting Guy! How can I open an existing Excel spreadsheet, add some additional information to that spreadsheet, and then save my changes? Every time I call the SaveAs method a dialog box pops up asking me if I want to save my changes.

-- RW

Hey, RW. Generally speaking, you can avoid that dialog box by using the Save method rather than the SaveAs method. If you open an existing spreadsheet and make some changes to it, just call the Save method; that should save your document without displaying the confirmation dialog box. For example, here’s a sample script that opens the file C:\Scripts\Test.xls, writes the current date and time into cell A1, saves the file, and then quits:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1).Value = Now
objWorkbook.Save()
objExcel.Quit

In most cases, that should do the trick. However, it’s also true that the SaveAs method offers more options than the Save method. If you want to add password protection to the spreadsheet, create a backup version of the file, or save in another format, you’ll need to use SaveAs. Likewise, suppose that every morning you run a script that grabs some data and populates cells in a brand-new spreadsheet. Every morning you want to save that file as C:\Scripts\Daily_report.xls. Because this is a new spreadsheet (remember, you didn’t open the existing Daily_report.xls), you need to use the SaveAs method. And, when you do, a dialog box will pop up telling you that a file by that name already exists and asking you if you want to replace it.

So how do you get around these dialog boxes? The secret is to set the DisplayAlerts property (part of the Excel Application object) to FALSE. The DisplayAlerts property suppresses the display of dialog boxes and alert messages; instead of allowing you to choose whether to do X, Y, or Z, Excel automatically selects the default action for you. The default action for overwriting existing files is Yes. So when DisplayAlerts is set to False, the revised worksheet will be saved and you won’t be nagged and bothered by dialog boxes. Here’s a sample script that sets DisplayAlerts to FALSE and then uses the SaveAs method to save the changes:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1).Value = Now
objWorkbook.SaveAs("C:\Scripts\Test.xls")
objExcel.Quit

And just for the heck of it, here’s a script that creates a new worksheet and then overwrites an existing worksheet, again by setting DisplayAlerts to FALSE and by using SaveAs:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1).Value = Now
objWorkbook.SaveAs("C:\Scripts\Test.xls")
objExcel.Quit

If you’re having problems saving files, setting DisplayAlerts to FALSE will more than likely take care of things for you.


How Can I Make the First Character in a Cell Uppercase and All the Other Characters Lowercase?

Hey, Scripting Guy! In an Excel worksheet, how can I make the first character in each cell uppercase and the remaining letters in each cell lowercase?

-- DW

Hey, DW. To begin with, we hope you had a happy holiday season; you didn’t eat too much turkey, did you? As for the Scripting Guys, we all – oh, sorry. Silly us; we made the mistake of looking out the window and naturally assumed that we must be in the middle of winter. Although it’s past 9:00 AM as we write this column the skies aren’t grey; the skies are flat-out black. It’s dark, it’s cold (maybe 55 degrees, tops) and while it’s not raining at the moment, it will be soon. Ah, yes: Paris in the springtime, and Seattle in the fall.

Actually, and in all fairness, September is usually our best month for weather; the typical pattern around here is miserable June, mediocre July, so-so August, then a really nice, often-times hot September. In fact, as a season ticket holder for University of Washington football the Scripting Guy who writes this column has occasionally gotten a bit of a sunburn from sitting at the games, even though he spends a large portion of the summer months outdoors. For last week’s game, the Scripting Fan not only wore a coat, but took along a pair of gloves as well, just in case.

But are we going to sit here all day whining and complaining about the weather? Well, we’d planned on it. But then we figured you might be more interested in seeing how you can make each character in each cell in an Excel worksheet uppercase and all the remaining characters in the cell lowercase. So:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1,1) = "aBcDeF"
objWorksheet.Cells(1,2) = "ghHiJk"
objWorksheet.Cells(1,3) = "lMnOpQ"
objWorksheet.Cells(1,4) = "rStUvW"
Set objRange = objWorksheet.UsedRange

For Each objCell in objRange
    strValue = LCase(objCell.Value)
    intLength = Len(strValue)
    strleft = UCase(Left(strValue, 1))
    strRight = Right(strValue, intLength - 1)
    strValue = strleft & strRight
    objCell.Value = strValue
Next

And sure, we’ll explain how this works. After all, it’s not like we’ll be going outside to enjoy the sunshine, at least not until next July or so.

As you can see (and as we can see, as long as we turn on the lights in the office), we start out by creating an instance of the Excel.Application object and then setting the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We use the Workbooks.Add() method to add a new workbook to our instance of Excel, then use this line of code to bind to the first worksheet in the workbook:

Set objWorksheet = objWorkbook.Worksheets(1)

Got that? Good. What we need next is some text that we can reformat; the following four lines of code simply type an assortment of characters– some uppercase, some lowercase – into four of the cells in our worksheet:

objWorksheet.Cells(1,1) = "aBcDeF"
objWorksheet.Cells(1,2) = "ghHiJk"
objWorksheet.Cells(1,3) = "lMnOpQ"
objWorksheet.Cells(1,4) = "rStUvW"

In turn, that gives us a worksheet that looks like this:

Well, we suppose it’s kind of cute. But it’s not what we had in mind.

So maybe we should see what we can do to change this into the kind of spreadsheet that we did have in mind. To begin with, we use the UsedRange property to create a range object that encompasses all the cells in the spreadsheet that we’ve “used;” in this example, that’s cells A1 through D1. And that’s a good question: what would the UsedRange be if we typed something in cell A1 and something in cell D10? In that case, the UsedRange would consist of cells A1 through D10. (In other words, all the “intermediary” cells – B5, C7, etc. – would be part of the range, even though none of these cells actually contain data.) Think of it this way: suppose you click in cell A1, then hold down the Shift key and click in cell D10. Do that, and you’ll select cells A1 through D10. The UsedRange property performs the exact same feat.

So why do we even care about the UsedRange? Well, we need to loop through each cell in the worksheet and then individually adjust the letter casing in each cell; as far as we know there’s no way to apply this type of formatting to the worksheet as a whole, at least not in one fell swoop. The UsedRange property gives us a quick and easy way to return a collection of individual cells.

Which brings up another question: now that we have this collection of individual cells what do we do with it? This:

For Each objCell in objRange
strValue = LCase(objCell.Value)
intLength = Len(strValue)
strLeft = UCase(Left(strValue, 1))
strRight = Right(strValue, intLength - 1)
strValue = strLeft
& strRight
objCell.Value = strValue
Next

As you can see, we set up a For Each loop to loop through all the cells in the range. Inside that loop the first thing we do is grab the Value of the cell, use the LCase function to convert all the letters to lowercase, and then assign the value to a variable named strValue:

strValue = LCase(objCell.Value)

What does that mean? That means that, on the first go-round, strValue will be equal to this:

abcdef

Which isn’t quite what we want, either. But we’re getting closer.

Our next step is to use the Len function to determine the number of characters in the variable strValue. Why do we do that? In a moment we’re going to divide our string into two pieces: the first character, which we’ll convert to uppercase, and all the remaining characters, which we’ll leave in lowercase. In order to divide the string, and then recombine it, we need to know how many characters are in that string. The Len function tells us that the string abcdef has 6 characters, a value that gets assigned to the variable intLength:

intLength = Len(strValue)

That brings us to this fancy-looking line of code:

strLeft = UCase(Left(strValue, 1))

What we’re doing here, for starters, is using the Left function to grab the very first character in the string (the letter a); that’s what this portion of the code does:

Left(strValue, 1)

As soon as we’ve latched on to that character we use the UCase function to convert the value to uppercase; that value – A – is then assigned to the variable strLeft.

That gives us our initial uppercase character; now we simply need to combine that initial character with the remaining characters in the string. In order to get those remaining characters we use this line of code:

strRight = Right(strValue, intLength - 1)

Here we use the Right function to grab characters from the end of the string strValue (as opposed to the Left function, which grabs characters from the beginning of the string). How many characters do we want to grab? Well, we want all the characters except 1 (the first one), which can only mean one thing: we want to grab intLength – 1 characters. In this case, that means 6 – 1, or 5; thus we take the last five characters of the string, which just happen to be bcdef. Those 5 characters are then assigned to the variable strRight.

As soon as we have both the beginning and ending portions of our string we can then combine the two using this line of code:

strValue = strLeft & strRight

That makes strValue equal to Abcdef, which is exactly what we want it to be equal to. With that in mind, we next set the value of the cell to the value of strValue:

objCell.Value = strValue

And then we loop around and repeat the process with the next cell in the collection.

Will this work? Let’s put it this way: does it ever rain in Seattle? Here’s what our finished spreadsheet looks like:

That should do it, DW. As for everyone else, all we can say is this: Happy New Year!


How Can I Open All the Excel Spreadsheets in a Folder and Run a Specified Macro Found in Each of Those Spreadsheets?

Hey, Scripting Guy! How can I loop through a folder, open all the Excel files in that folder, then run a specified macro in each of those Excel files?

-- BR

Hey, BR. So, did you watch college football this past Saturday? In particular, did you watch as the Washington Huskies dismantled Boise State 24-10, snapping the Bronco’s nation’s-best winning streak at 14 games? If so, did you see Jake Locker score the Huskies’ first touchdown?

Oh, you say that you didn’t see Jake Locker score the Huskies’ first touchdown? That’s too bad; neither did the Scripting Guy who writes this column. Thanks to a comedy of errors (none of which seemed particularly funny at the time), the Scripting Guy who writes this column showed up just after the Locker scored his touchdown, and just as the Huskies were kicking the extra point.

And no, he was not pleased about that.

Because we know you’re all dying to heat the story, we’ll start by noting that the morning got off to a somewhat rocky start. When the Scripting Family (and Kyle) arrived at their favorite breakfast spot, the restaurant was a little busy; that mean they had to wait a few minutes before they could be seated. In turn, that also meant that service was a tad bit slower than usual. But that was OK; the Scripting Guy who writes this column wanted to be at the Park ‘n Ride by 11:15, and the Scripting Family (and Kyle) still managed to get there at 11:20.

Considering the fact that the game didn’t start until 12:30, and that Husky Stadium is less than a half hour’s drive away, the Scripting Family was in pretty good shape. Or at least they would have been had one of the special Game Day express buses actually shown up at the Park ‘n Ride. In 19 years of attending Husky football games, the Scripting Guy who writes this column has never had to wait more than a few minutes before he was on the bus on his way; usually there are 2 or 3 buses lined up ready to take on riders. This time there were 0 buses lined up, and the increasingly-agitated Husky fans had to wait 25 minutes before one finally arrived.

Note. Strangely enough, no, it didn’t help to have a representative from the bus company assuring everyone that buses ran every 10 minutes, even though more than 20 minutes had not passed and not a single bus had shown up. On the other hand, maybe the buses did run every 10 minutes; it’s just that they weren’t running between Husky Stadium and the Park ‘n Ride.

Eventually the bus arrived and the Scripting Family (and Kyle) climbed aboard. They took off, merged onto the freeway, and then came to a dead stop. Welcome to Seattle, the traffic jam capital of the world.

Note. Contrary to popular belief, the Scripting Guy who writes this column was not in favor of using a sub-atomic disintegrator pistol, like the Manmelter 3600 ZX, to dispose of all the cars that weren’t going to the game, but were simply clogging up the freeway. Instead, he’d be perfectly satisfied if all those people were simply arrested and thrown into jail.

And yes, the Scripting Editor is on vacation this week. How did you know?

After a slow and tedious crawl, the bus finally made it to its stop a few blocks from Husky Stadium. Taking into account wind velocity, the rotation of the earth, and the fact that games scheduled to start at 12:30 hardly ever start exactly at 12:30, the Scripting Guy who writes this column calculated that the Scripting Crew still had an outside chance at getting to their seats before kickoff. But then came the final indignity: as they hurried towards the stadium, the Scripting Son suddenly said, “Hey, look, Dad: the drawbridge is going up.”

Sure enough, the drawbridge spanning the Montlake Cut was going up, slowly and tediously, the better to let some dumb sailboat pass underneath. How did the Scripting Guy who writes this column handle this turn of events? Let’s put it this way: it’s probably a good thing that he didn’t have a Manmelter 3600 ZX on him.

Anyway, if you’re wondering how the Scripting Guy who writes this column – who considers himself as big a fan of UW football as there is – managed to miss the Huskies’ first touchdown in their biggest win in years, well, now you know: between the crowded restaurant, the bus that runs “every 10 minutes,” the Seattle-area traffic, and the Montlake Cut drawbridge, well ….. And if you’re wondering how you can write a script that loops through a folder, opens all the Excel files in that folder, then runs a specified macro in each of those Excel files, well, now you know that, too:

strComputer = "tvsfrank"

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

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

Set objExcel = CreateObject("Excel.Application", strComputer)
objExcel.DisplayAlerts = False

For Each objFile In colFileList
    If objFile.Extension = "xls" Then
        Set objWorkbook = objExcel.Workbooks.Open(objFile.Name)
        Set objWorksheet = objWorkbook.Worksheets(1)

        objExcel.Run("BoldfaceHeadings")

  objWorkbook.SaveAs objFile.Name
        objWorkbook.Close
    End If
Next

objExcel.Quit

Good observation: this script does look a little different than our usual WMI script, doesn’t it? As everyone probably knows by now, any time we write a script that uses WMI (and, in this script, we use WMI to retrieve a collection of all the files found in the folder C:\Test) we invariably start things out with this line of code:

strComputer = "."

Why? Well, in our WMI scripts we typically assign the name of the computer we want to run the script against to the variable strComputer. Granted, a dot (.) might not look like a computer name, but, in WMI, at least, the dot is short for “the local computer.” We start our scripts with strComputer = "." because that allows us to post scripts that, without any additional coding, will run against the local computer, regardless of the actual name of that computer. We then point out that you can run that same script against a remote computer simply by assigning the name of that computer to the variable strComputer. You know, like this:

strComputer = "atl-ws-01"

So then why didn’t we start this script out by using strComputer = "."? Well, we wanted to create a script that – with minimal fuss – could run macros against Excel files found on the local computer or against Excel files found on a remote machine. As we noted, WMI uses the dot as a shorthand method for designating the local computer; for better or worse, however, Excel does not use the dot as a stand-in for the local computer. When we create an instance of Excel, we need to be able to specify which computer we want to run the script against. To do that, however, we need to specify the actual computer name, even if we’re working on the local computer.

Note. OK, that’s not entirely true. We could leave the computer name off altogether; in that case, Excel would default to the local machine. Like we said, however, we wanted a single script that –with minimal fuss – could work against either a remote computer or a local computer. Assigning an actual computer name to the variable strComputer was the easiest way to do that.

By the way, if you aren’t totally sure what were talking about here (which wouldn’t be a first for this column), well, don’t worry too much about it. Just assign a computer name to the variable strComputer (even if you want to work with spreadsheets found on the local computer) and everything will be fine.

After we make our connection to the WMI service, we then execute the following query in order to return a collection of all the files found in the folder C:\Test:

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

At this point, we’re ready to start running some macros.

Or at least we will be after we create an instance of the Excel.Application object:

Set objExcel = CreateObject("Excel.Application", strComputer)

You might have noticed that our call to the CreateObject method also looks a little different than it usually does. (Talk about Scripting Guys Gone Wild, huh?!?) As you can see, we pass CreateObject two parameters: Excel.Application (the ProgID of the object we want to create), and the variable strComputer. (Typically our CreateObject calls only include the ProgID.) When we pass CreateObject two parameters we’ll get a new instance of our object (in this case, Excel.Application); in addition, that instance will be created on the specified in the second parameter. Because we assigned the value atl-ws-01 to the variable strComputer, our instance of Excel is going to be created on the computer atl-ws-01. And that will happen regardless of whether atl-ws-01 is the local computer or a remote computer.

Note. We should probably add that this script will fail if Excel has not been installed on atl-ws-01. But you already knew that, didn’t you?

After we’ve created an instance of Excel, our next step is to set the application’s DisplayAlerts property to False; this tells Excel not to display any messages or dialog boxes when we go to save our file. If we don’t do this we’re going to get a message similar to this one when we try to save the file:

A file named C:\Test\Spreadsheet1.xls already exists in this location. Do you want to replace it?

If we set DisplayAlerts to False, Excel will suppress message boxes like that and, instead, use the default operation. In this case, the default operation is clicking the Yes button, as in, “Yes, I do want to replace this file.”

You might note as well that we didn’t bother setting Excel’s Visible property to True. If you want to, go ahead and add the following line of code to the script, right after the line that configures the DisplayAlerts property:

objExcel.Visible = False

Keep in mind, however, that Excel will appear onscreen only when it is being run locally; no matter what value you assign to the Visible property, Excel will always run in a hidden window any time you run this script against a remote computer. The script will work just fine, but you won’t see Excel pop up on your screen, and Excel won’t pop up on the remote computer’s screen either.

That’s a security measure, and there’s no way around it.

OK, now we’re ready to start running some macros. To begin with, we set up a For Each loop to run through all the files in the returned collection. Inside that loop, the first thing we do is check the value of the Extension property to make sure that we’re working with an Excel file, making sure that the Extension is just xls, without the period:

If objFile.Extension = "xls" Then

Note. In Office 2007 you’ll need to check for the file extension xlsm: macro-enabled spreadsheet.

Assuming that we do have an Excel speadsheet, we then use the Open method and the following line of code to open the first file in the collection:

Set objWorkbook = objExcel.Workbooks.Open(objFile.Name)

Note that we don’t have to know the path to the file; that’s what the Name property is for. We go ahead and bind to the first worksheet in the file, then use this line of code to run a macro named BoldfaceHeadings (needless to say, this macro must be available within each of the spreadsheets):

objExcel.Run("BoldfaceHeadings")

After the macro runs, we call the SaveAs method to save the newly-modified file, then call the Close method to close this particular workbook:

objWorkbook.SaveAs objFile.Name
objWorkbook.Close

And then we loop around and repeat this process with the next file in the collection. When we’re all done we exit the loop, call the Quit method to terminate our instance of Excel, and call it a day.

That should do it, BR. Incidentally, don’t worry about the Scripting Guy who writes this column; he – oh, you say you weren’t worrying about the Scripting Guy who writes this column? Well, that’s good, because he’s already taking steps to ensure that he’ll never be late for another football game: he’s hard at work on a machine that will enable him to stop time. That way he can stop time at, say, 12:25, take his seat at the stadium, and then start time up again.

Well, we’re not saying that creating a machine like that is going to be easy. But it’s still easier than getting up half an hour earlier on a Saturday morning. That’s never going to happen.


How Can I Open an Excel Workbook and Retrieve the Names of All the Worksheets?

Hey, Scripting Guy! How can I open an Excel workbook and retrieve the names of all the worksheets?

-- JG

Hey, JG. It’s the first day back after a three-day weekend, and this morning all the Scripting Guys stumbled into the office and just stared blankly into space. (All in all, pretty much business as usual.) Eventually one of us spoke up and said, “Didn’t we used to do some kind of daily question-and-answer column or something?” After much debate we realized that, yes, not only did we used to do this column, we still do this column. That meant we had to find a useful scripting question and provide an answer for it, hopefully without having to do much work.

To tell you the truth, JG, your question was a lifesaver: it’s a useful scripting task, it’s something a number of people have asked about, and, best of all, answering this question required very little effort on our part. You wanted a script that could open an Excel workbook and return the names of all the worksheets? All we had to do was toss this little baby together:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

For Each objWorksheet in objWorkbook.Worksheets
    Wscript.Echo objWorksheet.Name
Next

Nothing to it, right? We start off by creating an instance of the Excel.Application object; we then set the Visible property to true, to watch the excitement unfold. We then use the Open method to open the file C:\Scripts\Test.xls.

That’s pretty easy, but getting the names of all the worksheets is even easier. That’s because the Workbook object (which corresponds to a .xls file) includes a property named Worksheets; this property consists of a collection of all the worksheets found in the workbook. To determine the names of all the worksheets we merely set up a For Each loop that cycles through the worksheets collection and echoes back the name of each sheet. That’s what we do here:

For Each objWorksheet in objWorkbook.Worksheets
    Wscript.Echo objWorksheet.Name
Next

That’s it. You can now easily get the names of all the worksheets in a workbook, and we can go back to staring blankly into space, mentally calculating the number of days between now and July 4th, our next scheduled three-day weekend. In the meantime, you might want to check out the Office Space column for even more ways to script Microsoft Office.

P.S. Here’s a one line script that will tell you the number of days between now and the Fourth of July:

Wscript.Echo DateDiff("d", Date, #7/4/2005#)

No need to thank us; this happens to be one script we Scripting Guys actually use ourselves.


How Can I Password-Protect an Excel Spreadsheet?

Hey, Scripting Guy! In a previous column, you told us a couple different ways to save Excel spreadsheets. In that column, you said that one of the things you could do with the SaveAs method was password-protect a spreadsheet. However, you didn’t show us an example of this. How can I use a script to password-protect a spreadsheet?

-- MC

Hey, MC. Well, as we hinted at in the previous column you mentioned, any time you call the SaveAs method you can include password-protection as an optional parameter. To demonstrate, here’s a script that creates a new worksheet, writes the current date and time in cell A1, and then saves the worksheet as C:\Scripts\Test.xls. On top of that, it password-protects the spreadsheet, giving it the password %reTG54w:

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1).Value = Now
objWorkbook.SaveAs "C:\Scripts\Test.xls",,"%reTG54w"
objExcel.Quit

If you’ve done any scripting with Microsoft Excel, this is about as simple a script as you’ll ever write. The only “gotcha” to be aware of occurs in this line of code, where you actually save the file and password-protect it:

objWorkbook.SaveAs "C:\Scripts\Test.xls",,"%reTG54w"

Admittedly, there’s nothing fancy about this line of code; you just have to make sure the password (%reTG54w) is the third parameter passed to the SaveAs method. The first parameter is, of course, the file name. The second parameter is the file format. Because we’re using the default format, we don’t need to set a value for the second parameter; however, we do need to include a placeholder for that parameter. That’s what the back-to-back commas (,,) are for: they simply indicate that the value for the second parameter would go here if we actually had a value for the second parameter. By including this placeholder, the password becomes the third parameter, which is exactly what we want.

After you run this script, try to open the file C:\Scripts\Test.xls; you’ll be prompted to enter the password before the file will actually be opened. Incidentally, this will happen even if you try opening the file using a script. (Sorry, but using a script won’t allow you to bypass the password protection.) But can’t you specify the password when you open the file? Of course you can; that’s what happens with this script:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE

Set objWorkbook =
objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"%reTG54w")

Note that when opening a spreadsheet the password has to be the fifth parameter; thus we have the file name, three placeholders, and then the password. This can be a little confusing, to say the least, but here’s a rule of thumb: just put in one more comma than you have placeholders. In this example, we have three placeholders, so we insert four commas. If we had nine placeholders, we’d insert ten commas. And so on.

But what if you decide later on to remove the password? No problem: simply open the file and set the value of the Password property to an empty string. Here’s a script that does just that: it opens the spreadsheet, removes the password, and then uses the SaveAs method to re-save the file. After running this script, try to open this spreadsheet from within Windows Explorer; you should be able to do so without being prompted for a password.

Set objExcel =
CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE

Set objWorkbook =
objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"%reTG54w")
Set objWorksheet = objWorkbook.Worksheets(1)

objWorkbook.Password = ""

objWorkbook.SaveAs "C:\Scripts\Test.xls"
objExcel.Quit

What if you didn’t want to remove the password, but merely wanted to change it? In that case, just set the Password property to the new password rather than to an empty string. And before you ask, yes, we could have used the Save method here rather than SaveAs. We stuck with SaveAs simply to be consistent with the previous script. We’re also aware that “Consistency is the hobgoblin of little minds.” But, hey, what size minds would you expect the Scripting Guys to have?!?


How Can I Remove All the Formatting from an Excel Spreadsheet?

Hey, Scripting Guy! How can I remove all the formatting from an Excel spreadsheet?

-- MT

Hey, MT. You know, several years ago one of the Scripting Guys and his family went to the Blizzard Beach water park in Orlando. While there this Scripting Guy went down the Summit Plummet, reputed at the time to be the highest and fastest water slide in the world. What this Scripting Guy (who was dared by his son to go down the slide, leaving him little choice in the matter) remembers most about the experience is that he stood in line for about 45 minutes and then listened to a minute or so of safety instructions, all for the chance to experience a ride that maybe lasted 3 or 4 seconds, max.

On the bright side, though, no Scripting Son could call him chicken. Or at least not when it came to water slides.

We were reminded of this kind of experience – a huge buildup for something that was over before you knew it – as we sat down to write a script that addressed your question. That’s because it didn’t make much sense to show you how to clear all the formatting from a spreadsheet unless we had a spreadsheet that included several different types of formatting. Therefore we wrote a bunch of code to create a formatted spreadsheet. And then, when the time came, we found out that we needed just one line of code to clear all that formatting.

Talk about anti-climactic.

Here’s what the script looks like:

Const xlHAlignCenter = -4108

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

For i = 1 to 14
    objExcel.Cells(i, 1).Value = i
    objExcel.Cells(i, 2).Interior.ColorIndex = i
Next

Set objRange = objWorksheet.UsedRange
objRange.HorizontalAlignment = xlHAlignCenter
objRange.Font.Bold = True

Wscript.Sleep 5000

objRange.ClearFormats

We should note that we’re going to ignore most of the lines of code in this script; that’s because we’ve included those lines simply to give us a formatted Excel spreadsheet to work with. If you’d like to know more about scripting with Microsoft Excel you might want to take a peek at the Office Space archive; there you’ll find links to all sorts of articles that show you how to script with Excel.

However, we will at least give you a brief summary of what the script does. To begin with, we create an instance of Excel and then create a new workbook and a new worksheet. We add numbers and change the background color of a bunch of cells on that worksheet, then run smack-dab into this line of code:

Set objRange = objWorksheet.UsedRange

Our goal today is to write a script that removes all the formatting from the entire spreadsheet; as you might expect, the easiest way to do that is to first select the entire spreadsheet. That’s what the UsedRange does: it gives us a Range object that encompasses the, well, used portion of the spreadsheet. For example, suppose we have data in cell A1 and data in cell G37; the used range consists of cells A1 and G37, as well as all the cells in between. Obviously a quick and easy way to grab all the “used” cells.

Once we have this range we take advantage of it right away, using these two lines of code to boldface and center all the text on the spreadsheet:

objRange.HorizontalAlignment = xlHAlignCenter
objRange.Font.Bold = True

That just adds a little more formatting to the mix. Next we pause the script for 5 seconds (5,000 milliseconds). We don’t have to do that, but it gives you a chance to gaze at all the pretty formatting, right before the script comes in and removes it all.

And so how do we remove all that formatting? Here’s how:

objRange.ClearFormats

See: we weren’t kidding: one line of code. Using our Range object (the one that encompasses the used range) all we have to do is call the ClearFormats method and all formatting will be removed. Hey, we told you it was a bit anti-climactic. On the bright side, though, if you run this script no one will be able to call you chicken; at least not when it comes to removing all the formatting from an Excel spreadsheet.


How Can I Remove the Password When Opening an Excel Spreadsheet?

Hey, Scripting Guy! How can I remove the password when opening an Excel spreadsheet?

-- JE

Hey, JE. You know, it’s finally happened: after two-and-a-half years of writing this column we’ve finally run out of things to talk about. The “heartbeat sensor” that can tell you whether someone is hiding in the backseat of your car? We talked about that a week or so ago. The exploits of baseball legend Frank “Home Run” Baker? Old news; we covered that a long time ago. The origins of Grandparents Day? You know what they say: been there, done that.

In fact, as near as we can tell we’ve now covered every subject we could possibly cover. (Well, we suppose we could go over that; however, the Scripting Editor doesn’t really like us talking about her broomstick. Besides, we’ve already mentioned that in a column.) The truth is, there’s really nothing left for us to talk about. Try us again next week, or maybe next month; maybe we’ll have something for you by then.

Although, come to think of, there is one thing we haven’t tried: we could simply answer the question without rambling on about, say, Scripting Guys dying while riding an exercise bike. That’s a bit out of character for this column, but we suppose could give it a try. Granted, we wouldn’t want to make a habit of that; after all, why would you read a daily scripting column if all the column ever talked about was scripting-related stuff? But what the heck; the Scripting Guys will try anything once:

Set objExcel = CreateObject("Excel.Application")

objExcel.Visible = TRUE
objExcel.DisplayAlerts = FALSE

Set objWorkbook =
objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"L$6tg4HHE")

objWorkbook.Password = ""
objWorkbook.SaveAs
"C:\Scripts\Test.xls"

You know which commercial on TV drives us absolutely crazy? It’s the one where – oh, sorry; force of habit. We said we were going to just talk about scripting, didn’t we? OK. Well, in that case, what we have here is an Excel spreadsheet (C:\Scripts\Test.xls) that has been password-protected. (We’re assuming only the password to open the file has been set; if the password to modify the file has also been set you’ll still need to enter that one.) In order to open the spreadsheet you need to supply the password: L$6tg4HHE. (And yes, that is the Scripting Editor’s middle name. Thanks for noticing!) We need a script that can do two things: open the spreadsheet (which will require us to supply the password), and then remove the password protection. Can we do that? Let’s find out.

First things first, however. So what’s the deal with the weather in Cleveland, huh? All that snow, in April? Yuck. On the bright side, however, having 4 games snowed out helped the Seattle Mariners actually go several days without a loss; needless to say, as soon as they had to start playing again they got hammered by–

Sorry; this is harder than we thought it would be. Time to focus on the task at hand. As you can see, we begin by creating an instance of the Excel.Application object. We then set the Visible property to True and the DisplayAlerts property to False. Setting the Visible property to True simply ensures that we’ll be able to view our instance of Excel on screen. Meanwhile, setting DisplayAlerts to False suppresses the display of Excel message boxes and warnings while the script is running. Do we really need to do that? Well, maybe not. However, when we go to save the spreadsheet Excel will, by default, notify us that a copy of Test.xls already exists and ask us if we really want to overwrite that existing copy. We don’t want to deal with that message box, and setting DisplayAlerts to False ensures that we don’t have to.

Once Excel is up and running (and properly configured) we next use this line of code to open the file C:\Scripts\Test.xls:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"L$6tg4HHE")

Notice what we’re doing here. Here we’re calling the Workbooks.Open method, followed by the complete path to the file we want to open; that should be pretty straightforward. We then have a series of commas with nothing in between them, followed by the spreadsheet password:

,,,, "L$6tg4HHE"

What’s the deal with that? Well, the Open method accepts a number of parameters, most of which are optional. We aren’t interested in parameters 2 through 4 (UpdateLinks, ReadOnly, and Format); however, we are interested in parameter 5 (Password). So then why don’t we just list the two parameters we’re interested in, like so:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls","L$6tg4HHE")

You’re absolutely right: with that line of code the password shows up in the slot reserved for parameter 2; as you might expect, that’s not going to work. Instead, the password has to be the fifth parameter provided to the Open method. And the only way to make sure the password shows up as the fifth parameter is to include blank parameters between the file path and the password. Hence a set of commas with nothing in between.

Boy, this explaining stuff is hard work. No wonder we don’t do it very often!

Assuming we’ve provided the correct password we’ll have now accomplished step 1 in our two-part process: we’ll have opened Test.xls. All that’s left now is to remove the password protection. To accomplish that feat we first set the value of the Workbook’s Password property to nothing:

objWorkbook.Password = ""

And then, to make sure that the change is applied and the password is removed, we call the SaveAs method, taking care to save the file under its existing file name and file path:

objWorkbook.SaveAs "C:\Scripts\Test.xls"

What will that do for us? Well, suppose we go ahead and close Test.xls, then try to reopen it. Will we need to supply the password in order to open the spreadsheet? Come on; where’s your faith in the Scripting Guys?

OK, good point. But at least you’ll be able to open the spreadsheet without supplying a password.

So it looks like we’re done, doesn’t it? We have to admit that this was an interesting exercise; nevertheless, it’s probably just a one-time thing. After all, the Scripting Guys all lead such rich and fulfilling lives that something interesting – and worth talking about – is bound to happen to them sooner or later.

OK, another good point. On the other hand, the fact that we don’t have anything worth talking about has never stopped us before, has it? Tune it tomorrow for another thrilling edition of Hey, Scripting Guy!


How Can I Replace Numeric Values in an Excel Spreadsheet?

Hey, Scripting Guy! We have a spreadsheet where people enter a bunch of data. In order for this data to be used in another application, none of the values can exceed 999; if we have a value like 1234, that’s supposed to be entered as 999, the maximum-allowed value. However, people have a tendency to enter the value as 1234; that means that we can’t import this spreadsheet into our other application until we find this illegal value and change it to 999. How can I write a spreadsheet that will do this fix-up work for me?

-- LK

Hey, LK. You know, in addition to taking a vow of poverty, the Scripting Guy who writes this column has also vowed to help anyone who asks him for help. (OK, so, technically, the Scripting Guy who writes this column didn’t actually take a vow of poverty; instead, he sort of had poverty forced upon him. That’s what happens when you have a 17-year-old son, especially one who plays baseball.) Because of his latter vow, the Scripting Guy who writes this column will try to help you; to be honest, however, he’s a little concerned about anyone who comes to him requesting help with “fix-up work.” Fix-up work?!? You obviously haven’t seen the Scripting House lately, have you?

Note. OK, so, in all fairness, no one has seen the Scripting House lately; with the Scripting Family having been out of town for a couple weeks, the Scripting House is now hidden behind a jungle of uncut grass and overgrown weeds.

Or at least we assume the Scripting House is still back there behind the jungle of uncut grass and overgrown weeds. It’s kind of hard to tell.

At any rate, fix-up work is definitely not what the Scripting Guy who writes this column does best. But maybe that isn’t his fault. Instead, maybe that’s because there isn’t much need around the Scripting House for a script that goes through an Excel spreadsheet and changes any cell values greater than 999 to the maximum-allowed value of, well, 999:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Set objRange = objWorksheet.UsedRange

For Each objCell in objRange
    If IsNumeric(objCell.Value) Then
        If objCell.Value > 999 Then
            objCell.Value = 999
        End If
    End If
Next

As you can see, there really isn’t much to this script; as it turns out, it’s surprisingly easy to zip through a spreadsheet, find any values that are greater than 999, and then change those values to 999. For starters, we create an instance of the Excel.Application object and then set the Visible property to True; that gives us a running instance of Excel that we can see on screen. We then use these two lines of code to open the file C:\Scripts\Test.xls, and to bind to the first worksheet in that file:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Needless to say, there’s nothing too terribly hard about any of that.

Of course, we know what you’re thinking: “Sure, there’s nothing terribly hard about any of that. But now we have to figure out how to do a search-and-replace operation that can find – and then replace – any values greater than 999. How hard is that going to be?”

Well, to tell you the truth, that might be very hard; we don’t know for sure. That’s because we aren’t going to do a true search-and-replace operation (that is, we aren’t going to use Excel’s Replace method). Although we might (or might not) be able to use the Replace method to find and replace these values, we opted for a much easier approach: we’re simply going to grab a collection of all the cells in the worksheet, check the value of each cell individually, and then, if needed, replace that value with 999. As you’re about to see, there’s nothing too terribly hard about any of that, either.

What’s that? You don’t believe us? You still think that this search and replace thing is going to be hard? Let’s see if we can put your mind at ease by explaining exactly how this all works.

To begin with, we use this line of code and the UsedRange property to grab a collection of all the cells in the worksheet. Nothing too terribly hard there:

Set objRange = objWorksheet.UsedRange

Note. OK, we need to clarify this a little. The UsedRange property doesn’t necessarily return all the cells in the spreadsheet; instead, it returns the range of cells that actually contain data. For example, suppose you have data in cell A1 and another piece of data in cell D5. The UsedRange property returns cells A1 through D5; that is, the first cell that has data in it, the last cell that has data in it, and any cells in between.

It goes without saying that the UsedRange property is a very handy little property to know about.

When you create an instance of Excel’s Range object (which is what happens when we reference the UsedRange property) we get back a collection of all the cells in that range. In turn, that means we can access each of the individual cells in the range simply by setting up a For Each loop to walk through all the items (cells) in the collection. And – surprise, surprise – that’s exactly what we do next:

For Each objCell in objRange

Inside this For Each loop we do a couple of things. First, we need to determine whether the value in a given cell is a numeric value; if it’s not, we’re going to run into problems (i.e., script-crashing problems) when we try to figure out if that value is greater than 999. Therefore, to avoid problems, we use VBScript’s IsNumeric function to determine whether or not the value in the first cell happens to be a number:

If IsNumeric(objCell.Value) Then

If IsNumeric returns False that means we’re dealing with a string, a date, or some other non-numeric piece of information; in that case, we return to the top of the loop and repeat the process with the next cell in the collection. Let’s assume, however, that IsNumeric returns True; that means that we are dealing with a numeric value. In that case, we then use this line of code to determine if that value is greater than 999:

If objCell.Value > 999 Then

And what if the value is greater than 999? No problem; we simply assign a new value (999) to the cell:

objCell.Value = 999

From there we go back to the top of the loop and try again with the next cell in the collection. By the time we’ve looped through the entire collection of cells any values greater than 999 will have been replaced with 999.

Which, fortuitously enough, is just what we wanted them to be replaced with.

That should do the trick, LK. If for some reason it doesn’t do the trick, just let us know and we’ll write you a new script. In fact, we might write you a new script anyway. In fact, we might write everyone a new script, as long as that means we can stay away from the Scripting Jungle.

Note. How bad could the Scripting Jungle really be? Let’s put it this way: eyewitnesses claim that there are soldiers living in that jungle who still don’t know that World War II is over. To be honest, that’s a little hard to believe.

Although, come to think of it, we haven’t seen the Scripting Son since we returned home from Italy. Hmmmm, you don’t suppose ….


How Can I Replace Text in an Excel Spreadsheet?

Hey, Scripting Guy! How can I replace text in an Excel spreadsheet?

-- PD

Hey, PD. We’ll talk about replacing text in an Excel spreadsheet in just a moment. But first, let’s address the question that’s on everyone’s mind: what is the Scripting Guy who writes this column going to do today as his Italian vacation continues? Well, to be honest, the term “everyone” actually includes the Scripting Guy who writes this column: he has no idea what he’s going to do today, either. (Ah, the joys of trying to get a group of family members to agree on something!)

It’s possible that everyone will board the train this morning and head for the city of Pompeii, the city that was literally wiped from the face of the earth by the cataclysmic eruption of Mt. Vesuvius back in August of 79 AD. As most of you know, the destruction of Pompeii ranks as one of the world’s great tragedies, alongside the Great Chinese Earthquake of 1556, the sinking of the Titanic (1912), and the release of Windows ME (1999).

Note to our fellow Microsoft employees. Come on, guys, loosen up a little: we’re just kidding. After all, everyone knows that Windows ME was a truly great operating system, one that introduced a huge number of innovative new features to the computer world.

Like what? Well, like – sorry; gotta run. We have a train to catch.

At any rate, the Scripting Guy who writes this column doesn’t know what he’s going to do today, other than moderate a debate about what he and the rest of the crew are going to do today. On the off-chance that no one out there is interested in listening the Scripting Family discuss what they do or don’t want to do today, here’s a script that searches a worksheet in an Excel spreadsheet and (per PD’s request) replaces any instances of the file path C:\Test\Image.jpg with the path C:\Backup\Image.jpg. That should tide everyone over until the Scripting Family reaches a consensus.

Wait; scratch that: there’s a good chance that the Scripting Family will never reach a consensus. But here’s the Excel search-and-replace script anyway:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Set objRange = objWorksheet.UsedRange

objRange.Replace "C:\Test\Image.jpg", "C:\Backup\Image.jpg"

You say you’d like to know how this script works? What a coincidence; so would we. Let’s see if we can go through it line-by-line and come up with the answer.

As you can see, the script starts out simple enough: all we do is create an instance of the Excel.Application object and then set the Visible property to True. Needless to say, that gives us a running instance of Microsoft Excel that we can see onscreen. We next call the Open method to open the file C:\Scripts\Test.xls, then use the following line of code to bind to the first worksheet in that file:

Set objWorksheet = objWorkbook.Worksheets(1)

Wow; that was simple, wasn’t it?

Amazingly enough, the rest of the script is equally simple. When you perform a search-and-replace operation in Excel, you must specify the portion of the worksheet (i.e., the “range”) in which to carry out this task. Because we want to replace text throughout the entire worksheet that means that our range must encompass the entire worksheet (that is, each and every cell that has data in it). Admittedly, that sounds pretty darn complicated: how are we supposed to know which cells have data in them and which ones don’t? To tell you the truth, we have no idea how we’re supposed to do that. Fortunately, though, it doesn’t matter; that’s because we can use Excel’s UsedRange property to create an instance of the Range object that automatically includes every cell that has data in it:

Set objRange = objWorksheet.UsedRange

If only deciding what to do today was that easy, eh?

After we’ve defined the range we can then call the Replace method, passing this method two parameters: the text we want to search for (C:\Test\Image.jpg) and the replacement text (C:\Backup\Image.jpg). That’s what this line of code is for:

objRange.Replace "C:\Test\Image.jpg", "C:\Backup\Image.jpg"

Believe it or not, that’s all we have to do.

Well, that and figure out today’s agenda, of course. We know what you’re thinking: “Didn’t the Scripting Guy who writes this column carefully plan out an agenda (plus a backup agenda) for the entire vacation, just to ensure that problems like this didn’t happen?” Of course he did. And yet he’s saying that this didn’t prevent the problem from happening anyway? Let’s put it this way: if you’ve ever gone on a family vacation, then you understand why having an agenda and a backup agenda doesn’t really mean much.

Oh, well: ci vediamo domani. (Which, we think, means “See you tomorrow.” If it doesn’t, well, we’ll see you tomorrow anyway.)


How Can I Save a Single Excel Worksheet to a CSV File?

Hey, Scripting Guy! I’d like to be able to open an Excel spreadsheet, get all the information off one of the worksheets, and then save that worksheet data to a comma-separated values file. How can I do that?

-- SS

Hey, SS. You know, you just have to love Excel. Yes, we know, to be “cool” these days you’re supposed to say that you don’t use Excel, that you use one of those Office wannabes instead. But we’d like to see one of those Office wannabes carry out a task like this, and in just a few lines of code to boot.

Now, to be honest, when we first read the question we weren’t sure how easy this was going to be. Turns out it was this easy:

Const
xlCSV = 6

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Testsheet.xls")
objExcel.DisplayAlerts = FALSE
objExcel.Visible = TRUE

Set objWorksheet = objWorkbook.Worksheets("Sheet1")
objWorksheet.SaveAs "c:\scripts\test.csv", xlCSV

objExcel.Quit

That’s it; that’s the whole script right there. We begin by creating a constant named xlCSV and setting the value to 6; we’ll use this constant later on to specify the file format for our new file. We then create an instance of the Excel.Application object and use the Workbooks.Open method to open the spreadsheet C:\Scripts\Testsheet.xls. (We also set the Visible property to TRUE, but that’s just so you can see what’s going on. If you’d prefer to have Excel run in an invisible window, then just leave this line out.)

Oh, and we have one other line of code that might be of interest:

objExcel.DisplayAlerts = FALSE

This line of code tells Excel not to display any message boxes, but to instead go with the default value any time it has a question. Why do we bother with this? Well, we’re going to save worksheet information to a CSV (comma-separated values) file. If that file already exists Excel will pop up a message box asking if we want to overwrite the existing file. For this script we’re assuming that we always want to overwrite the existing file. By setting the DisplayAlerts property to FALSE we suppress the display of that message box and tell Excel to go ahead and carry out the default operation. In this case, the default operation just happens to be, “Go ahead and overwrite any existing file.”

After we have Excel up and running it takes just two lines of code to save worksheet data to a CSV file. First, we bind to the desired worksheet (in this case, the worksheet named “Sheet1”):

Set objWorksheet = objWorkbook.Worksheets("Sheet1")

After that we simply call the SaveAs method, passing two parameters: the path to CSV file (C:\Scripts\Test.csv) and the format for the new file. Because we want this saved as a CSV file, we use the xlCSV constant we defined way back when. Thus:

objWorksheet.SaveAs "c:\scripts\test.csv", xlCSV

All we have to do now is call the Quit method to terminate our Excel instance and we’re done. Now try and tell us that one of those Excel wannabes is as good as the real thing!

Shameless Self-Promotion: If you’re interested in scripting Microsoft Office applications, be sure and check out the Scripting for Microsoft Office center. Among other things, the center features the column Office Space, which offers up new Microsoft Office scripting tips every Tuesday and Thursday.


How Can I Save an Excel Chart as a Picture?

Hey, Scripting Guy! How can I save an Excel chart as a picture?

-- JB

Hey, JB. You know, yesterday was a good day for the Scripting Guy who writes this column. If the phone rings at the Scripting House and if it’s for the Scripting Guy who writes this column, well, it’s never really for the Scripting Guy who writes this column; instead, it’s someone trying to sell him something, or someone wondering if they can count on him to donate to their annual fund-raising drive. (Not being particularly smart, the Scripting Guy who writes this column always thought annual meant “once – and only once – a year.”) The same is true for the mail: if something is addressed to the Scripting Guy who writes this column (as opposed to Occupant or Current Resident) then it’s a bill.

Either that, or a restraining order from one of the other Scripting Guys.

Yesterday, however, was different: yesterday the Scripting Guy who writes this column actually got a letter addressed specifically to him. As it turns out, a new European day spa was opening in town, and they were offering the Scripting Guy who writes this column a chance to get a complete makeover, at 50% off the regular price. (Editor’s Note: Obviously they’ve never seen the Scripting Guy who writes his column, or they’d never have offered a discount on a complete makeover. They would have charged extra.)

Now, admittedly, the Scripting Guy who writes this column thought these folks were being a bit naïve: after all, if they really do think they can give him a complete makeover in just one day, well …. Still, he was intrigued by the offer. What we’re about to say might come as a shock to those of you who read this column regularly, and have understandably pictured the Scripting Guy who writes this column as very cultured and refined. But it’s true: the Scripting Guy who writes this column has never been to a European day spa. Never. “Maybe it’s time to give this a try,” he thought.

With that in mind he perused the different offerings, and immediately discovered that he was way out of his league. For example, he wasn’t totally sure about the Full Body Chocolate Raspberry Massage, a massage that promised to give him “skin like you’ve never had before.” Usually if something includes the tem Chocolate Raspberry the Scripting Guy who writes this column will take at least one. This was a little different, however. Plus there was the promise about skin like he’s never had before; what exactly did that mean? After all, the Scripting Guy who writes this column has never had skin like an iguana or a pterodactyl before.

Which isn’t to say that having skin like an iguana or pterodactyl might not be an improvement over the skin he currently has.

Even more confusing was the Heated Swiss Chocolate Milkshake Foot Bath and Beverage. Philosophically, the Scripting Guy who writes this column had difficulty figuring out how you could heat a milkshake and still have a milkshake. Far more important, he wasn’t sure whether this offer consisted of one item or two. Do you get a foot bath and then, on top of the foot bath, get a beverage as well? Or was this one item, a combination foot bath and beverage? To tell you the truth, the Scripting Guy who writes this column didn’t really want to know.

As it turned out, though, it was all a moot point: even at 50% off a day at the European day spa cost $299. For $299, the Scripting Guy who writes those column can make 100 or so trips to Dairy Queen and heat his own milkshakes.

Besides, he doesn’t have time to lounge around at a European day spa; he has work to do:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook
= objExcel.Workbooks.Add()
Set objWorksheet
= objWorkbook.Worksheets(1)

objWorksheet.Cells(1,1) = "Operating System"
objWorksheet.Cells(2,1) = "Windows Server 2003"
objWorksheet.Cells(3,1) = "Windows XP"
objWorksheet.Cells(4,1) = "Windows 2000"
objWorksheet.Cells(5,1) = "Windows NT 4.0"
objWorksheet.Cells(6,1) = "Other"

objWorksheet.Cells(1,2) = "Number of Computers"
objWorksheet.Cells(2,2) = 145
objWorksheet.Cells(3,2) = 987
objWorksheet.Cells(4,2) = 611
objWorksheet.Cells(5,2) = 41
objWorksheet.Cells(6,2) = 56

Set objRange
= objWorksheet.UsedRange
objRange.Select

Set colCharts
= objExcel.Charts
colCharts.Add()

Set objChart
= colCharts(1)
objChart.Activate

objChart.Export "C:\Scripts\Test.jpg",
"JPG"

Well, OK: not too much work. We’re actually not going to explain the details behind creating a chart in Microsoft Excel; that’s because we have entire series of articles devoted to that topic. Instead, we’ll just briefly note that our script starts out by creating an instance of the Excel.Application object and then sets the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We use the Add method to add a new, blank workbook to our running instance of Excel, then use this line of code to bind to the first worksheet in that workbook:

Set objWorksheet = objWorkbook.Worksheets(1)

That’s all pretty easy. What follows next are several lines of code designed to add data to the worksheet. When that’s done, we select all the data (using the UsedRange property and the Select method), then use these two lines of code to create a chart:

Set colCharts = objExcel.Charts
colCharts.Add()

The resulting chart will look a little like this:

And no, sorry, it’s just a plain old chart. We haven’t yet figured out how to make a combination chart and foot bath, although we plan to continue the research.

Now that we have a chart it’s time to export said chart as a picture. In order to do that we need to create an object reference to the chart itself. That’s something we can do by binding to the chart (which, of course, is the first – and only – chart in our Charts collection) and then using the Activate method to make this the active chart:

Set objChart = colCharts(1)
objChart.Activate

At that point, saving the chart as a picture is as easy as calling the Export method:

objChart.Export "C:\Scripts\Test.jpg", "JPG"

As you can see, all we do here is call the Export method, handing that method two parameters: the complete path to the resulting picture file, and the type of picture file we’re creating. In this example, we’re creating a file named C:\Scripts\Test.jpg and we want the resulting picture to be a .JPG file; hence the “JPG” parameter. Can we use other graphic types? You bet; for example, we know for sure that charts can be saved as .GIF files or as .PNG files. Can charts be saved in other formats? Probably, but we didn’t try any other format. We were saving that to help pass the time while getting our Chocolate Raspberry massage.

Of course, we know the question that’s now on everyone’s mind: will the Scripting Guys be giving away Heated Swiss Chocolate Milkshake Foot Baths and Beverages as part of the 2007 Winter Scripting Games? Just a second; we’ll check on that ….

No, sorry; we won’t be. However, we are giving away 250 Dr. Scripto Bobblehead dolls. On top of that, the good folks at Manning Publications are giving away 20 copies of Bruce Payette’s new book Windows PowerShell in Action. Is there a better and more definitive book on Windows PowerShell? Well, if there is, we haven’t seen it. This is a prize worth winning.

And how can you win one of these great prizes? Well, the requirements are pretty steep: you have to enter at least one event in the Scripting Games. That’s it. Enter an event (you don’t even have to successfully complete the event!) and you’ll automatically be entered in a random drawing for one of these great prizes. We’re sorry that we can’t give Heated Swiss Chocolate Milkshake Foot Baths and Beverages. But we think the bobbleheads and Windows PowerShell books make for pretty good consolation prizes.


How Can I Save an Excel Spreadsheet, and Then Save a Copy as HTML?

Hey, Scripting Guy! How can I save an Excel spreadsheet, and then save a copy of that spreadsheet as HTML?

-- TH

Hey, TH. You know, it’s interesting that you used the word “save” in your question. After all, the Scripting Guy who writes this column (who’s much lazier – um, much busier than usual) just glanced at the clock and realized, “Uh-oh: I haven’t written tomorrow’s column yet.” There were only two things that could save him: 1) the end of the world (though no doubt even then his editor would want to know where the column was) or, 2) a question that he could answer without having to do any research. TH, you saved the Scripting Guys!

Well, at least this Scripting Guy.

In return for your heroic action we’d like to give you something near and dear to our hearts: a script that can save an Excel spreadsheet, and then save a copy of that spreadsheet as HTML. That’s OK; don’t bother to hide your tears. This is a pretty emotional moment for us, too.

Here’s your script:

Const
xlHTML = 44

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

objExcel.DisplayAlerts = False

objExcel.Cells(1, 1).Value = "A"
objExcel.Cells(1, 2).Value = "B"
objExcel.Cells(1, 3).Value = "C"
objExcel.Cells(1, 4).Value = "D"

objWorkbook.SaveAs "C:\Scripts\Test.xls"
objWorkbook.SaveAs "C:\Scripts\Test.htm", xlHTML

You can see why – facing a deadline and almost certain doom – we decided to answer this question: there’s hardly anything at all to the script. We start off by defining a constant named xlHTML and setting the value to 44; we’ll use this constant to tell Excel that we want to save a copy of the spreadsheet as HTML. We then have four lines of code that create a visible instance of the Excel.Application object and then create a new workbook and a new worksheet for us to play around with.

And then, just for the heck of it, we threw in this line of code:

objExcel.DisplayAlerts = False

Why did we do this? Well, first we’re creating a brand-new worksheet with this script, and then we’re going to save it as C:\Scripts\Test.xls (and then as C:\Scripts\Test.htm). That’s fine, unless a file named Test.xls or Test.htm already exists. In that case Excel won’t automatically overwrite the existing file; instead, it will pop up a dialog box asking if you’re sure that you want to overwrite that file. The dialog box will sit there on screen until you click Yes, No, or Cancel; needless to say, that means your script will also sit there – doing nothing – until you click Yes, No, or Cancel.

Unless, that is, you set DisplayAlerts to False. When DisplayAlerts is set to False Excel will not display any message boxes; instead, it automatically uses the default option of any message boxes it would otherwise show. For example, the default option for overwriting an existing file is Yes; therefore, instead of displaying a message box and waiting for you to answer, the script simple goes with the default value (yes) and overwrites the file. Make sense?

And, no, we didn’t have to use this option here. However, we get a lot of questions about this (“How can I stop Excel from asking me if I want to overwrite a file?”), and this seemed like a sneaky way to answer two questions for the price of one. Although, now that we think about it, it would have been smarter to save that question for tomorrow’s column, putting us ahead of the game for once. Dang.

After configuring the DisplayAlerts property we have some code that simply writes the values A, B, C, and D into four cells of the spreadsheet. That brings us to the last two lines of the script:

objWorkbook.SaveAs "C:\Scripts\Test.xls"
objWorkbook.SaveAs "C:\Scripts\Test.htm", xlHTML

In the first line we’re saving our spreadsheet as a spreadsheet; that’s done by calling the SaveAs method and including just one parameter, the full path to the new file. In the second line we’re saving a copy of the spreadsheet as an HTML file. Note that we also use the SaveAs method and that we again specify the full path to the new file (this time with a .htm file extension). However, we also add a second parameter, the constant xlHTML; as we noted earlier, this tells Excel the file format for the new file. Could we save a file in a format other than HTML? Of course; hey, variety is the spice of life, right? Here are a just few of the constants and their corresponding values that we can use with the SaveAs method:

Constant | Value
xlCSV | 6
xlCSVMac | 22
xlCSVMSDOS | 24
xlCSVWindows | 23
xlDBF4 | 11
xlExcel9795 | 43
xlHtml | 44
xlSYLK | 2
xlTemplate | 17
xlTextWindows | 20
xlXMLData | 47
xlXMLSpreadsheet | 46

For a complete list, take a peek at the Excel VBA Language Reference on MSDN. (Look for the xlFileFormat enumeration.)

So have the Scripting Guys learned their lesson from this close escape? Will they now do a better job of getting their columns in before the deadline? You bet they will; in fact, you can count on it.

Just don’t count on it.


How Can I Select a Column of Data in Excel and Then Paste that Data into a Text File?

Hey, Scripting Guy! How can I select a column of data in Excel and then paste that data into a text file?

-- RR

Hey, RR. You know, if you’re like most people, any time you hear the words “Scripting Guys” you undoubtedly think of two things: elegance and sophistication. Despite our well-deserved reputation, however, the Scripting Guys are – at heart – simple guys with simple tastes. Sure, we like caviar as much as anybody (or we probably would if any of us had the guts to actually eat that stuff), but the bottom-line is that we just like to find a way to make things work. If what we do turns out to be simple and elegant, well, so much the better. But our focus is on practicality, not on appearances.

Why do we mention that? Simply because it seems like there ought to be a simple and elegant solution to your problem. However, we have no idea what that simple and elegant solution could be. But – heaven forbid! – the Scripting Guys can’t admit that there’s something about scripting we don’t know. Therefore, rather than expose our deepest and darkest secrets, we decided to give you more of a brute force method of doing what you need to do. It’s just that, instead of brute force, we’re calling it “rustic and hand-crafted.” With any luck, no one will be the wiser.

In other words, rather than putting on airs, we humbly offer you this rustic and hand-crafted script, custom built for you and only you, RR:

Set
objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

i = 1

Do While True
strValue = objWorksheet.Cells(i,3)
If strValue = "" Then
Exit Do
End If
strText = strText & strValue & vbCrLf
i = i + 1
Loop

objExcel.Quit

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\ExcelData.txt")

objFile.Write strText
objFile.Close

When you run this script two things happen: you’ll grab all the data found in column C of the spreadsheet Test.xls, and then you’ll save that data into a text file named ExcelData.txt. It’s not quite copying and pasting, but the net result is the same, and no one will ever be the wiser here, either.

Well, unless they read this column. So just don’t let anyone else read this column and you’re home free.

If anyone is reading this column, the script starts out with a forbidding block of code that really does nothing more than create a visible instance of Excel, open the workbook C:\Scripts\Test.xls, and then make Sheet 1 the active worksheet:

Set
objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Activate

Once we get through with that we’re ready to get down to business.

For starters, we assign the value 1 to a counter variable named i. We’re going to use this variable to keep track of the current row in the spreadsheet. We gave this variable the value 1 because the data in our practice spreadsheet starts in row 1 (that is, there’s no header row). What if your data starts in, say, row 3? No problem: just assign i the value 3.

Note. This is a good time to mention that we’re assuming that all the data in your spreadsheet is contiguous, which is just a fancy way of saying that there are no blank rows in column 3. If you do have blank rows in column 3 you might need to use the UsedRange property to determine the last row in the spreadsheet, and then use a For Next loop to loop from row 1 to row whatever-the-number-of-the-last-row-is. For more information on the UsedRange property see this Office Space article.

In turn, that brings us to the following Do While loop:

Do While True
    strValue = objWorksheet.Cells(i,3)
    If strValue = "" Then
        Exit Do
    End If
    strText = strText & strValue & vbCrLf
    i = i + 1
Loop

What we’re doing here is reading row 1, column 3 of the spreadsheet, then reading row 2, column 3 of the spreadsheet, then row 3, column 3, etc. How do we know when to stop? Well, for each cell we use this line of code to store the value of that cell in a variable named strValue:

strValue = objWorksheet.Cells(i,3)

If strValue is equal to an empty string (“”) we assume we’ve reached the end of the data; with that in mind, we use the Exit Do command to exit the loop. That’s what these three lines of code are for:

If strValue = "" Then
    Exit Do
End If

But wait, you say: what if the value of the cell is not equal to an empty string? Well, in that case we add the value plus a carriage return-linefeed (vbCrLf) to a string named strText:

strText = strText & strValue & vbCrLf

All we’re doing with strText is keeping track of the data we find in column C of the spreadsheet: each time we access a new cell we append that data to the value of strText. (Note that strText is always assigned the current value of strText plus the new value and a carriage return-linefeed.)

After we’ve run through all the cells in column C we use the Quit method to close Excel. That brings us to the halfway point: we’ve retrieved all the data from the designated column in Excel. Now we just have to get that data into a text file.

And while there are some crazy and somewhat-convoluted ways that we might be able to paste the data into a text file it seemed much easier to just save the data as a text file. That’s what this code is for:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\ExcelData.txt")

objFile.Write strText
objFile.Close

Here we’re creating a new file named C:\Scripts\ExcelData.txt and then using the Write method to write the value of strText to that file. What if we wanted to add this data to an existing text file? No problem; in that case we’d use code like this:

Const ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("C:\Scripts\ExcelData.txt", ForAppending, True)

objFile.Write strText
objFile.Close

Note. For more information on writing to new text files or appending data to existing text files, take a look at this section of the Microsoft Windows 2000 Scripting Guide.

And there you have it: a brand-new script carefully hand-crafted by the Scripting Guys, the people who don’t believe in the cold, impersonal style of mass-produced scripts.

Well, unless you mass produce them using the Scriptomatic, of course. But that’s different.


How Can I Set the Default File Path in Excel to a User’s Home Directory?

Hey, Scripting Guy! How can I set Excel’s default file path to a user’s home directory?

-- LL

Hey, LL. We hope that the script we’re about to show you will answer your question; if it doesn’t, you might have to wait awhile before we can get back to you on it. That’s because the Scripting Guy who writes this column isn’t even at work today; instead, he’s spending the day at King County Superior Court in Seattle.

You know, that’s what everyone thinks when they first hear the news; as it turns out, though, it’s not illegal to write a daily scripting column that talks about pretty much everything except scripting. (Whether that should be illegal is, of course, a different question.) As it is, instead of being on trial himself the Scripting Guy who writes this column received the “honor and privilege” of being selected for jury duty. Consequently, he’ll be spending the day hanging around with hardened criminals, miscreants, and other low-life types.

Which, now that we think about it, pretty much does describe a day spent hanging around with the other Scripting Guys, doesn’t it?

But don’t worry. While the Scripting Guy who writes this column is off battling for truth and justice, he did leave a little something for everyone to remember him by:

Set objUser = GetObject("LDAP://cn=Ken Myer,ou=Finance,dc=fabrikam,dc=com")
strHomeDirectory = objUser.homeDirectory

Set objExcel = CreateObject("Excel.Application")
objExcel.DefaultFilePath = strHomeDirectory
objExcel.Quit

As you can see, what we have here is a two-part script: in part 1 we retrieve the user’s home directory, and in part 2 we set Excel’s default location to that home directory. To determine the user’s home directory we start out by binding to the appropriate user account in Active Directory. We then use this line of code to grab the value of the homeDirectory attribute and store it in a variable named strHomeDirectory:

strHomeDirectory = objUser.homeDirectory

You know, come to think of it, part 1 was pretty easy, wasn’t it? That’s good. And the good news just keeps on coming: part 2 is equally easy. In the second section of the script we start out by creating an instance of the Excel.Application object. As soon as Excel is up and running we then use this line of code to assign the value of the variable strHomeDirectory to the DefaultFilePath property:

objExcel.DefaultFilePath = strHomeDirectory

That’s it; all we have to do now is call the Quit method and terminate our instance of Excel.

And you’re right: in most of our Excel scripts we use the command objExcel.Visible = True in order to make Excel visible onscreen. Why didn’t we do that here? Well, for one thing, the script takes only a second or two to run; no sooner would Excel appear on screen then we’d be calling the Quit method to get rid of it. On top of that, all we’re doing is setting an Application object property; as a result, there’s really nothing to see onscreen anyway.

You say you have another question: how will the Scripting Guy who writes this column decide whether someone is innocent or guilty? To be honest, he doesn’t know; after all, he’s never served on a jury before. He does know one thing, however: whoever the accused happens to be had better hope that he or she gave the Scripting Guy who writes this column a good evaluation score during Windows PowerShell Week.

Not that we Scripting Guys keep track of that sort of thing, mind you.


How Can I Set the Print Area in an Excel Spreadsheet?

Hey, Scripting Guy! How can I set the print area in an Excel spreadsheet?

-- PRP

Hey, PRP. You know, before the Scripting Guy who writes this column wrote his very first column he took a sacred vow: he vowed never to take a sacred vow, at least not when it came to writing the Hey, Scripting Guy! column. That’s because he didn’t want to make any work-related commitments that he wouldn’t be able to live up to.

Note. Not that the Scripting Guy who writes this column has ever made a work-related commitment that he wasn’t able to live up to, mind you.

Regardless of what the Scripting Editor might tell you.

At any rate, had he chosen to take such a vow the Scripting Guy who writes this column would likely have vowed to never take shortcuts; to always work extremely hard when answering a question; to never take, say, 3 minutes to write a script and then try to pass that off as a full column. In his defense, he usually tries to live up to those ideals anyway. But sometimes the Scripting Son plays in a baseball tournament, and the Scripting Guy who writes this column needs to sneak out of the office by 11:00 AM or so if he wants to attend the game. (Does that mean he came in at 3:00 AM in order to put in a full 8-hour day? Well, kind of ….)

On top of that, sometimes the Scripting Car works and sometimes it doesn’t; that means the Scripting Guy who writes this column has to deal with mechanics, weigh the pros and cons of buying a new car, and has to venture out of Scripting Guys headquarters to actually look at new cars. And then there are the weeds that need to be pulled, the doughnuts that need to be bought and, well, here’s the deal: you can’t really break a promise if you never actually made that promise, now can you? With that in mind, the Scripting Guy who writes this column feels only a little guilty about the following script, one that sets the print area in an Excel spreadsheet:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook
= objExcel.Workbooks.Add()
Set objWorksheet
= objWorkbook.Worksheets(1)

For x = 1 to 10
For y = 1 to 10
objExcel.Cells(x, y).Value = x + y
Next
Next

objWorksheet.PageSetup.PrintArea = "B2:D4"

As you can see, this isn’t the most complicated script in the world. In fact, the bulk of the script doesn’t even have anything to do with setting the print area; that code is there simply to add some data to the spreadsheet, making it a little more obvious what it means to set the print area.

To begin with, we create an instance of the Excel.Application object and then set the Visible property to True; long-time readers of this column know that this simply gives us a running instance of Excel that we can view onscreen. We then use these two lines of code to add a new workbook to our instance of Excel, and then to bind us to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

And yes, those two lines of code are very important: when you programmatically create an instance of Excel all you get is an instance of Excel; you don’t get a workbook or any worksheets. This might simply be a personal preference on the part of the Scripting Guys, but we don’t think there’s much fun in having an instance of Excel unless you also have a worksheet or two to play with.

Once we do have a workbook and once we do bind to the first worksheet in that workbook we then use this block of code to add a little data to the pot:

For x = 1 to 10
    For y = 1 to 10
        objExcel.Cells(x, y).Value = x + y
    Next
Next

This really isn’t that big a deal; if you run the script you should be able to figure out what it does and how it does that.

So now that we have some data on our worksheet how do we go about assigning a print area to that worksheet? Why, like this, of course:

objWorksheet.PageSetup.PrintArea = "B2:D4"

You can see why we felt a little guilty about today’s column; after all, it takes only a single line of code to set the print area. All we have to do is assign a value to the PrintArea property, a property of the PageSetup object (which happens to be a child object of the Excel Worksheet object). The only thing even remotely tricky about this line of code is the need to use the so-called “A1-style” cell addresses; note that we set the print area to B2:D4 and not something like objExcel.Cells(2, 2):objExcel.Cells(4, 4). Depending on what you’re doing that could be a little tricky; by default you typically don’t work with A1-style addresses in a script. Fortunately, though, there’s an alternate way to specify a print area. If you’ve assigned a Range in your spreadsheet you can then specify the print area using code like this:

Set objRange = objExcel.Range("B2:D4")
objWorksheet.PageSetup.PrintArea = objRange.Address

Needless to say, we didn’t really knock ourselves out with this bit of code, either: after creating a range we then assign the value of the range’s Address property as the print area. As you’ve probably already guessed, the Address property gives us A1-type cell addresses.

Which, by the way, is a handy little tip to keep in mind.

That’s all we need to do. If you later decide to remove the print area simply set the value of the PrintArea property to an empty string, like so:

objWorksheet.PageSetup.PrintArea = ""

We hope that answers your question, PRP; it better, because it’s just about time to pack up and head for the baseball game. (And, for once at least, it looks like the weather’s going to be absolutely gorgeous.) As a general rule, this is the time we’d likely vow to work a little harder and put in a little more effort on the next column. Unfortunately, though, we can’t do that. After all, we did make a sacred vow never to make a sacred vow. And it just wouldn’t be like the Scripting Guy who writes this column not to live up to his commitments.

Note. Yes, we know: the Script Center is filled-to-overflowing with promises the Scripting Guy who writes this column made but – for various reasons – was never able to keep. But don’t worry: you don’t have to write in and point these out to him. The Scripting Editor – who never nags, by the way – continually … reminds … him of these things.

Over and over and over again ….


How Can I Sort a Row in an Excel Spreadsheet?

Hey, Scripting Guy! How can I sort a row in an Excel spreadsheet?

-- RF

Hey, RF. You know, this is a tough question, but as a resident of the state of Washington the Scripting Guy who writes this column doesn’t shy away from a challenge. Therefore, he fully intends to address this matter … in the year 2013.

In case you’re wondering about that, a few years ago the Washington state legislature – concerned that our public schools were graduating students who had yet to master the fundamentals of reading, writing, and arithmetic – passed a law stating that no one could graduate from a Washington state high school without passing the WASL (a standardized achievement test). The class of 2008 was targeted as the first group that had to pass the WASL in order to graduate.

As it turned out, a large percentage of students failed the math portion of the test. (Some students failed the reading and/or the writing portions, but math was by far the major bugaboo.) That’s a shame, but, then again, that is what the test was designed to do: identify students who don’t meet a minimum level of competency. Granted, that’s a tough pill to swallow, but, in turn, it should help encourage the schools to do a better job. Besides, it’s not as hopeless as it might sound; after all, students can retake the test as many times as they need to until they finally pass.

Note. To tell you the truth, the Scripting Guy who writes this column isn’t a particularly big fan of standardized testing, but rules are rules. And, in the interests of fair disclosure, we should note that the Scripting Son took the WASL and passed all three sections.

So how did the state of Washington respond to this situation? The only way it could: by declaring that students didn’t have to pass the math portion of the WASL after all; instead, all they had to do was pass the reading and writing portions. One day a huge numbers of students had flunked the WASL; the next day, like magic, most of those found out they’d passed the test after all. The system works!

Of course, in theory, the class of 2013 will have to pass the math portion of the test. If they don’t, then they will not graduate from high school. And this time we mean it.

Unless, of course, we don’t.

Fortunately, the Scripting Guys don’t have the patience to wait until the year 2013:

Const xlAscending = 1
Const xlNo = 2
Const xlSortRows = 2

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

objWorksheet.Cells(1, 1) = "C"
objWorksheet.Cells(1, 2) = "D"
objWorksheet.Cells(1, 3) = "B"
objWorksheet.Cells(1, 4) = "A"

Set objRange = objExcel.ActiveCell.EntireRow
objRange.Sort objRange, xlAscending, , , , , , xlNo, , , xlSortRows

So how does this script work? Well, we don’t mean to alarm residents of the state of Washington, but we start off with something that looks a lot like math. But don’t worry, this code doesn’t involve actual arithmetic; instead, we’re simply assigning values to three different constants:

· xlAscending, which we’ll use to sort the data in ascending (A to Z) order. Can we sort the data in descending (Z to A) order if we want to? Of course we can; in that case we just use the constant xlDescending and assign it the value 2.

· xlNo, which we’ll use to indicate that our data does not include a header row. (Which should be obvious, seeing as how we’re sorting a row rather than a column.)

· xlSortRows, which tells the script to sort the selected cells by row rather than by column.

After defining our constants we create an instance of the Excel.Application object and set the Visible property to True; that gives us a running instance of Excel that we can see onscreen. We then use these two lines of code to create a new workbook and to bind to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)

Now, as far as the Scripting Guys are concerned, sorting data isn’t much fun unless you actually have some data to sort. Therefore, our next block of code simply inserts the values C, D, B, and A in cells A1 through D1 (that is, in the first four cells in the first row of our spreadsheet):

objWorksheet.Cells(1, 1) = "C"
objWorksheet.Cells(1, 2) = "D"
objWorksheet.Cells(1, 3) = "B"
objWorksheet.Cells(1, 4) = "A"

Note. Despite the fact that the Scripting Guy who writes this column went to high school in the state of Washington he was still able to determine that this code did, indeed, put data in the first four cells in the spreadsheet. And he managed to do that largely on his own, with only a little bit of help.

Now comes the good part. Before you can sort data you must create a range that encompasses all the data to be sorted. We want to sort the data in row 1, so we use this line of code to turn row 1 into a range:

Set objRange = objExcel.ActiveCell.EntireRow

As you can see, there’s not much to this. By default, the ActiveCell happens to be the first cell in the spreadsheet (unless we specifically make a different cell the ActiveCell). By referencing both the ActiveCell and the EntireRow method we can easily select all the cells in row 1.

Good question: what if we have multiple rows that need to be sorted? In that case, you’ll need to sort each row individually, which also means that you have to create a different range for each row of data. How do you do that? Well, one way is to change the ActiveCell any time you start a new row. Here’s a simple way to do that:

intNewRow = objExcel.ActiveCell.Row + 1
strNewCell = "A" & intNewRow
objExcel.Range(strNewCell).Activate

What we’re doing here is adding 1 to the row for the current ActiveCell; if we’re currently in row 1, that makes intRow equal to 2. We then tack the letter A on to the front of the row number; that makes strNewCell equal to A2. Why A2? Well, A2 just happens to be a cell in row 2. By identifying a cell in row 2 we can then use the Activate method to move the active cell to the next row.

And, sure, there are other ways of doing this. If you have a better approach then by all means use that approach. Otherwise, this tactic will do the trick.

Now, where were we? Oh, right: sorting a row of data. After we’ve created a range encompassing the row in question we can sort that data using this command:

objRange.Sort objRange, xlAscending, , , , , , xlNo, , , xlSortRows

Admittedly, this is a strange-looking line of code, what with the commas and empty parameters and all. That’s because Excel offers a number of sort options (including the ability to sort first by one column of data and then by another) that we aren’t interested in. However, because the parameters we provide to the Sort method must appear in a specific order we can’t just leave out these items; if we did, that would change the order and lead to all sorts of problems. To make sure that everything ends up exactly where it needs to we have to insert blank placeholders for each option we want to ignore. That’s exactly what we’ve done here.

Note. For more information about these other options, and to see some additional examples on sorting data in Excel, see our Office Space article on, well, sorting data in Excel.

For now, the important thing is to note that we specified the range of data to be sorted (objRange), and used our three constants to indicate that we: 1) want to sort the data in ascending order, 2) don’t want to use a header row, and 3) want to sort the data by rows rather than by columns. Will our data actually get sorted when we run this script? Give it a try and see for yourself.

We hope that answers your question, RF. As for the state of Washington, well, Homer Simpson probably said it best: if something is hard to do, then it’s not worth doing. An important lesson for all of us, not just high school students.


How Can I Sort Worksheets in a Workbook?

Hey, Scripting Guy! How can I sort worksheets in a workbook?

-- FS

Hey, FS. How can you sort worksheets in a workbook? Well, to tell you the truth, not as easily as we had hoped you could sort worksheets in a workbook. This doesn’t mean it can’t be done; it just means that the process is a tiny bit convoluted. Because of that, don’t feel bad if you don’t fully understand everything we’re about to say; we’re not even sure if we fully understand it. But the script seems to work, and, ultimately, that’s all we care about anyway.

Let’s assume we have a workbook that has the following set of worksheets:

How do we sort these worksheets in alphabetical order? Here’s how:

On Error Resume Next

Dim arrNames()
intSize = 0

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Inventory.xls")

For Each objWorksheet in objWorkbook.Sheets
    ReDim Preserve arrNames(intSize)
    arrNames(intSize) = objWorksheet.Name
    intSize = intSize + 1
Next

For i = (UBound(arrNames) - 1) to 0 Step -1
    For j= 0 to i
        If UCase(arrNames(j)) > UCase(arrNames(j+1)) Then
            strHolder = arrNames(j+1)
            arrNames(j+1) = arrNames(j)
            arrNames(j) = strHolder
        End If
    Next
Next

For i = UBound(arrNames) to 1 Step -1
    Set objSheet1 = objWorkbook.Sheets(arrNames(i))
    Set objSheet2 = objWorkbook.Sheets(arrNames(i-1))
    objSheet2.Move objSheet1
Next

Yes, yes, we know. But we’re going to try to explain this as best we can.

Actually the script starts off simply enough. We begin by creating a dynamic array named arrNames(). Why? Well, although Excel has a ton of features built into it, it is missing at least one thing: a command for sorting worksheets. Because Excel has no built-in method for sorting worksheets we’re going to have to take care of that ourselves. And the way we’re going to do that is by grabbing all the worksheet names, storing those names in a dynamic array, and then sorting the items in that array. Once we’ve sorted the array we’ll know the correct order for all the worksheets; at that point we can then use Excel’s Move method to move each worksheet to the desired location.

So after we create the array we have some standard boilerplate code that creates an instance of the Excel.Application object, sets the Visible property to True, and then uses the Open method to open the file C:\Scripts\Inventory.xls. Now we’re ready to roll up our sleeves and start working.

The first thing we need to do is grab all the worksheet names and put them into our dynamic array. That’s what we do in this block of code:

For Each objWorksheet in objWorkbook.Sheets
    ReDim Preserve arrNames(intSize)
    arrNames(intSize) = objWorksheet.Name
    intSize = intSize + 1
Next

All we’re doing here is setting up a For Each loop to loop through the Sheets collection; as the name implies, this collection consists of all the worksheets found in our workbook. For each worksheet in the collection we use the ReDim Preserve command to resize our dynamic array. The array starts off as size 0, meaning it’s an array with a single item. (An array size will always be the number of items in the array minus 1.) How do we know that we set the size to 0? Because we assigned it the value of a counter variable named intSize, a variable that we set to 0 at the beginning of the script.

We then assign the first item in the array the name of the first worksheet in the collection; that’s what we do here:

arrNames(intSize) = objWorksheet.Name

We increment our counter variable by 1, then loop around and repeat the process for the second worksheet in the collection, a worksheet whose name will become the second item in our array. When we’re all done we’ll have an array consisting of the following items, in this order:

Sheet2
c
a
Sheet1
b

OK, that wasn’t too bad. That’s not necessarily true of this next section, however. Here we use a “simple” bubble sort to sort the items in the array in alphabetical order:

For i = (UBound(arrNames) - 1) to 0 Step -1
    For j= 0 to i
        If UCase(arrNames(j)) > UCase(arrNames(j+1)) Then
            strHolder = arrNames(j+1)
            arrNames(j+1) = arrNames(j)
            arrNames(j) = strHolder
        End If
    Next
Next

We’re not going to try and explain the ins and outs of a bubble sort in today’s column; for a reasonably good explanation of how this works you might take a look at the Scripting Week 2 webcast Things the Scripting Guys Never Told You. In short, what happens in a bubble sort is that you compare each item in the array with every other item in the array, and then swap their places in that array if need be. For example, our first two items in the array are these:

Sheet2
c

Alphabetically-speaking, c comes before Sheet2. Therefore, our bubble sort will swap those two names, meaning that these become the first two items in the array:

c
Sheet2

This continues until each item has been compared with every other item. The net result? An array sorted in alphabetical order:

a
b
c
Sheet1
Sheet2

As luck would have it, this also turns out to be the order we want for our spreadsheets. Now that we know which spreadsheet should come first, which should come second, etc., we can use this block of code to rearrange the spreadsheets within the workbook:

For i = UBound(arrNames) to 1 Step -1
    Set objSheet1 = objWorkbook.Sheets(arrNames(i))
    Set objSheet2 = objWorkbook.Sheets(arrNames(i-1))
    objSheet2.Move objSheet1
Next

What we’re doing here is starting at the end of the array (UBound connects us to the last item in the array, in this case Sheet2) and then working our way down (that’s what the Step -1 does). Thanks to our sorted array we know that the last item in the array – Sheet2 – should also be the last worksheet in the workbook. We also know that the next-to-last item in the array – Sheet1 – should be the next-to-last worksheet in the workbook. Therefore, we use this code to create an object reference to Sheet 2:

Set objSheet1 = objWorkbook.Sheets(arrNames(i))

We then use this code to create an object reference to Sheet1:

Set objSheet2 = objWorkbook.Sheets(arrNames(i-1))

Finally, we call the Move method to move Sheet1 in front of (before) Sheet2:

objSheet2.Move objSheet1

How did we know that Sheet1 would be moved in front of Sheet2? That’s easy: there’s no comma after the Move method:

objSheet2.Move objSheet1

If we wanted to move Sheet1 in back of (after) Sheet2 then we would put a comma after the Move method:

objSheet2.Move, objSheet1

Admittedly, this isn’t the most intuitive thing in the world, but it works.

When all is said and done, we’ll end up with a workbook that looks like this:

And if you aren’t totally sure how we got to that point, well, don’t worry about it. Sometimes it’s enough just to have faith, without questioning the how or the why.


How Can I Specify the Number of Decimal Places to Display in an Excel Spreadsheet?

Hey, Scripting Guy! How can I specify the number of decimal places to be displayed in an Excel spreadsheet?

-- JS

Hey, JS. Yes, it is Presidents Day here in the US, a day on which schools, banks, and government offices are all closed. Fortunately, though, the Scripting Guys are open and ready for business.

Why are the Scripting Guys open and ready for business on a national holiday? You know, we never thought about that. Hmmm ….

Interestingly enough, JS, Presidents Day is really supposed to mark the birthday of George Washington, the first US President. Because of that, the holiday used to be celebrated on February 22nd, which is Washington’s actual birthday. Many years ago, however, the US Congress voted to move the holiday to the third Monday in February, and to rename it Presidents Day. Is that because the US has had lots and lots of outstanding Presidents, each one as deserving of a national holiday as George Washington?

Uh, maybe we should just leave that question unanswered for now. William Howard Taft, who weighed over 300 pounds, once got stuck in the White House bathtub. But we’re not sure if that really merits a national holiday.

Oh, wait: and General Zachary Taylor, old “Rough and Ready” himself, once loaned us a script that lets you specify the number of decimal places to be displayed in an Excel spreadsheet:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

For i = 1 to 10
    objExcel.Cells(i, 1).Value = i/6
Next

Set objRange = objWorksheet.UsedRange
objRange.NumberFormat = "#.0000"

How does this script work? Good question. Although Zachary Taylor died in 1850, Scripting Guy Peter Costantini held a séance for us and managed to contact the late President. Here’s what President Taylor had to say:

Well, sir, I start off by creating an instance of the Excel.Application object (crackin’ good object, Excel.Application) and then set the Visible property to True. That gives us a running instance of Excel that we can view on the screen. I then use these two lines of code to add a new workbook to our instance of Excel, and to bind us to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

If that don’t beat all, huh? Of course, a spreadsheet without data isn’t much fun now, is it? We need some numbers to work with, so I stuck in this little For Next loop that simply takes the number 1 through 10, divides the number by 6, then puts each quotient in a separate cell in column A:

For i = 1 to 10
    objExcel.Cells(i, 1).Value = i/6
Next

That’s gonna give us a spreadsheet that looks like this:

Now, I know what you’re thinking. You’re thinking, “Tarnation, Zachary, that ain’t no good; that spreadsheet ain’t fit for man nor beast. Can’t we format those cells so they all display 4 decimal places?”

Easy does it now; just hold on to your hats. Yes, we can format those cells so that they all display 4 decimal places; why else would I put in these two lines of code:

Set objRange = objWorksheet.UsedRange
objRange.NumberFormat = "#.0000"

As you can see, in the first line we create an object reference (objRange) to the worksheet’s “used range.” What’s a used range? Well, sir, it’s just what the name says it is: the used range represents the range of cells that have data in them (that is, the cells that have been used). In our sample spreadsheet we have data in cells A1 through A10, so guess what: our used range is going to encompass those very same cells, cells A1 through A10.

And then in the second line of code we specify the number of decimal places for the cells in that range. We want to display four decimal places, so we put 4 zeroes after the decimal point when assigning a value to the NumberFormat property. In turn, that’s gonna give us a spreadsheet that looks like this:

Now that’s the cat’s pajamas.

What’s that? You say you’d prefer to display five decimal places? Boy, there’s just no pleasing some folks, is there? But that’s OK; just put five zeroes after the decimal point, like so:

objRange.NumberFormat = "#.00000"

Want to put a leading zero before the decimal point? Then just change the # sign to a 0, like this:

objRange.NumberFormat = "0.00000"

I tell you, I could sit here and do this all day. Or at least I could, except for the fact that they serve dinner early up here in heaven, and tonight we’re having johnnycakes and cornpone. Good day to you all.

So there you have it, JS: Zachary Taylor’s take on specifying the number of decimal places to display in an Excel spreadsheet. You know, the interesting thing about having a séance with President Taylor was the fact that – what’s that? No, we really did, we … that is, Peter, he – oh, OK, we admit it: we didn’t really have a séance with Zachary Taylor; we made the whole thing up. And no US President gave us this script; we wrote it ourselves. The Scripting Guys apologize for any misconceptions we may have created here. Sorry.

We’re curious, though: how did you know we were making that up? Ah, good point: implying that a US President would make it to heaven was kind of a dead giveaway, wasn’t it? Such a silly mistake.

Note to US Presidents. Come on, guys; we’re just kidding around. Listen, would it help if we gave you a Dr. Scripto bobblehead doll? If so, well, there’s still plenty of time to enter the 2007 Winter Scripting Games. Remember, all you have to do is enter a single event and you’ll have a chance to win one of 250 Dr. Scripto bobbleheads or a copy of the book Windows PowerShell in Action. Those are pretty good prizes, even for a US President.

Editor’s Note. After a week of late nights scoring events for the 2007 Scripting Games, the lack of sleep has apparently made the Scripting Guy who writes this column even more disagreeable than usual. We’ll throw a few donuts into his office to try to keep him under control for the rest of the week, but we apologize in advance to everyone he manages to insult this week.


How Can I Tell If a Specified Worksheet Exists in an Excel Workbook?

Hey, Scripting Guy! How can I tell if a specified worksheet exists in an Excel workbook?

-- MVK

Hey, MVK. Back when he was in high school (that was about 3 or 4 years ago, of course), the Scripting Guy who writes this column was kind of half-watching a variety show on TV. The show featured a man and his dogs: “The Smartest Dogs in the World!” The man would show the dogs a cue card that had a multiple choice question on it. The dogs would - allegedly - read the cue card and then bark out the answer: one bark for A, two barks for B, etc.

Note. Sure, that sounds impressive. But, to tell you the truth, the questions were pretty easy; they were all questions like “Who was the first President of the United States?” Come on; even a cat could get that one right.

At any rate, as this future Scripting Guy watched the show he suddenly realized how the trick worked: he figured out how the guy was telling his dogs which answer to bark out. Smelling an opportunity, the Scripting Guy recruited his brother and put together a similar act, one in which the two would lay an entire deck of cards down on a table. They would ask someone to select a card, and then - using his telekinetic powers - the Scripting Guy would beam the answer into his brother’s mind. One hundred percent of the time the Scripting Brother would come up with the correct answer! The Scripting Guy and his brother spent many happy hours enthralling friends and relatives with their act. And no one ever came close to figuring out how they did it.

The point: after mulling this over a bit, we’ve decided that psychic powers are the best way to determine whether a specified worksheet exists in an Excel spreadsheet.

What’s that? No, we aren’t going to tell you how the trick worked, even if you do promise not to tell anyone else. (What, you don’t think we’ve heard that before?) In fact, maybe it would be best to forego telekinesis altogether; now that we think about it, it might be better to use a script like this one instead:

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

x = 0

For Each objWorksheet in objWorkbook.Worksheets
    If objWorksheet.Name = "Budget" Then
        x = 1
        Exit For
    End If
Next

objExcel.Quit

If x = 1 Then
    Wscript.Echo "The specified worksheet was found."
Else
    Wscript.Echo "The specified worksheet was not found."
End If

Granted, this script probably won’t get you on any variety shows, but - never mind, forget we said that. After all, if the guy with The World’s Smartest Dogs can get on a variety show, well who knows what you could do with this?

As you probably already guessed, there isn’t much magic to the script. We start out by creating an instance of the Excel.Application object, then use the Open method to open the file C:\Scripts\Test.xls.

Note. Isn’t this about the time we usually set the Visible property of Excel to True? Yes, it is. This time around, however, we decided to leave Excel running in an invisible window: that enables us to quickly open the workbook, look for the specified worksheet (a worksheet named Budget), and then close Excel, all without anyone knowing what we were doing or how we were doing it. (No, that’s not magic: after all, when you’re a Scripting Guy no one ever knows what you’re doing, let alone how - or why - you’re doing it.)

We then have this seemingly-random little line of code:

x = 0

Why are we, totally out of the blue, assigning the value 0 to a variable named x? We don’t want to spoil the suspense, so hang tight; we’ll explain why in a minute.

Promise.

Our next step is to set up a For Each loop that loops through the collection of all the worksheets found in Test.xls; that’s what this line of code is for:

For Each objWorksheet in objWorkbook.Worksheets

Inside that For Each loop we simply check each worksheet to see if the sheet has a Name equal to Budget. If it doesn’t, we skip past the worksheet and check the next item in the collection. If the Name is equal to Budget, we then assign x the value 1:

x = 1

Now you can probably see what we’re doing here: we’re just using x as a simple way to track whether or not we’ve found a worksheet named Budget. This variable begins life with a value of 0; that value will never change … unless, of course, we find a worksheet named Budget. In turn, that means that we can determine whether or not the target worksheet was found simply by examining the value of x. If x is 0 then the worksheet wasn’t found. But if x is 1 ….

Note. Yes, by making a change here or there we could have reduced the size of this script by 3 or 4 lines. We chose not to do that simply because variables like our friend x can be very useful in scripting; instead, we decided to take the opportunity to show you a simple example of using a variable to keep track of some condition within the script. It might not seem like much, but it’s a good little trick to have in your repertoire.

And one other thing: if a worksheet named Budget is found we also execute the Exit For statement in order to exit our For Each loop. After all, once we find a worksheet named Budget we don’t need to look through all the other worksheets in the collection; we’ve already found what we were looking for. So why not exit the loop and get on with things?

Good question: what does it mean to “get on with things,” at least in this script? Well, to begin with, we use this line of code to close Excel:

objExcel.Quit

And then we use this block of code to examine the value if x and report back whether or not the worksheet was found:

If x = 1 Then
    Wscript.Echo "The specified worksheet was found."
Else
    Wscript.Echo "The specified worksheet was not found."
End If

Yes, it’s like magic. But it’s not really magic.

And don’t bother asking that question: we aren’t telling you how the mind-reading trick worked. (How did we know you were going to ask that question? Sorry; we can’t tell you that, either.)


How Can I Tell if an Excel Workbook is Open and, If It Isn’t, Open It?

Hey, Scripting Guy! How can I tell if a specific Excel workbook is open and, if it isn’t, open it?

-- RK

Hey, RK. Every now and then we see a question and think, “We should answer this one; it’s so easy it won’t take more than a few minutes to finish the column and then we can all take a nice long lunch.” At some point you’d think we’d learn that every time we say that it turns out that the problem isn’t as easy as it first looked, and that the column takes far more than just a few minutes to complete. But, no, we never do learn, which is why we’re still hard at work trying to answer this question.

Of course, we still plan on taking a nice long lunch, but that’s another story.

How did we get fooled on this one? Well, we knew Excel has a Workbooks collection that returns a list of all the open workbooks. Foolishly, we assumed this meant all the open workbooks. (And, yes, we know what people say about assuming things.)

Unfortunately, though, the Workbooks collection applies only to a specific instance of Excel; that meant we couldn’t just create a new instance of Excel and get back a list of all open workbooks. Instead, we’d have to bind to each running instance of Excel, query its Workbooks collection, and then combine all those collections into a single list. That’s theoretically possible, but….

And so we thought to ourselves, “OK, we’re having difficulty solving a problem with Microsoft Excel. What do most people do when they’re having problems with Excel?” And once we put it that way, the answer was obvious: we’d use Microsoft Word to extricate ourselves from our predicament.

Yes, Microsoft Word. As it turns out, Word has something called the Tasks collection that can replicate many of the functions of the Windows Task Manager. (If you’re interested in learning more about the Tasks collection, take a look at the “Build Your Own Task Manager Using Microsoft Word” article on the Scripting for Microsoft Office center.) As you can see here, open workbooks are displayed – by file name – in Task Manager:

We figured we could use the Tasks collection in Word to check and see if a particular workbook was open and, if not, open it. All of which gave rise to one of the craziest workarounds the Scripting Guys have ever come up with:

Set
objShell = CreateObject("Wscript.Shell")

Set objWord = CreateObject("Word.Application")
Set colTasks = objWord.Tasks
i = 0

For Each objTask in colTasks
strName = LCase(objTask.Name)
If Instr(strName, "inventory.xls") Then
i = 1
End If
Next

strCmdLine = "excel.exe " & chr(34) &
"C:\Scripts\Inventory.xls" & chr(34)

If i = 0 Then
objShell.Run strCmdLine, 3
End If

objWord.Quit

The script begins innocuously enough; we simply create an instance of the WSH Shell object (we’ll use that, if we need to, to open the workbook in question). We then create an instance of the Word.Application object, and use the Tasks property to retrieve the collection of tasks running on the computer. (Tasks include all the items you see on the Applications tab in Task Manager.) We also set a variable named i to 0, for reasons we’ll explain in a second.

Next we loop through the collection of tasks. We grab a task and store the lowercase version of the task name in a variable named strName; that’s what we do here:

strName = LCase(objTask.Name)

We then use VBScript’s InStr function to see if the string inventory.xls can be found anywhere in the task name. If it is, that means Inventory.xls is already open and we set the value of i to 1. If it isn’t, then Inventory.xls must not be open, and the value of i stays at 0. We then loop around and continue with all the remaining tasks.

When we exit the loop we check the value of i. If i is equal to 0 we do two things. First we create a command string that calls Excel.exe, passing as a command-line parameter the path to Inventory.xls. That’s what we do here:

strCmdLine = "excel.exe " & chr(34) & "C:\Scripts\Inventory.xls" & chr(34)

Second, we use the Run method to call this command string. In addition to the command string we pass Run one additional parameter: the value 3, which means “Open this file in a maximized window, and then make it the active window.” We then call the Quit method to dismiss Microsoft Word (which, incidentally, never appears on screen) and then go about our business.

And, yes, we’re well aware of how crazy this all sounds, but it will work. Admittedly, it’s not 100% foolproof; that’s because only the file names are captured by the Tasks collection, not the complete file paths. That could be a problem if you have the file D:\Archive\Old Inventory Files\January 2004\Inventory.xls open; the script will see only the file name – Inventory.xls – and assume that C:\Scripts\Inventory.xls is open. We think there’s a way to deal with that issue as well, but that will have to wait for another day.


How Can I Update and Then Break All the Links in an Excel Spreadsheet?

Hey, Scripting Guy! How can I open an Excel spreadsheet, update all the links to other Excel spreadsheets, and then break those links?

-- YH

Hey, YH. Before we begin, we’d like to make an announcement: the Scripting Guy who writes this column gives up.

To tell you the truth, that’s not something we ever expected to say; after all, the Scripting Guy who writes this column never gives up. For example, a week or so ago this same Scripting Guy squared off against the Scripting Son in a 100-yard dash. The Scripting Son -- who’s bigger, faster, and (slightly) younger than his father -- won by a good 5 or 6 yards. But did the Scripting Dad give up? No sir; instead, he’s spent all his spare time since that race plotting how he could win the rematch.

Note. In case you’re interested, the only plan he’s been able to come up so far with involves buying and ingesting a bottle of super leg vitamins from the Acme Corporation. Granted, that’s not much of a plan. Nevertheless, it that worked out pretty good for Wile E. Coyote.

Or at least we think it did. We never actually saw how that particular cartoon ended.

But 100-yard dashes are easy, especially if you’re willing to cheat by taking super leg vitamins. Unfortunately, however, there’s one force even the Scripting Guy who writes this column can’t combat: the grocery store express lane.

We’re not sure how ubiquitous grocery store express lanes are in the rest of the world, but in the US larger grocery stores typically have one more express lanes, checkout stands for people who have only x number of items or less.

Totally unrelated note. Saying the phrase “rest of the world” caused the Scripting Guy who writes this column to remember a meeting he recently attended, a meeting where it was noted that “42% of users came from the US and 39% came from other countries.” If you add 42% and 39% you’ll see why we found this statistic so intriguing.

Anyway, the idea behind the express lane is a good one: if all you need to do is pick up a loaf of bread or a carton of milk you don’t need to stand in line behind a band of survivalists buying enough groceries to last through the current millennium. Instead, you pop into the express lane, pay for your loaf of bread, and then get on with your life.That’s the idea, anyway. In practice, however, otherwise decent and law-abiding citizens have made a mockery of the express lane, sometimes pulling off elaborate schemes in an attempt to sneak through the line, other times just brazenly taking 13 items through the 10-item or less line and daring the clerk to stop them. And how have the grocery stores responded to this? Well, a few years ago the grocery store where the Scripting Guy who writes this column shops responded by raising the 10 item to 15 items. Unfortunately, that didn’t work; instead, people simply tried to sneak 18 items through the 15-item line. So what did the store do in response to that? You got it: they recently raised the express lane limit to 20 items.

Needless to say, there’s no point in ranting and raving about express lane violations any more; we give up. (Besides, it’s getting to the point where it will soon be physically impossible to cart around enough items to push you over the limit anyway.). From now on, feel free to take as many items through the express lane as you wish; we won’t say a single word about it.

What’s that? Good point: we haven’t said a single word about a script that can update and then break all the links in an Excel spreadsheet either. But that’s OK; after all, one script is worth a thousand words, right? Right:

Const xlLinkTypeExcelLinks = 1
Const xlExcelLinks = 1

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls", 3)

arrLinks = objWorkbook.LinkSources(xlLinkTypeExcelLinks)

For i = 1 to Ubound(arrLinks)
    objWorkbook.BreakLink arrLinks(i), xlLinkTypeExcelLinks
Next

Let’s take a minute or two to talk about the script and to how it works. We start off by defining a constant named xlLinkTypeExcelLinks and setting the value to 1; we’ll use this to tell the script that we want to work with “Excel type links,” links to other cells, worksheets, or workbooks. Are there other constants we could use here? Well, there’s one: xlLinkTypeOLELinks, which has a value of 2. We’d use this constant if we wanted to work with OLE objects (for example, a Word document or PowerPoint presentation embedded within our spreadsheet. But that’s not what we want to do, so forget we even mentioned it.

We then define a second constant (xlExcelLinks) and set its value to 1; we’ll use this constant when we retrieve a collection of all the Excel type links found in the workbook. And yes, we could have simply used the same constant (xlLinkTypeExcelLinks) in both places; after all, they both have same value (1) and they both refer to the same kind of link. Nevertheless we chose to use two different constants simply because that keeps our script in synch with the official Excel documentation.

After defining the two constants we create an instance of the Excel.Application object and then set the Visible property to True; that gives us a running instance of Execl that we can see on screen. That also brings us to this line of code:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls", 3)

As you can see, what we’re doing here is calling the Open method in order to open the workbook C:\Scripts\Test.xls. That’s fine; you’ve seen that a million times by now. But what’s that second parameter, the 3, doing tacked onto the end of the method call?

We’re glad you asked that question. The optional second parameter for the Open method determines what happens to all the links in the spreadsheet you’re about to open. We tacked on the value 3 because, upon opening, that causes all the links in the workbook to be updated. Alternatively, we could have set the second parameter to 2: that would open the file without updating any of the links. Or, because this is an optional parameter, we could leave it out altogether; in that case, each time we open the file Excel will ask us whether or not we want to update the links.

The long and short of it? Make 3 the second parameter to the Open method and you’ll get all your links updated.

And yes, that will be the case regardless of whether you have less than 20 links or more than 20 links.

Of course, updating the links is only half the battle: once the links have been updated we then need to break each of those links. Sound hard? Don’t worry; breaking things is what the Scripting Guys do best.

True story: Scripting Guy Jean Ross once ripped a door off its hinges just by turning the doorknob and pulling the door open. Ever since then whatever Scripting Guy Jean Ross wants Scripting Guy Jean Ross gets.

As near as we can tell, links within a workbook must be broken one at a time. With that in mind, we use the following line of code to retrieve a collection of all the Excel type links found in the file, stashing that collection in an array named arrLinks:

arrLinks = objWorkbook.LinkSources(xlLinkTypeExcelLinks)

After that we’re ready to start breaking links. To break those links one-by-one we set up a For Next loop that runs from 1 to the total number of links in the collection (that is, until we reach the upper bound – Ubound – of the array arrLinks):

For i = 1 to Ubound(arrLinks)

Note. As you know, most arrays start at 0; the index number of the first item in an array is almost always 0. For some reason, however, Microsoft Office arrays usually start with 1; the first item in the array has an index number of 1 rather than an index number of 0. That’s why our For Next loop starts at 1 rather than 0.

Just in case you were wondering.

So what are we going to do inside this For Next loop? We’re going to do this:

objWorkbook.BreakLink arrLinks(i), xlLinkTypeExcelLinks

As you can see, there’s nothing very fancy going on here; we’re simply calling the BreakLink method, passing that method two parameters:

· arrLinks(i), which is simply a reference to an individual link in the collection. The first time through the loop we’ll be working with the first link in the collection. How do we know that? Because the counter variable i starts off being equal to 1.

· xlLinkTypeExcelLinks, the constant that identifies the type of link we want to break.

That’s pretty much all we have to do; by the time we exit the loop all the links in the workbook will have been broken.

That should do it, YH. If you have any more questions about this please let us know. Oh, if you have 20 or fewer questions, be sure and send them to the Scripting Guys Express Lane. That way you can – oh, never mind. If you have 23 or 29 or 578 questions go ahead and send them all to the Express Lane. We know when we’re beat.


How Can I Use a Blank Row to Separate Data in an Excel Spreadsheet?

Hey, Scripting Guy! How can I use a blank row to separate rows of data in a Microsoft Excel spreadsheet?

-- AR

Hey, AR. You know, most people believe that the Scripting Guys all think alike, that we always agree on everything. That’s not true, though; instead, like any other team, we disagree from time-to-time. Sure, the team would be best-served by always agreeing with Greg, but for some strange reason we don’t.

Take, for example, the question of which is the best sport in the world. Peter Costantini, our resident bon vivant, would undoubtedly argue that soccer is the best sport in the world. Dean Tsaltas, Canadian-born and bred, would likely vote for hockey. (Of course, “hockey” is Dean’s answer to pretty much any question that comes up.) Jean Ross – well, Jean Ross thinks that car racing is a sport, which means that there’s no reason to consider her opinion, at least not on this question.

As it turns out, though, those three Scripting Guys are all wrong. The best sport in the world, and the truest test of athletic prowess, is this: miniature golf.

Yes, miniature golf, the sport of champions. And no, this has nothing to do with the fact that the Scripting Dad and Scripting Son squared off in miniature golf this past weekend; the Scripting Guy who writes this column has always believed miniature golf was the most grueling and most demanding of all sports. The fact that he shot a 61 – 11 under a par – and beat the Scripting Son by 13 strokes is totally irrelevant.

Although it does prove, once again, that the Scripting Dad is way better than the Scripting Son at anything sports-related.

Weight-lifting? Well, sure, the Scripting Son can bench press more than the Scripting Dad, but weight-lifting doesn’t seem all that sporting-like. Long-distance running? Come on, this is the age of the automobile; no one runs anymore. Throwing a ball harder, farther, and more accurately? Oh, please; name one sport in which throwing a ball hard, far, and accurately matters.

OK, fine. But the Scripting Dad still won the miniature golf showdown. Not only that, but he can write a script that uses a blank row to separate rows of data in a Microsoft Excel spreadsheet:

Const xlShiftDown = -4121

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

i = 1

strStartValue = Left(objExcel.Cells(i, 1), 1)

Do Until objExcel.Cells(i, 1) = ""
    strValue = Left(objExcel.Cells(i, 1), 1)
    If strValue <> strStartValue Then
        Set objRange = objExcel.Cells(i,1).EntireRow
        objRange.Activate
        objRange.Insert xlShiftDown
        strStartValue = Left(objExcel.Cells(i + 1, 1), 1)
    End If
    i = i + 1
Loop

Before we talk about how this script works we need to explain what AR’s spreadsheet looks like. AR has a spreadsheet that looks something like this:

As you can see, in this spreadsheet there are five different users (listed in column A):

· A Person

· B Person

· C Person

· D Person

· E Person

The spreadsheet contains one or more rows of data for each of these users; for example, user A Person has data listed in the first three rows. What AR would like to do is insert a blank row between the data for A Person and the data for B Person, eventually ending up with a spreadsheet that looks like this:

But how in the world are we supposed to do that?

Well, to begin with, we create a constant named xlShiftDown and set the value to -4121; we’ll use this constant to tell Excel to shift cells down any time we insert a new, blank row. (If we were working with columns, we could use the constant xlShiftRight, with a value of -4161, to move cells to the right.) We then use this block of code to create a running instance of Excel that we can see onscreen, and to open the file C:\Scripts\Test.xls:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")
Set objWorksheet = objWorkbook.Worksheets(1)

Now comes the fun part. To begin with, we assign the value 1 to a counter variable named i; we’ll use this variable to keep track of our position in the spreadsheet. We then use this line of code to grab the first character in cell 1,1 and store it in a variable named strStartValue:

strStartValue = Left(objExcel.Cells(i, 1), 1)

Note. In case you’re wondering, objExcel.Cells(i, 1) gives us the value of cell A1 (row 1, column 1). We then use the Left function to grab just the first character in that value.

The tricky part here is knowing when to insert a blank row; after all, we want to insert a blank row only when we’re done with one user’s data and are moving on to another user’s data. (Needless to say, this script isn’t going to work very well unless the spreadsheet has been sorted by user name. For a programmatic way to sort data take a look at this Office Space article.) How will we figure out when we’re finished with the data for our first user, A Person? Well, we’re going to do that by storing the user data found in the first row in strStartValue, then looking one row ahead to see if the data in that row matches. If it does, then we know we’re still working with A Person. If it doesn’t, that means we’re now working with a new user.

Don’t worry; that should make a little more sense as we walk through the script.

To get things rolling, we set up a Do Until loop designed to loop until we encounter a blank cell in column A:

Do Until objExcel.Cells(i, 1) = ""

Inside that loop, the first thing we do is grab the initial character in the current row and store it in a variable named strValue:

strValue = Left(objExcel.Cells(i, 1), 1)

We now have the first character in cell A1 stored in two different variables: strStartValue and strValue. Our next step is to compare these two values and see if they are different:

If strValue <> strStartValue Then

The first time through the loop the values are not different; both strStartValue and strValue are equal to A. Therefore, we skip the If Then statement, increment the value of i by 1, then loop around and repeat the process with the second row in the spreadsheet. The important thing to note here is that we did not change the value of strStartValue. That variable still holds the letter A, the first character in cell A1. Because we incremented the value of i by 1, however, strValue will now be equal to the first character from cell A2.

See how that works? Good. Now let’s move on.

Sooner or later we’re going to find a discrepancy between strStartValue and strValue; notice that the first character in cell A4 is a B. What happens when strStartValue and strValue no longer match? This happens:

Set objRange = objExcel.Cells(i,1).EntireRow
objRange.Activate
objRange.Insert xlShiftDown
strStartValue = Left(objExcel.Cells(i + 1, 1), 1)

In the first line of code we’re creating a Range object that encompasses all of row 4; we do that by specifying cell A4 (objExcel.Cells(i, 1) and then referencing the EntireRow property. We then use the Activate method to make this the active row in the spreadsheet:

objRange.Activate

Basically what we’ve done here is moved the cursor down to cell A4 and then selected the entire row. With the row selected we use the Insert method to insert a blank row, using the parameter xlShiftDown to shift the existing cells down. What does that mean? That means that the data currently in row 4 will be shifted down to row 5, and row 4 will become a blank row.

Which is exactly what we wanted to happen.

We then use this line of code to grab the first character in row 5 (i + 1) and store that data in strStartValue:

strStartValue = Left(objExcel.Cells(i + 1, 1), 1)

That makes strStartValue equal to B.

We then increment the value of i by 1, loop around, and start the process all over again. Granted, it’s a bit confusing. But it works. Give it a try and you’ll see.

Incidentally, the more we think about it the more we’re wondering if miniature golf really is the best sport in the world. For one thing, it cost the Scripting Dad $16 for he and the Scripting Son to play. For another, what did the Scripting Dad get for winning? You got it: nothing.

Now, compare that to, oh, say, the 2007 Winter Scripting Games. How much will cost you to enter the Scripting Games? Zilch; the Scripting Games are always free to everyone. And what do you get if you win? Well, how about a Dr. Scripto Bobblehead doll or a copy of the book Windows PowerShell in Action? Not bad, huh? You know, come to think of it, maybe the Winter Scripting Games are the best sport in the entire world.

Well, unless it turns out that the Scripting Son actually can write scripts better than the Scripting Dad. In that case, we’ll go back to miniature golf as being the best sport in the world.


How Can I Use Windows PowerShell to Automate Microsoft Excel?

Hey, Scripting Guy! How can I use Windows PowerShell to automate Microsoft Excel?

-- MW

Hey, MW. Yes, it’s true: the week of November 6-10, 2006 will be Windows PowerShell Week on TechNet, with the Scripting Guys presenting a series of webcasts (one each day, Monday through Friday) designed to introduce you to Windows PowerShell. Those are all the details at the moment (we’ll keep you posted), but you can bet that the week will be filled with fun and excitement as we show how to carry out all sorts of useful system administration tasks using Windows PowerShell.

What’s that? Well, we suppose it is like a dream come true, assuming that you always dream about Windows PowerShell, of course. And no, that’s OK: all things considered, we’d just as soon not know what it is you always dream about.

Note. What do the Scripting Guys dream about? Surprisingly enough, none of the Scripting Guys dream. But, then again, maybe that’s not so surprising: after all, when you work at Microsoft you’re living the dream.

More or less.

So, MW, are we going to make you wait until November before we show you how you can use Windows PowerShell to automate Microsoft Excel? Well, to tell you the truth, that was the original plan. But then we realized that we’d have to go read a few more emails, that we’d have pick out a different question to answer, that we’d – well, needless to say, that sounded like an awful lot of work. Especially when you consider how easy it was to just write a sample script that uses Windows PowerShell to automate Microsoft Excel:

$a = New-Object -comobject Excel.Application

$a.Visible =
$True

$b = $a.Workbooks.Add()
$c = $b.Worksheets.Item(1)

$c.Cells.Item(1,1)
= "A value in cell A1."
$b.SaveAs("C:\Scripts\Test.xls")

$a.Quit()

The key to this script occurs in the very first line, the one where we use the New-Object Cmdlet to create an instance of Microsoft Excel. Notice that we need to pass New-Object two items: the –comobject parameter, which tells the Cmdlet that we want to create a new COM object (as opposed to, say, a new .NET Framework object); and Excel.Application, the ProgID of the object we want to create. And no, we didn’t make a mistake (well, not here anyway): you don’t need to enclose the ProgID in double quote marks.

And you wondered why people were so excited about Windows PowerShell!

In effect, line 1 is equivalent to this line of VBScript code:

Set a = CreateObject("Excel.Application")

Although the lines are functionally equivalent, there are a couple differences we should point out. For one, in Windows PowerShell we don’t have to use the Set keyword (or some equivalent) in order to create an object reference. For another, we do need to use a variable name like $a; in Windows PowerShell variable names must begin with a dollar sign ($).

Minor things, sure, but things you need to know.

Now, back to the script. In the next line we set the Visible property of Excel to true, thus enabling us to see the application onscreen:

$a.Visible = $True

That should be reasonably familiar: you set property values in Windows PowerShell using the “dot” notation just like you do in VBScript. The only real difference here? We set the Visible property to $True, prefacing the built-in variable with a dollar sign. Remember when we said you had to preface variable names with a dollar sign? There's a reason why we mentioned that.

Come to think of it, yes, that just might be the first time we ever had a valid reason for saying something in this column.

That brings us to the following two lines of code, lines where we use the Add method to add a new workbook to our instance of Excel, then use the Item property to create an object reference to the first worksheet in that workbook:

$b = $a.Workbooks.Add()
$c = $b.Worksheets.Item(1)

Thanks for pointing that out: the second line is a tad bit different than what you’d see in VBScript, isn’t it? In VBScript we create an object reference to worksheet 1 using code similar to this:

Set c = b.Worksheets(1)

That’s easy enough: we want worksheet 1 so we specify Worksheets(1). Then why, in Windows PowerShell, do we refer to worksheet 1 as Worksheets.Item(1)?

Well, as it turns out, the Item property is the default property for the Worksheet object. In VBScript you can usually get away without explicitly referencing the default property; if you don’t specify a property name then VBScript uses the default property. That’s not the case with Windows PowerShell, however. Suppose we left out the default property and tried creating an object reference using code like this:

$c = $b.Worksheets(1)

What's going to happen in that case? This:

Method invocation failed because [System.__ComObject] doesn't contain a method named 'Worksheets'.

In other words, make sure you always include the property name, even when dealing with the default property.

Note. Interestingly enough, VBScript lets you do this any which way you want. Want to leave out the default property name? That's fine. But what if you want to reference the default property name? Then go ahead and do so; this line of code works just fine in VBScript:

Set c = b.Worksheets.Item(1)

The next two lines of code are there just to show you how to actually do something with Excel once the application is up and running. In line 1, we add some text to cell A1 (row 1, column 1); again, notice that we explicitly referenc the Item property when dealing with the cells in the worksheet. After modifying cell A1 we then call the SaveAs method to save the spreadsheet as C:\Scripts\Test.xls:

$c.Cells.Item(1,1) = "A value in cell A1."
$b.SaveAs("C:\Scripts\Test.xls")

Speaking of methods, any time you call a method in Windows PowerShell you must include parentheses on the end, even if those parentheses are empty. For example, in VBScript you can call the Save method using code similar to this:

b.Save

That won’t do you much good in Windows PowerShell, however. Instead, you need to tack on a set of parentheses, which tells Windows PowerShell that we’re using a method:

$b.Save()

And, yes, we know. But you’ll get used to it in no time.

After we save the spreadsheet we call the Quit method (there’s those parentheses again) and we're done:

$a.Quit()

Hope that gets you started, MW. As for everyone else, if today’s column seemed a bit mystifying, mainly because you’ve never even heard of Windows PowerShell, well, you might want to take a peek at our Windows PowerShell center. Oh: and did we mention that Windows PowerShell Week is coming November 6-10, 2006? Well, we should have: undoubtedly those 5 webcasts will represent the best thing that’s happened to Windows PowerShell since … well, Windows PowerShell is brand-new, so not a lot has actually happened to it yet. But you know what we mean.

Update: Windows PowerShell Week is over, but you can watch the webcasts on-demand anytime.


How Can I Change the User Information in Microsoft Office?

Hey, Scripting Guy! How can I change the user information in Microsoft Office to match the user’s name as stored in Active Directory?

-- SM

Hey, SM. Back in the days when one of the Scripting Guys worked for a local university, every document in one department of 300+ users appeared to have been created by the same person; that’s because a single help desk technician installed Office on every machine and, lacking any other instructions, simply entered his name as the default user name. Because this technician was allegedly the user on every copy of Office, every Office document showed him as the author. Even after he left the university his legacy lived on; that’s because no one knew of a quick and easy way to change that information. JC, you might be gone, but you were never forgotten.

Oh, if only they had had Scripting Guys back in those days! It’s actually pretty easy to change the user information in Office, and to make it match the user’s name as found in Active Directory to boot. Let’s first show you a generic, hard-coded script for changing user information, and then we’ll talk about how to make that script a bit more dynamic.

We should note that we’re going to use Microsoft Word to change the user information. We could also use Microsoft Excel or Microsoft PowerPoint; using any of these applications will change user information for Microsoft Office as a whole. However, Excel and PowerPoint allow you to change only the user’s name; they don’t allow you to change the user’s initials. That could be a problem: in Microsoft Word (the only place – we think – that user initials make themselves known) you could end up with a user named Ken Myer who had the initials JC. With Word we can change both the name and the initials. So we decided to use Word.

And yes, this script does require you to have Word installed on the user’s computer. But if you don’t have Word installed then you probably don’t have Office installed, making this a moot point.

Here’s what our script looks like:

Set objWord =
CreateObject("Word.Application")
objWord.UserName = "Ken Myer"
objWord.UserInitials = "KM"
objWord.Quit

That’s right: just 4 lines of code. We begin by creating an instance of the Word.Application object. You might note that in this script – unlike most of our Microsoft Office scripts – we don’t set the Visible property to True. That’s because we don’t want to view Word onscreen; we just want to start it, change the user information, then dismiss it. There’s no need for anyone to see that process take place.

After starting Word we use these two lines of code to change the value of the UserName and UserInitials properties:

objWord.UserName = "Ken Myer"
objWord.UserInitials = "KM"

That’s it: we call the Quit method to terminate our instance of Word, and we’re done. The next time you start Word – or any Office application – the user name will show up as Ken Myer and the user initials as KM.

This script works great as long as you know – in advance – the name of the user logged on to the computer. But what if you don’t know the user name in advance, or what if the computer might have multiple users? How can you determine the user name and then change the Office user information on-the-fly?

Why you simply use this script, of course:

Set objSysInfo =
CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

Set objWord = CreateObject("Word.Application")
objWord.UserName = objUser.givenName & " " & objUser.SN
objWord.UserInitials = Left(objUser.givenName, 1) & Left(objUser.SN, 1)
objWord.Quit

Don’t worry, we’ll explain what’s going on here. Before we do so, we should note that we’re assuming this script is being run as a logon script; that way it will run each time a user logs on to a computer and will then modify the Office user information accordingly. This particular script can’t be run remotely (at least not easily) simply because the ADSystemInfo object can only be created locally.

Of course, that leads to an obvious question: what is the ADSystemInfo object and why do we even need it? Well, the ADSystemInfo object can be used to grab some useful Active Directory-related information about the logged-on user, including said user’s Distinguished Name. The Distinguished Name is an Active Directory property that provides a direct path to the user account, a path that looks something like this:

CN=kenmyer, OU=Finance, DC=fabrikam, DC=com

If we know the Distinguished Name for a user account we can bind to that account and retrieve all sorts of cool information, including the values of the givenName (first name) and SN (surname, or last name) attributes.

So how do we bind to the user account? We just use these two lines of code:

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

In the first line we store the value of the UserName property in a variable named strUser; the UserName property (as derived from the ADSystemInfo object) just happens to be the user’s Distinguished Name. In line 2 we then combine that value with the LDAP:// provider, creating an ADsPath similar to this:

LDAP://CN=kenmyer, OU=Finance, DC=fabrikam, DC=com

By passing the ADsPath to the GetObject method we can connect to the logged-on user’s Active Directory user account. And once we’re there, we’re pretty much home free.

In fact, the rest of the script is just a variation of the hard-coded script we showed you at the beginning of today’s column. We create an instance of the Word.Application object, then use this line of code to configure the user name:

objWord.UserName = objUser.givenName & " " & objUser.SN

As you can see, we’re concatenating the givenName, a blank space, and the SN, resulting in a name like Ken Myer. That’s pretty easy.

Setting the user initials is just a tiny bit trickier; that’s because Active Directory doesn’t have a property for storing user initials. Therefore we have to extract the initials from the user’s first and last names. To do that we use the Left function and this line of code:

objWord.UserInitials = Left(objUser.givenName, 1) & Left(objUser.SN, 1)

To grab the first letter of the user’s first name we use this code:

Left(objUser.givenName, 1)

We simply pass the Left function two parameters: objUser.givenName, which contains the user’s first name, and the value 1. The 1 instructs the function to start at the left-hand side of the string and extract 1 character; if the user’s first name is Ken this function will return the letter K. We then repeat the process to grab the first letter of the last name, then jam the two letters together to form the user initials.

Run the preceding script as a logon script, and each time a user logs on the user information for Microsoft Office will be set accordingly; in turn, that means your Office documents will show the real author, not just the person who installed the software. Sorry, JC, but you know what they say: nothing lasts forever.


How Can I Get a List of Available Metadata for Microsoft Office Documents?

Hey, Scripting Guy! How can I get a list of available metadata for Microsoft Office documents?

-- JR

Hey, JR. If we understand your question correctly, you’d like to know if it’s true that July 21, 2006 will mark the 500thHey, Scripting Guy! column; in addition, you’d like to know whether we are planning to do anything to mark this auspicious occasion. Well, the answer to both questions is yes. To mark the 500thHey, Scripting Guy! we’re sponsoring a special contest: all you have to do is send us a postcard and you’ll be entered in a drawing where you could win a Hewlett-Packard iPAQ Pocket PC or – even better—a Scripting Guys T-shirt! For more details (and to find out how to enter) see the 500th Hey, Scripting Guy! Article Celebration and Giveaway home page.

And, yes, technically you can send us an electronic postcard. But that doesn’t sound half as much fun as getting a real postcard, if you know what we mean.

Anyway, thanks for writing, JR. Hope we were able to help.

Hmmm, now that you mention it, we guess your question did say something about metadata, didn’t it? Wonder how we could have missed that? But, what the heck: we have some time to kill while we wait for the postcards to start rolling in, so let’s see if we can show you how to get a list of the available metadata for a Microsoft Office document.

To begin with, we should note that there is actually a separate COM object (Dsofile.exe) that you can download and use to get metadata from Microsoft Office documents. The advantage of using Dsofile is that it can retrieve metadata from any Microsoft Office document. The disadvantage to Dsofile? You’ll obviously have to download and install it on every computer where you might need to run your metadata-retrieving script. Because a lot of people don’t like to download and install additional utilities, we won’t discuss Dsofile today; if you’re interested, take a look at the Tales From the Script column Dsofile: The Untold Story.

Instead, what we’re going to do today is show you how to get at metadata (also referred to as document properties) using plain old Microsoft Office. The nice thing about this, of course, is that there’s nothing to download or install: if you’ve got Microsoft Office up and running then you’ve already got everything you need. The one disadvantage: you’ll have to modify your script slightly depending on the type of document you want to examine. But don’t worry, we’ll explain how to do that. Because you asked a fairly simple question – How can I get a list of available metadata for Microsoft Office documents? – let’s start off with an equally-simple script, one that retrieves metadata for an Excel spreadsheet (C:\Scripts\Test.xls):

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

For Each strProperty in objWorkbook.BuiltInDocumentProperties
    Wscript.Echo strProperty.Name
Next

Yes, it is a very simple little script. We start out by creating an instance of the Excel.Application object. And you’re right: because we’re using Excel we’ve pretty much limited ourselves to retrieving information about Excel spreadsheets. What if we wanted to retrieve information about Word documents? In that case, we’d need to use the Word.Application object. OK, then how about PowerPoint presentations? That’s easy: we’d need to use the PowerPoint.Application object. Microsoft Publisher? That’s right: Publisher.Application. And so on.

After creating the Excel.Application object we use this line of code to make Excel visible onscreen:

objExcel.Visible = True

Needless to say, we don’t need to do this: the script works just fine even if you can’t see Excel onscreen. We do this primarily to help you visualize what’s going on. If you want to run the script without ever seeing the spreadsheet then simply leave out this line of code. Of course, each time you run the script you’ll also end up with an invisible copy of Excel running in the background. To combat that, just make sure you call the Quit method to dismiss Excel when the script finishes. The “invisible” version of this script looks like this:

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

For Each strProperty in objWorkbook.BuiltInDocumentProperties
    Wscript.Echo strProperty.Name
Next

objExcel.Quit

After getting Excel up and running we then use this line of code to open the spreadsheet C:\Scripts\Test.xls:

Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

So far so good. Now, how do we get to the metadata? Well, it turns out that Microsoft Office documents store their metadata in a collection named BuiltInDocumentProperties. To get a list of available metadata for a Microsoft Excel spreadsheet all we have to do is set up a For Each loop to walk through this collection, echoing back the Name of each item (each piece of metadata) found in the collection:

For Each strProperty in objWorkbook.BuiltInDocumentProperties
    Wscript.Echo strProperty.Name
Next

What does that give us? That gives us something very much like this:

Title
Subject
Author
Keywords
Comments
Template
Last author
Revision number
Application name
Last print date
Creation date
Last save time
Total editing time
Number of pages
Number of words
Number of characters
Security
Category
Format
Manager
Company
Number of bytes
Number of lines
Number of paragraphs
Number of slides
Number of notes
Number of hidden Slides
Number of multimedia clips
Hyperlink base
Number of characters (with spaces)

There’s your available metadata.

But wait: we aren’t done yet. After all, that script shows us all the metadata that could be configured for Test.xls. But what if we’d like to find out which metadata has been configured for Test.xls? Can we get at that information? Of course we can; this script returns metadata property names and configured values:

On Error Resume Next

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls")

For Each strProperty in objWorkbook.BuiltInDocumentProperties
    Wscript.Echo strProperty.Name & " – " & strProperty.Value
Next

The only difference between this script and our first script? This line right here, where we echo back not only the item Name but also the item Value:

Wscript.Echo strProperty.Name & " – " & strProperty.Value

Oh: and we also added On Error Resume Next to the beginning of the script. That’s important: in some cases the script will crash upon encountering a property that has never been configured. To guard against that possibility, just use On Error Resume Next.

The net result will be similar to this, depending on which properties have values assigned to them and which ones do not:

Title – Metadata Test
Subject – Excel Test Scripts
Author – Ken Myer
Keywords – testing, scripts
Comments – This is a sample spreadsheet used for testing purposes.
Template -
Last author – Ken Myer
Revision number -
Application name - Microsoft Excel
Creation date – 6/13/2006 8:40:17 PM
Last save time - 6/13/2006 9:07:15 AM
Security - 0
Category -
Format -
Manager -
Company - Microsoft Corporation
Hyperlink base -

Ah, we had a feeling you were going to ask that. Yes, you can use a script to programmatically set the value of some of these properties. Some properties – such as the document Creation date or Number of characters – are read-only, for obvious reasons; however, other properties – such as Title, Subject, and Author – can be configured using a script similar to this:

On Error Resume Next

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Book1.xls")

For Each strProperty in objWorkbook.BuiltInDocumentProperties
If strProperty.Name = "Title" Then
strProperty.Value = "Test
Title"
End if
Next

Again, this is fairly simple. After setting up our For Each loop we use this line of code to check each document property to see if the Name of the property is equal to Title:

If strProperty.Name = "Title" Then

If it is, then we use this line of code to set the Value of the property to Test Title:

strProperty.Value = "Test Title"

Give it a try and see what happens.

Two quick notes. Yes, there might be a slightly faster way to get at the value of an individual property like Title. As usual, though, we went for the approach we thought was easier rather than the approach that might have been a tiny bit faster. Also, as you doubtless know, you can add your own, custom metadata to Microsoft Office documents. Can you get at those properties using a script? You bet: just use the CustomDocumentProperties collection instead of the BuiltInDocumentProperties collection.

Oh, and did we mention that all you have to do is send us a postcard and you could win a really cool prize? Well, we should have.


How Can I Write to the Comments Field of a Document?

Hey, Scripting Guy! Can I add text to the Comments field of a file using a script?

-- EF

Hey, EF. For those of you who aren’t sure what EF is referring to, right-click a document in Windows Explorer and then click Properties. In the dialog box that appears, click on the Summary tab. See the text box labeled Comments? That’s the field EF would like to be able to populate using a script.

So is that possible? As a matter of fact it is, provided you go to the Downloads Center on Microsoft.com and download Dsofile. Dsofile installs a new COM object that allows you to both read from and write to the summary information fields for a document. For example, here’s a script that adds the clever comment “This is a comment” to the Comments field for a document:

Set objPropertyReader = CreateObject("DSOleFile.PropertyReader")
Set objDocument = objPropertyReader.GetDocumentProperties _
    ("C:\Scripts\Test.doc")
objDocument.Comments = "This is a comment."

Pretty slick, huh? Summary information fields you can change using a script include: Author; Category; Comments; Company; Keywords; LastEditedBy; Manager; Subject; and Title.

And, of course, you can read the summary information fields as well:

Set objPropertyReader = CreateObject("DSOleFile.PropertyReader")
Set objDocument = objPropertyReader.GetDocumentProperties _
    ("C:\Scripts\Test.doc")
Wscript.Echo "App name: " & objDocument.AppName
Wscript.Echo "Author: " & objDocument.Author
Wscript.Echo "Byte count: " & objDocument.ByteCount
Wscript.Echo "Category: " & objDocument.Category
Wscript.Echo "Character count: " & objDocument.CharacterCount
Wscript.Echo "Character count with spaces: " & objDocument.CharacterCountWithSpaces
Wscript.Echo "CLSID: " & objDocument.CLSID
Wscript.Echo "Comments: " & objDocument.Comments
Wscript.Echo "Company: " & objDocument.Company
Set colCustomProperties = objDocument.CustomProperties
For Each strProperty in colCustomProperties
    Wscript.Echo vbTab & strProperty.Name & ": " & strProperty.Value
Next
Wscript.Echo "Date created: " & objDocument.DateCreated
Wscript.Echo "Date last printed: " & objDocument.DateLastPrinted
Wscript.Echo "Date last saved: " & objDocument.DateLastSaved
Wscript.Echo "Has macros: " & objDocument.HasMacros
Wscript.Echo "Hidden slides: " & objDocument.HiddenSlides
Wscript.Echo "Icon: " & objDocument.Icon
Wscript.Echo "Is read only: " & objDocument.IsReadOnly
Wscript.Echo "Keywords" & objDocument.Keywords
Wscript.Echo "Last edited by: " & objDocument.LastEditedBy
Wscript.Echo "Line count: " & objDocument.LineCount
Wscript.Echo "Location: " & objDocument.Location
Wscript.Echo "Manager: " & objDocument.Manager
Wscript.Echo "Multimedia clips: " & objDocument.MultimediaClips
Wscript.Echo "Name: " & objDocument.Name
Wscript.Echo "Page count: " & objDocument.PageCount
Wscript.Echo "Paragraph count: " & objDocument.ParagraphCount
Wscript.Echo "Presentation format: " & objDocument.PresentationFormat
Wscript.Echo "Presentation notes: " & objDocument.PresentationNotes
Wscript.Echo "ProgID: " & objDocument.ProgID
Wscript.Echo "Revision number: " & objDocument.RevisionNumber
Wscript.Echo "Slide count: " & objDocument.SlideCount
Wscript.Echo "Subject: " & objDocument.Subject
Wscript.Echo "Template: " & objDocument.Template
Wscript.Echo "Thumbnail: " & objDocument.Thumbnail
Wscript.Echo "Title: " & objDocument.Title
Wscript.Echo "Version: " & objDocument.Version
Wscript.Echo "Word count: " & objDocument.WordCount

Before you ask, we don’t know of any method built into the operating system that enables you to modify the summary information fields; you’ll have to download Dsofile to do that. It is possible, however, to read at least some of this information using the Windows Shell object. For details, check out this section of the Microsoft Windows 2000 Scripting Guide.


How Can I Connect to the Junk Mail Folder in Outlook?

Hey, Scripting Guy! Over the past year or so you’ve answered several questions in which you use a script to connect to the Inbox in Microsoft Outlook. But how can I connect to the Junk Mail folder in Outlook?

-- ME

Hey, ME. Let’s see, what time is it? OK good: we have enough time to answer this question and get the column posted. We were a little worried about that, because the Scripting Guy who writes this column has been running late all day today. But that’s not entirely his fault; for example, after taking a shower this morning he was drying himself off when his eye caught this notice on the bath towel label:

Not suitable for wearing apparel.

A bath towel, by itself, is not suitable for wearing apparel?!? Why didn’t someone tell him that earlier? As you might expect, that forced him to rethink his wardrobe for the day, and got him off to a bad start before the day ever really got started.

Note. Although, admittedly, rethinking his wardrobe isn’t really that big of a deal; most days that simply involves picking between the black hooded-sweatshirt and the grey hooded-sweatshirt. Today he went with grey. Tomorrow? Who knows?

Fortunately, the Scripting Guy who writes this column was able to make it to work on time, and – for once – showed up suitably attired. In turn, that gave him enough time to come up with a simple little demonstration script that connects to the Junk Mail folder in Outlook and reports back the number of items in that folder:

Const olFolderJunk = 23

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderJunk)

Set colItems = objFolder.Items
Wscript.Echo colItems.Count

Pretty simple, isn’t it? Not only that, but the key to this script comes in the very first line, the line where we define a constant named olFolderJunk and set the value to 23:

Const olFolderJunk = 23

As you know, Outlook contains a number of predefined folders, including the Inbox, the Calendar, the Contacts folder, and, of course, the Junk Mail folder. Connecting to one of these folders – as we’re about to see – is as simple as passing the appropriate value to the GetDefaultFolder method. For example, to bind to the Junk Mail folder you need to pass the value 23 (either directly, or by using a constant) to GetDefaultFolder.

Note. Is it just a coincidence that 23 is also the number of flavors blended together to make Dr. Pepper? Unfortunately, we can’t help you there; that’s the sort of thing everyone will have to decide for themselves.

After we define the constant olFolderJunk we create an instance of the Outlook.Application object and then use the GetNamespace method to bind to the MAPI namespace. (Which, interestingly enough, is the only namespace we can bind to.) We then use this line of code to create an object reference to the Junk Mail folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderJunk)

That’s basically the whole script right there. In the last two lines of code we retrieve a collection of all the items in the Junk Mail folder and then echo back the value of the Count property; that simply tells us how many junk emails are currently in the folder. Obviously that’s not required; we added that code simply so the script would do something besides bind to the folder. Alternatively, we could have used a script like this one to go ahead and delete all the items in the folder:

Const olFolderJunk = 23

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderJunk)

Set colItems = objFolder.Items

For Each objItem in colItems
    objItem.Delete
Next

Cool, huh?

In case you’re wondering here are the constants (and their corresponding values) for the other default folders built into Microsoft Outlook:

Constant | Value
olFolderCalendar | 9
olFolderContacts | 10
olFolderDeletedItems | 3
olFolderDrafts | 16
olFolderInbox | 6
olFolderJournal | 11
olFolderJunk | 23
olFolderNotes | 12
olFolderOutbox | 4
olFolderSentMail | 5
olFolderTasks | 13
olPublicFoldersAllPublicFolders | 18
olFolderConflicts | 19
olFolderLocalFailures | 21
olFolderServerFailures | 22
olFolderSyncIssues | 20

So what are we supposed to do with all this information? Well, according to the table, the Drafts folder has a value of 16. That means a simple little script like this will tell you how many items are in the Drafts folder:

Const olFolderDrafts = 16

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderDrafts)

Set colItems = objFolder.Items
Wscript.Echo colItems.Count

Etc., etc.

By the way, we have a confession to make: the Scripting Guy who writes this column didn’t really consider wearing a towel to work today. After all, thanks to the last such … incident … one of his fellow Scripting Guys always calls every morning to remind him that a towel is not suitable for wearing apparel. He learned his lesson, the hard way.


How Can I Convert an Outlook Email Message into a Text File?

Hey, Scripting Guy! How can I convert an Outlook email message into a text file?

-- MW

Hey, MW. You know what drives the Scripting Guys crazy? (We mean besides the other Scripting Guys.) It’s this: people who barely say hello before they start making excuses:

· “I’m sorry I did such a lousy job on this, but ….”

· “I realize that I was supposed to have this finished today, but ….”

· “I know I should have stopped to pull you out of that burning building, but ….”

The Scripting Guys don’t believe in excuses: either you can do something or you can’t. Period.

On the other hand, the Scripting Guys do believe in caveats. And that’s exactly what you’re going to get right off the bat. Can we show you how to convert an Outlook email message into a text file? You bet we can, but you have to realize that we’ll be able to show you only the simplest such example: converting the first item in your Inbox to a text file. What if you want to be a little more specific? For example, what if you’d like to convert a message with a specific subject line or a message sent on a specific date to a text file? Unfortunately, we won’t be able to show you that in today’s column; if you’d like to know how to locate specific messages take a look at the Office Space column Filtering Email Messages in Microsoft Outlook.

Another excuse – um, another caveat is that we can’t give you a fully-automated solution here; that’s due to the security built into Outlook. In order to save an email message as a text file you need to be able to access certain properties (such as the body of the message) that trigger a security alert in Outlook. That means that when you run the script a security warning dialog box will pop up. Before the script can continue (and before the message can be saved) you will need to click Yes in that dialog box. (And it must be an actual click; you can’t use SendKeys to emulate a mouse click.) Your script will work, but (there’s that word again) you can’t just schedule the script to run by itself; you’ll need to be there when the script runs so you can take care of the dialog box.

As always seems to be the case, the … caveats … take up way more space than the script. Here’s a script that saves the first message in your Inbox to a file named C:\Scripts\MailMessage.txt:

Const
olFolderInbox = 6
Const olTxt = 0

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colMailItems =  objFolder.Items

Set objItem = colMailItems.GetLast()
objItem.SaveAs "C:\Scripts\MailMessage.txt", olTxt

We begin by defining a pair of constants: olFolderInbox (with a value of 6), which we’ll use to indicate the Outlook folder we want to connect to; and olTxt (with a value of 0) that tells Outlook in which format to save our mail message. We then have three lines of code that connect us to Outlook and the MAPI namespace and bind us to the Inbox folder (using the GetDefaultFolder method):

Set
objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

After we’ve made the connection to the Inbox the rest is easy. We use this line of code to return a collection of all the items found in the Inbox:

Set colMailItems =  objFolder.Items

Once we have that collection in hand we use the GetLast() method to bind to the first item in the collection (that is, the first mail message in the Inbox). We then call the SaveAs method to save the message, passing SaveAs a pair of parameters: the complete path for the text file (C:\Scripts\MailMessage.txt) and the constant olTxt.

Simple, straightforward, and, best of all, no excuses. Just the way we Scripting Guys like it.


How Can I Create a New Folder in Microsoft Outlook?

Hey, Scripting Guy! How can I create a new folder in Microsoft Outlook?

-- TY

Hey, TY. You know, this is an exciting time here at Microsoft; after all, last night at midnight we officially released one of the most highly-anticipated software products ever created: a script that can create a new folder in Microsoft Outlook!

Oh, wait; sorry. We’re releasing that script right now. Last night we released some video game. Halo Something-Or-Other.

No, hey, just kidding: even people as out-of-it as the Scripting Guys know about Halo 3. After all, if you watch TV, read magazines, drink soda or do just about anything then you can’t help but know about Halo 3.

And the Scripting Guys watch a lot of TV and drink a lot of soda.

In case you’re wondering, however, the answer is: no, the Scripting Guy who writes this column did not stand in line at midnight in order to be one of the first people to receive a copy of Halo 3. To be honest, the Scripting Guy who writes this column will probably never even play Halo 3, let alone get his very own copy of it. Why won’t he play Halo 3? Well, a few years ago the Scripting Guy who writes this column got talked into playing the original Halo with his then-13-year-old son and his two nephews. The boys are still laughing about that.

How bad could that have been? Well, shortly after killing his own teammate – by accident, we hasten to add – the Scripting Guy who writes this column stumbled upon some flying vehicle, a flying vehicle armed with deadly missiles or bombs or something. “Oh, you are so lucky,” said the Scripting Son. “You can’t possibly lose now.”

And here we thought the Scripting Son knew his father better than that.

Note. Does that mean that the Scripting Dad had problems flying this vehicle? No, he had trouble just getting into the vehicle. Regardless of which button or combination of buttons he pushed, he could never get onboard.

“Dad, just press the X button,” the Scripting Son kept saying.

“I am pressing the X button!” the Scripting Dad kept replying. “Wait: which one is the X button?”

Fortunately one of the nephews came by and, to put it euphemistically, put the Scripting Dad out of his misery.

Besides, last night at midnight we were standing in line to get our hands on this baby: Microsoft Outlook Folder Creator 1. At the risk of making everyone out there jealous, here’s what the script looks like (you can look, but don’t touch):

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set objNewFolder = objFolder.Folders.Add("Scripts
Received")

Granted, this has nowhere near as many lines of code as Halo 3, and the graphics aren’t quite as nice, either. But, then again, how many Microsoft Outlook folders can you create using Halo 3?

Really? That many, huh? Wow; we never would have guessed that.

Note. Well, if nothing else our script is still cheaper than Halo 3. Although, admittedly, that’s only because Microsoft won’t let us charge people to read this column.

Let’s see if we can figure out how Microsoft Outlook Folder Creator 1 works. We start out by defining a constant named olFolderInbox and setting the value to 6; we’ll use this constant when we bind to the Inbox folder in Microsoft Outlook. After defining the constant, we create an instance of the Outlook.Application object, then use the GetNamespace method to bind to the MAPI namespace. (And yes, the MAPI namespace is the only namespace you can bind to. Nevertheless, you still have to call the GetNamespace method and make the connection.)

From there we use this line of code to return an object reference (objFolder) to the Inbox folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Once we’ve done that we can create a new folder simply by calling the Add method (part of the Inbox’s Folders collection), passing the name of the new folder as the sole method parameter:

Set objNewFolder = objFolder.Folders.Add("Scripts Received")

Just like that our Inbox has a new subfolder named Scripts Received.

Note. At the risk of tooting our own horn, Halo 3 won’t come right out and tell you how it ends; you have to play – and win – the game before you can find out the dramatic conclusion. The Scripting Guys would never make you do that; instead, we tell you flat out exactly how our script ends: it creates a new folder named Scripts Received.

As we noted, our new folder is a subfolder of the Inbox. That’s fine … unless you wanted this new folder to be at the same level as the Inbox (along with folders like Deleted Items, Junk Mail, and Sent Items). If that’s the case, well, too bad; you’re just going to have to wait until Microsoft Outlook Folder Creator 2 is released.

Fortunately, that release date turns out to be right about now:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

strFolderName = objFolder.Parent
Set objMailbox = objNamespace.Folders(strFolderName)

Set objNewFolder = objMailbox.Folders.Add("Scripts
Received")

The tricky part with this script is binding to the Outlook Mailbox as opposed to the Inbox. As far as we know, there’s no straightforward way to bind to the Mailbox; we have no idea what constant value would be required to do that. But as everyone knows, all great video games have their cheat codes, and Microsoft Outlook Folder Creator is no exception. Can’t bind directly to the Mailbox? That’s OK; we’ll just bind to the Inbox and then retrieve the value of the Inbox’s Parent folder:

strFolderName = objFolder.Parent

Once that value has been stashed in the variable strFolderName we can then use it to create an object reference to the Mailbox folder:

Set objMailbox = objNamespace.Folders(strFolderName)

That’s nice, but how do we create a new subfolder of the Mailbox folder? Pretty much the same way we created a new subfolder of the Inbox folder:

Set objNewFolder = objMailbox.Folders.Add("Scripts Received")

That’s all there is to it.

Now, we know what you’re thinking. You’re thinking, “Yeah, that’s all well and good, but Halo 3 has a reset button. If things ever go wrong I can just press the reset button and start all over again. I bet Microsoft Outlook Folder Creator can’t do that, can it?”

Oh, can’t it? Take a look at this little block of code:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

strFolderName = objFolder.Parent
Set objMailbox = objNamespace.Folders(strFolderName)

Set objNewFolder = objMailbox.Folders("Scripts
Received")
objNewFolder.Delete

You’re right: this does look very similar to the script that creates a subfolder in the Mailbox folder, doesn’t it? However, there are a couple of important differences. For example, after we bind to the Mailbox folder, we use this line of code to create an object reference to the Scripts Received subfolder:

Set objNewFolder = objMailbox.Folders("Scripts Received")

From there we then call the Delete method to remove the folder:

objNewFolder.Delete

At that point, just like Halo 3, we can start all over again.

We hope that answers your question, TY. In the meantime, we’ve got work to do: we’re still negotiating with the Mountain Dew people to create a limited edition Microsoft Outlook Folder Creator-brand Mountain Dew. How are those negotiations going? To be honest, not quite as well as we had hoped. But we’re working on it.


How Can I Create a New Outlook Distribution List Based On the Membership of an Active Directory Group?

Hey, Scripting Guy! How can I create a new Outlook distribution list based on the membership of an Active Directory group?

-- HKM

Hey, HKM. Before we launch into today’s column we’d like to make a nomination for the Most Courageous Scripting Performance of the Year. With all due modesty, that would be today’s Hey, Scripting Guy! column, a column written despite the fact that the Scripting Guy who writes this column did something really weird to his back and, as a result, can now barely stand up and sit down, let alone walk. (Well, unless you consider a slow shuffle in which his feet never leave the ground to be a walk. If you do, then he can walk just fine. If you don’t, well, then he’s having some trouble walking these days.)

So was the Scripting Guy who writes this column doing something juvenile and immature when he hurt his back? Possibly. Did he persist in this behavior even though he was well-aware that someone (namely himself) was likely to get hurt? Maybe. When he could still barely walk, but was at least starting to feel better did he challenge the Scripting Son to a race, a race he not only was doomed to lose, but also a race that ended up making his back hurt even worse than it did before? That doesn’t really sound like the Scripting Guy who writes this column, but we suppose that could have happened. Despite the fact that he could barely walk, did he really drive 160 miles roundtrip just to watch a Scripting Son baseball game and then, upon returning home, engage in the very same behavior that got him hurt in the first place, all in an effort to prove … well, whatever it is he was trying to prove? You know what; it’s time to take a look at a script that can retrieve a list of users from an Active Directory group and then add each of those users to a new Microsoft Outlook distribution list:

On Error Resume Next

Const olDistributionListItem = 7

Set objOutlook = CreateObject("Outlook.Application")
Set objList = objOutlook.CreateItem(olDistributionListItem )

objList.DLName = "Test Group"
objList.Save

Set objGroup = GetObject("LDAP://CN=Finance Users,OU=Finance,,DC=fabrikam,DC=com")

For Each strUser in objGroup.Member
    Set objUser = GetObject("LDAP://" & strUser)
    strUserName = objUser.displayName

    Set objRecipient = objOutlook.Session.CreateRecipient(strUserName)
    objRecipient.Resolve
    objList.AddMember objRecipient
Next

So can we explain how this script works? You bet we can … as long as we don’t have to get up out of this chair, that is. (You don’t want to know how long it took us to get into this chair. Nor do you want to know how long it took the Scripting Guy who writes this column to climb seven flights of stairs this morning, all because – for some bizarre reason that no one, including himself, understands – he refused to take the elevator.)

As you can see, we start out by defining a constant named olDistributionListItem and setting the value to 7; we’ll use this constant to tell Outlook what type of item (a distribution list) we want to create. We next create an instance of the Outlook.Application object, then call the CreateItem method to create a new distribution list:

Set objOutlook = CreateObject("Outlook.Application")
Set objList = objOutlook.CreateItem(olDistributionListItem )

Actually, all we’ve done so far is create a distribution list in memory. To create a real distribution list, we need to give the list a name (that is, assign a value to the DLName property) and then call the Save method to save our new list. That’s what these two lines of code are for:

objList.DLName = "Test Group"
objList.Save

Yes, those are short, little lines of code, aren’t they? And that’s good: you have no idea how painful it is to type right now.

Although typing is nowhere near as painful as sneezing turned out to be.

After we create the distribution list all we have to do is connect to an Active Directory group, retrieve the membership list for that group, then add each of those users to our new list. Needless to say, that means that step 1 involves binding to the Active Directory group, in this case a group named Finance Users, which just happens to be located in the Finance OU of fabrikam.com:

Set objGroup = GetObject("LDAP://CN=Finance Users,OU=Finance,DC=fabrikam,DC=com")

Now it gets a little tricky. Active Directory groups have a property named Member; this property is actually a collection consisting of the entire membership of the group. To get at an individual group member we need to set up a For Each loop that will walk us through the entire collection, something we do with this line of code:

For Each strUser in objGroup.Member

Our next task is to bind to each individual user account and retrieve the value of the displayName property; the display name corresponds to the name shown in Outlook when you enter an email address and then click the Resolve button. The Member property lists group members by their distinguished name; hence a single user in that group will look something like this:

CN=kenmyer, OU=Accounting, DC=fabrikam, dc=com

By tacking the moniker for the LDAP provider (LDAP:) onto the front of the distinguished name we can create an object reference that binds us to that user account. That’s what we do here:

Set objUser = GetObject("LDAP://" & strUser)

Once we make the connection, we then grab the value of the displayName property and store it in a variable named strUserName:

strUserName = objUser.displayName

Got all that? Good. Now we’re ready to add the first user in the collection to our distribution list. To programmatically add someone to an Outlook distribution list, we need to create a Recipient object for that person. We do that by calling the CreateRecipient method, passing along the user’s display name as the sole method parameter:

Set objRecipient = objOutlook.Session.CreateRecipient(strUserName)

After creating the Recipient object we call the Resolve method; this resolves the user name against the Outlook address book. By the way, this is going to cause a security dialog box to pop up, inform you that someone or something is trying to access your address book, and then ask you if you want to allow this access. You must click Yes in this dialog box or the script will fail. That’s a security feature built into Outlook and, to be honest, we don’t know of any way to avoid it.

Of course, even if we did, we wouldn’t tell you. We get in enough trouble without trying to find ways to bypass Outlook security features.

Once the name has been resolved we can then call the AddMember method to add this new recipient to the distribution list. At that point we then loop around and repeat the process with the next user in the Member collection. And that, as they say, is that.

So are we going to explain exactly how the Scripting Guy who writes this column hurt his back? To tell you the truth, no, we aren’t. We already told the story to the Scripting Editor, and, as a result, have had the … pleasure … of listening to her opinion of both the Scripting Guy who writes this column and his tendency to try things that no one in their right mind would try. (In case you’re wondering, her opinion seems to consist primarily of the word “stupid.”) But don’t worry; the Scripting Guy who writes this column expects to be fully-recovered in another day or two. You know what they say: you can’t keep a good man down.

And, with any luck, you can’t keep the Scripting Guy who writes this column down, either.


How Can I Delete All the Email Sent From a Specific Person?

Hey, Scripting Guy! How can I delete all the email sent from a specific person?

-- VP

Hey, VP. Ah, yes, deleting email from a specific person; that’s something we Scripting Guys are all too familiar with. For example, the first time the Scripting Guys ever heard of such a thing was back in the early days of Microsoft Mail, when Bill Gates supposedly had a “bozo filter” installed on his machine that could automatically delete mail sent by a specified person or persons. (Of course, nowadays Outlook has also sorts of filters and rules built right into the product. But back then – or so the story goes – only Bill’s version of Microsoft Mail had this capability.)

We can’t say for sure that Bill had this bozo filter constructed specifically to delete mail sent from the Scripting Guys; when you consider the fact that the Scripting Guys didn’t even exist back then, well, that’s a reasonably good argument against that notion. On the other hand, there is the name they gave this contraption. Can you think of anyone who better fits the definition of “bozo” than the Scripting Guys? With that in mind, the Scripting Guys got together and developed a script that zips through our Inboxes and deletes any mail Bill sends to us. Does this script work? Let’s put it this way: you won’t find mail from Bill Gates in any of our Inboxes. No way.

In other words, if Bill Gates has been hounding you day and night with questions, business proposals, and what-have-you, well, here’s a script that will delete all that mail from your Inbox:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colItems = objFolder.Items
Set colFilteredItems = colItems.Restrict("[From] = 'Bill Gates'")

For i = colFilteredItems.Count to 1 Step -1
    colFilteredItems(i).Delete
Next

You know, that’s a very good question: how exactly does this script work? Well, we start out by defining a constant named olFolderInbox and setting the value to 6; we’ll use this constant to tell the script which Outlook folder we want to work with. Could we work with a different folder, or – even better – could we work with all the folders and subfolders in the Inbox? Sure; for an example of working with all the folders and subfolders in your Outlook Inbox take a look at this Hey, Scripting Guy! column.

Note. Don’t worry: none of our Web pages include a bozo filter of any kind. They can’t; otherwise how would we access those pages?

After defining the constant we create an instance of the Outlook.Application object, and then use the GetNamespace method to bind to the MAPI namespace (which we need to do even though the MAPI namespace is the only namespace we can bind to). Once we’re bound to the MAPI namespace we can then use this line of code to connect to the Inbox:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

After we’ve made the connection to the Inbox we could methodically walk through each and every mail message, checking to see if the message came from Bill Gates and, if it did, delete it. That works, but it’s a bit cumbersome. And if you’ve seen some of the Inboxes around here, be forewarned that a script that took that approach could also take forever to complete.

True story. The Scripting Guy who writes this column once had a manager who asked him to send her something by email. He did, but the manager said she couldn’t find the message anywhere. She asked the Scripting Guy if he could glance through her Inbox and pick out the message for her. When he went to look for that elusive message, he discovered that her Inbox contained over 14,000 items, 13,800 of which were unread. (Including pretty much every email the Scripting Guy had ever sent her.)

With that in mind we decided to do something a little fancier, and a little more efficient. To begin with, we use this line of code to grab a collection of all the items found in the Inbox:

Set colItems = objFolder.Items

Like we said, though, we aren’t going to start walking through this entire collection. Instead, we’re going to apply a filter of our own, one that restricts the collection to those messages sent by Bill Gates:

Set colFilteredItems = colItems.Restrict("[From] = 'Bill Gates'")

As you can see, we simply call the Restrict method, indicating that we want to restrict the collection to items where the From property is equal to Bill Gates. Just like that, our collection will be filtered, and the only things left in colItems will be messages that were sent from Bill Gates.

Note. Can you filter on properties other than From? You bet you can; for more information, take a look at this Office Space column devoted to that very subject.

Why do we use a filter? Well, for one thing, we now have a collection that contains only messages sent by Bill Gates; that means we don’t have to walk through all the items in the Inbox and laboriously check each message to see who sent it. Instead, we have a collection where we already know that each message was sent from Bill Gates. In turn, that means we can set up a For Next loop to walk through our filtered collection; each time through the loop we can then call the Delete method to delete the message:

For i = colFilteredItems.Count to 1 Step -1
    colFilteredItems(i).Delete
Next

Very observant of you: that is a somewhat unusual-looking For Next loop, isn’t it? With this loop we start with the very last item in the collection (which we can determine by checking the value of the Count property). We delete that item and then delete the next-to-last item in the collection, continuing in this vein until we finally delete the first item in the collection. (The Step -1 parameter tells the script to count backwards as it loops through the collection.)

Why don’t we just start at the beginning and then stop at the end? As it turns out, if you start deleting items from a collection Outlook can lose its place; that’s because, as items get deleted, the order of those items – and thus the nature of the collection – begins to change. For example, say you delete item 1. Obviously the next thing you’ll want to do is delete item 2. However, what used to be item 2 is now item 1 (because item 1 is gone). Consequently, you’ll end up deleting item 3 instead. See how that works? If you follow this all the way through, you’ll end up deleting only about half the items.

That’s right: we wanted to delete all the items, not just every other item. Fortunately we can do that as log as we start at the bottom and work our way up. With that approach, items don’t change position as other items are deleted; if you delete item 5, item 1 will still be item 1. As long as we delete items in inverse order – 5-4-3-2-1 – everything will be fine.

It’s weird, but it works. Just like the Scripting Guys.

Well, OK: except maybe for the part about working. But, hey, we must be doing something right; why else would they have named an email filter after us?


How Can I Delete All the Messages in My Sent Items Folder?

Hey, Scripting Guy! How can I delete all the messages in my Sent Items folder?

-- OG

Hey, OG. You know, throughout his entire life the Scripting Guy who writes this column has been told that “the only dumb question is the question that goes unasked.” Now, to be perfectly honest, he doesn’t really believe this. For example, when Doug Williams became the first black quarterback to lead his team to the Super Bowl one of the questions that was asked of him was this classic inquiry: “How long have you been a black quarterback?”

Which would seem to support the statement that it isn’t just unasked questions that can be dumb.

Nevertheless, there’s no doubt that a question that doesn’t get asked isn’t going to get answered … unless, of course, you’re a devoted reader of the Hey, Scripting Guy! column. In Hey, Scripting Guy! we not only answer the questions asked of us (sooner or later, anyway) but we also answer the questions that weren’t asked of us. Like what? Like this: yes, it is time for you to start preparing for the 2007 Winter Scripting Games.

That’s right, the 2007 Winter Scripting Games are headed for the Script Center (February 12-23, 2007), and this year the Games are going to be bigger and better than ever: not only do we have way more events than last year, but we also have more divisions (VBScript Beginners; VBScript Advanced; Windows PowerShell Beginners; and Windows PowerShell Advanced). And did we mention that we have prizes this year, some of which have yet to be announced and some of which we have announced? And – brace yourselves – those announced prizes include 250 Dr. Scripto Bobblehead dolls.

Note. Good point: the term Dr.Scripto bobblehead does seem a bit redundant, doesn’t it?

Best of all, all you have to do to have a chance of winning a prize is to enter at least one event; you don’t even have to successfully complete the event. Try naming one other major sporting competition where they randomly choose winners like that!

Well, OK. We mean besides figure skating.

Of course, if you’re anything like the Scripting Guys you probably put off your training until the end, and, suddenly, now the Scripting Games are almost upon us. But, hey, relax. Not ready for the Games? Then download the official Scripting Games Program, featuring tips and tricks, a sample event, and a pair of crossword puzzles designed to get you ready for the Games themselves. You say you’re a scripting novice? Then take a look at our special training article on dealing with arrays. This year there’s no excuse for not trying at least one event in the Scripting Games.

Nope, sorry: no excuse.

OK, now that we’ve taken care of the question no one ever asked let’s see if we can tackle the question that someone did ask. OG, you say you need to delete all the messages in your Sent Items folder? This little script should do just that:

Const olFolderSentMail  = 5

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)

Set colItems = objFolder.Items

For i = colItems.Count to 1 Step - 1
    colItems(i).Delete
Next

How does this script work? Wow, yet another question we’re going to answer today! To begin with, we start out by creating a constant named olFolderSentMail and setting the value to 5; we’ll use this constant to tell the script which Outlook folder we want to connect to. After defining the constant we create an instance of the Outlook.Application object, then use the GetNamespace method to bind us to the MAPI namespace. As soon as that’s done we use the GetDefaultFolder method to connect to the Sent Items folder, like so:

Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)

And as soon as that’s done we use the following line of code to return a collection of all the messages found in the Sent Items folder:

Set colItems = objFolder.Items

Now it gets a little weird (but just a little). What we need to do next is loop through all the items in the collection and delete each one; to do that we’d usually set up a For Each loop. In Outlook, however, that won’t work; instead, we have to specify the index number of each item when we go to delete it. Furthermore – and for reasons we won’t bother going into – you’ll often run into problems if you try deleting the first message, then the second message, then the third message. Instead, we need to start by deleting the last message, then the next-to-last message, and so on. That’s why we have this odd-looking loop:

For i = colItems.Count to 1 Step - 1
    colItems(i).Delete
Next

As you can see, this loop runs backwards (Step -1) from the total number of messages in the folder (a value we can determine by using the Count property) to the first message in the folder (item 1). This has to do with the fact that each time you delete a message from the collection the individual items in the collection get re-numbered. Because of that, if we start at message 1 and work our way down, we’ll, at best, delete only half the items. If we start at the bottom and work our way up we’ll delete all the items.

We know, we know. But try it yourself and you’ll see what we’re talking about.

Anyway, that should do it, OG. We hope that helps, and we hope to see you – and everyone else – at the Scripting Games.

Note. You’re right: deleting all the items in an Outlook folder would have made for a good Scripting Games event. Dang! Probably too late for that now, isn’t it?


How Can I Delete Unread Emails That Are More Than 6 Months Old?

Hey, Scripting Guy! We have a lot of users who let unread email messages pile up in their Outlook Inboxes; after awhile, that causes their Inboxes to fill up. How can I write a script users could run that would delete any unread messages that are more than 6 months old?

-- ST

Hey, ST. You know, sometimes it’s tough being a Scripting Guy. After all, people don’t come to the Script Center just hoping to pick up a tip or two about system administration scripting; instead, people come to the Script Center hoping to be uplifted and inspired. Even well-known TV personalities like Oprah Winfrey and Dr. Phil don’t bother trying to help people anymore; instead, they simply say, “Listen, you want to be uplifted and inspired? Then read the Hey, Scripting Guy! column every morning.”

Disclaimer. OK, so, technically, neither Oprah Winfrey nor Dr. Phil has ever said anything even remotely similar to that. However, if you watch their shows, and if you carefully observe their body language, well, it’s pretty obvious what they’re implying.

Now, as a general rule, the Scripting Guys welcome the responsibility of spreading joy and happiness throughout the world. However, fulfilling their roles as the Happiness Fairies can be difficult at times, especially during a week like this one. What’s so bad about this week? Boy, where do we start? For one thing, the Scripting Guy who writes this column started the week off with a car that was broken and could never be fixed; saw that change to a car that could be fixed simply by replacing a $1.25 part; then ended the week with a car that was broken and could never be fixed.

Meanwhile, Scripting Guy Jean Ross made arrangements to have a few things taken care of, the only stipulation being that everything had to be taken care of by July 3rd. No problem, she was told; everything will be done by July 3rd. When she went in to sign the final contract they told her, “Here you go, ma’am. Everything will be done by July 13th, just like you asked.”

Note. The punch line? On top of everything else, the stuff they told her would be done by July 13th wasn’t even the stuff she had asked them to do in the first place.

Let’s see, what else …. Well, TechNet is planning to make some changes to the publishing process, changes that will make the Scripting Guys’ work life much … better …. (Apparently TechNet didn’t think it was challenging enough for a two-person team to write, publish, and maintain the entire Script Center. Therefore they decided to up the ante a little.) As for the icing on the cake, we’re also in the midst of performance reviews here at Microsoft. We’re probably not supposed to share this information, but here’s an excerpt from last year’s performance review for the Scripting Guy who writes this column:

· Achievements and Contributions: None

· Skills and Competencies: None

· Expectations for Future Success: None

Too bad for him that they don’t have a category for consistency. That he has.

But hey, you didn’t come to the Script Center to listen to the Scripting Guys bemoan their fates, did you? Instead, you came here because Oprah Winfrey and Dr. Phil promised that the Scripting Guys would uplift and inspire you. OK, let’s see what we can do about that … uplifting and inspiring … hmmm …

Oh, we know: Suppose we show you a script that can delete all the unread messages in your Outlook Inbox that are more than 6 months old? You know, a script like this one:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colItems = objFolder.Items
Set colFilteredItems = colItems.Restrict("[UnRead] = True")

For i = colFilteredItems.Count to 1 Step - 1
    If DateDiff("m", colFilteredItems(i).ReceivedTime, Now) > 6 Then
        colFilteredItems(i).Delete
    End If
Next

Feel better? Good; after all, your happiness is the only thing that matters to the Scripting Guys. Of course, you’d probably feel even better if you understood how this script works. Very well; let’s see if the two Happiness Fairies can help you with that.

To begin with, we Happiness Fairies start out by defining a constant named olFolderInbox, then assign olFolderInbox the value 6; we’ll use this constant to tell the script which Outlook folder to work with. After defining the constant we create an instance of the Outlook.Application object, then use the GetNamespace method to connect to the MAPI namespace. (A required step, even though the MAPI namespace is the only namespace we can connect to.) Finally, we use the GetDefaultFolder method to bind to the Outlook Inbox:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Now we’re ready to roll. With the connection complete we use the following line of code to retrieve a collection of all the items (that is, all the email messages) found in the Inbox folder:

Set colItems = objFolder.Items

That’s a good point: we’re really only interested in unread emails, aren’t we? That’s fine; all we have to do is apply a filter and create a “sub-collection” (named colFilteredItems) that contains only the unread emails found in the Inbox folder:

Set colFilteredItems = colItems.Restrict("[UnRead] = True")

We’re not going to discuss email filtering in any detail today; if you’d like to know more about filtering email take a look at our Office Space article on that very subject. About all we’ll say here is that we’re limiting our collection to items where the Unread property is True; as you might expect, if the Unread property is True that means that the message hasn’t been read yet.

Our next step is to loop through the entire collection of unread messages and then delete any emails more than 6 months old. In theory, we could have expanded our filter so that we limited the sub-collection to messages that were both unread and more than 6 months old. We didn’t bother with that simply because we were afraid the filter would become unduly complicated. Instead, we’re going to loop through all the unread messages, checking each one to see if it’s more than 6 months old. If it is, we’ll delete it. If it’s not, we won’t.

Just exactly the way Dr. Phil would do things.

Because we’re going to be deleting items we need to start our loop at the bottom (that is, with the very last email in the collection) and then work our way to the top (the very first email in the collection). That’s why we have a For Next loop that starts with the last item (whose index number can be determined by using the Count property) and works its way towards the first item, the one with the index number 1:

For i = colFilteredItems.Count to 1 Step -1

What’s that? You say you have two questions? Look, we don’t really feel like answering a bunch of – no, sorry. After all, we are the Happiness Fairies; we have a job to do. With that in mind, we’d be thrilled to answer your questions. As to the first question (“Why the Step -1 parameter?”), well, that’s easy: Step -1 is the key to doing a “backwards” loop. Let’s say we have 100 items in our collection. In that case, the counter variable i starts out equal to 100, and the first time through the loop we’ll be working with item 100. When we’re done with that item we need to turn our attention to item 99. How do we get from item 100 to item 99? That’s right: by subtracting 1 from the current value of our counter variable. That’s what Step -1 is for.

As for your other question (“Why does deleting items require us to start at the bottom and work our way up?”), well, take a peek at this Hey, Scripting Guy! column for an explanation.

As we noted, inside the loop we need to take a look at each email and determine whether the message is more than 6 months old. That’s what this line of code is for:

If DateDiff("m", colFilteredItems(i).ReceivedTime, Now) > 6 Then

So what are we actually doing with that line of code? Well, we’re using VBScript’s DateDiff function to determine the number of months (the “m” parameter) between the date and time the message was received (colFilteredItems(i).ReceivedTime) and the current date and time (Now). If the number of months is greater than 6 we then use this line of code to delete the message:

colFilteredItems(i).Delete

And if the number of months is not greater than 6 then we don’t do anything at all.

Which, as our performances reviews suggest, is what we do best.

From there we loop around and repeat the process with the next email in the collection. By the time we exit the loop we’ll have deleted all the unread emails that are more than 6 months old.

At any rate, we hope that you found today’s column uplifting and inspiring, ST. If not, here’s something that might help. Remember, no matter how bleak things might look, they could be worse: you could be a Scripting Guy.


How Can I Determine the Follow-Up Status of Outlook Emails?

Hey, Scripting Guy! How can I go through all the emails in an Outlook folder and determine which messages are flagged for follow-up and which messages were flagged for follow-up but are now complete?

-- KF

Hey, KF. We’ll be honest with you: we’re going to try to get to your question today, but first we’ve got something far more important to do. Yesterday when he got home the Scripting Guy who writes this column found a letter in his mailbox from the Cambridge Who’s Who. This letter informed him that he was “being considered for inclusion in the 2007/2008 Cambridge Who’s Who Among Executives and Professionals ‘Honors Edition’ of the Registry.”

Was the Scripting Guy who writes this column excited by this? That’s putting it mildly; as the letter notes “Inclusion is considered by many as the single highest mark of achievement.” You can count the Scripting Guy who writes this column as being one of those people who consider this their single highest mark of achievement. And no, that isn’t because he doesn’t actually have any other achievements. He doesn’t, but that’s not why he’s so excited.

Hold on a second here. You know, now that we look at this a little more closely we realize that there’s a string attached: to actually be included in the Registry itself you need to fill out an application form, indicating such things as the company you work for, your job title, and your specialty. Uh-oh; apparently you don’t get this honor unless you’ve actually done something to deserve it. Is that a problem? You bet it is. After all, we were hoping this was like the Nobel Prize for Literature which, as near as we can tell, is awarded simply by drawing names out of a hat.

So what exactly is the Scripting Guy who writes this column’s specialty? To tell you the truth, we were hoping you’d tell us. But let’s see, he can … um … he’s good at … he …. Oh, wait we know: he can write scripts that can go through all the emails in an Outlook folder and determine which messages are flagged for follow-up and which messages were flagged for follow-up but are now complete:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colItems = objFolder.Items

For Each objItem in colItems
    If objItem.FlagStatus = 1 Then
        Wscript.Echo "Follow-up complete"
        Wscript.Echo objItem.Subject
        Wscript.Echo
    End If

    If objItem.FlagStatus = 2 Then
        Wscript.Echo "Marked for follow-up"
        Wscript.Echo objItem.Subject
        Wscript.Echo
    End If
Next

What’s that? You’d like to know how this script actually works? Well, explaining things isn’t exactly our specialty, but we’ll see what we can do.

We start out by defining a constant named olFolderInbox and setting the value to 6; that’s going to tell our script which folder we want to work with. And that folder is – oh, that’s right: the Inbox folder. Apparently the constant name was a bit of a giveaway, wasn’t it?

Oh, well; so much for the suspense, huh? (Fortunately writing stuff that has no suspense and no plot will actually help our chances of getting the Nobel Prize for Literature.) After defining the constant olFolderInbox we then use these three lines of code to create an instance of the Outlook.Application object, connect to the MAPI namespace, and bind to the Inbox folder:

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Once we’ve done that we then use this line of code to return a collection of all the mail messages found in that folder:

Set colItems = objFolder.Items

At this point there are at least two different paths we can take: we can write a filter to weed out messages that don’t have any follow-up flag setting, or we can simply loop through the entire collection and check the flag setting for each and every item. Because people tend to find Outlook filters confusing we’ve opted to loop through the entire collection. But if you’re willing to tackle filter writing you’ll find some information about that very thing in this Office Space article.

And no, we can’t guarantee that writing an Outlook filter script will get you included in the Registry alongside the Scripting Guy who writes this column. But it can’t hurt.

Seeing as how we’ve decided to loop through the entire collection our next step is to set up a For Each loop that, well, loops through the entire collection. The first thing we see inside that loop is a block of code that looks like this:

If objItem.FlagStatus = 1 Then
    Wscript.Echo "Follow-up complete"
    Wscript.Echo objItem.Subject
    Wscript.Echo
End If

What we’re doing here is examining the value of the FlagStatus property. If FlagStatus is equal to 1 that means that this particular email was originally marked for follow-up but the follow-up is now complete. (In Outlook itself you’ll see a little checkmark in the status column.) Therefore, we echo back the fact that follow-up is complete; in addition, we echo back the email Subject line (just so we can tell which message has been marked as follow-up complete).

Good question: what about emails that are still marked for follow-up? Well, that’s what this block of code is for:

If objItem.FlagStatus = 2 Then
    Wscript.Echo "Marked for follow-up"
    Wscript.Echo objItem.Subject
    Wscript.Echo
End If

Here we’re checking to see if the value of FlagStatus is equal to 2; it probably goes without saying that this represents an email still marked for follow-up. In a case like that we then echo back the appropriate message (Marked for follow-up) as well as the subject line.

And then, of course, we loop around and repeat the process with the next message in the Inbox. What if we encounter an email that has no follow-up status of any kind (neither complete nor pending)? That’s easy. That means that the FlagStatus for the message is equal to 0, which in turn means that we simply skip that particular email.

Incidentally, there are a couple of other follow-up related properties you might be interested in. For example, if you’d like to know the actual color of the flag assigned to a message use the FlagIcon property and one of these values:

Constant | Value
olBlueFlagIcon | 5
olGreenFlagIcon | 3
olNoFlagIcon | 0
olOrangeFlagIcon | 2
olPurpleFlagIcon | 1
olRedFlagIcon | 6
olYellowFlagIcon | 4

And if you’d like to know when you’re supposed to follow-up a message you can look at the value of the FlagDueBy property. (If no due date has been assigned then this property defaults to 1/1/4501, which probably still isn’t enough time for the Scripting Guy who writes this column to follow-up on all the items he has marked for follow-up.) As an added bonus, all these properties are read/write, which means you can programmatically change the follow-up status, icon, or due date. Pretty cool, huh?

OK; so much for that. We’re willing to bet that some of you are skeptical about this whole Who’s Who thing. “Is the Scripting Guy who writes that column really among the ‘country’s most accomplish professionals,’” you’re wondering, “or is his name simply on some mailing list somewhere?” To tell you the truth, we’re not worried about that. After all, when you’ve earned as many honors and awards as the Scripting Guy who writes this column (0, unless you count making the Little League and PONY League All-Star teams), well ….

Hey, wait a second: why don’t you count making the Little League and PONY League All-Star teams? That’s way more than Bill Gates ever did.

Well, baseball-wise, anyway.


How Can I Filter Outlook Messages By Email Address?

Hey, Scripting Guy! I read your article on filtering email messages in Microsoft Outlook, and it’s almost what I need, but not quite. What I really need to know is this: how can I filter Outlook messages by the email address of the person who sent the message?

-- SJS

Hey, SJS. Say, you weren’t by any chance at the Columbia Athletic Club this past weekend, were you? We didn’t think so, but we thought we should ask. On Saturday the Scripting Guy who writes this column and the Scripting Son went to the gym to play basketball. As they stepped inside some stranger began glaring at the Scripting Guy who writes this column, as if to say, “Listen, buddy, there’s no way that both of us are leaving this gym alive.” At first the Scripting Guy who writes this column didn’t pay much attention; having never seen this guy before, he assumed the man must be glaring at someone else. But then the Scripting Son spoke up:

“So Dad: are you gonna fight that guy over there?”

“Well, it kind of looks that way, doesn’t it?”

In fact, the man in question glared at the Scripting Guy who writes this column as he and the Scripting Son checked in, and continued glaring as the two walked through the exercise room and headed for the basketball court.

Yes, very strange. Not that the Scripting Guy who writes this column isn’t used to people glaring at him with looks of utter hatred and disgust; he gets that at work pretty much every day. But it was a bit unusual for someone to glare at him at the gym. At the gym people are much more likely to giggle when he walks by.

And then burst out laughing when he tries doing the bench press.

At any rate, the Scripting Guy who writes this column spent the weekend wondering why a total stranger would glare at him like that. At first he couldn’t come up with any reason. And then he thought, “Hmmm, maybe that guy wanted to know how to do something like, say, filter Outlook email messages by email address. Maybe he wrote to Hey, Scripting Guy! and we haven’t answered him yet. Maybe we should answer his question and see if that makes him happy.”

If you were the angry guy at the gym, SJS, we hope this helps, and we apologize for taking so long to get around to your question. And if you weren’t the angry guy at the gym, well, do us a favor, OK: if you happen to run into the crazy guy, please don’t tell him that we answered your question instead of his?

Here’s how you (or the angry guy, for that matter) can filter Outlook email messages by email address:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colItems = objFolder.Items
Set colFilteredItems = colItems.Restrict("[SenderEmailAddress] = 'kenmyer@fabrikam.com'")

For Each objMessage In colFilteredItems
    Wscript.Echo objMessage.Subject
Next

Whoa, don’t look at us like that; we were just about to explain how the script works. (Really.) We start out by defining a constant named olFolderInbox and setting the value to 6; that tells the script which Outlook folder (in this case the Inbox folder) we want to work with. Next we create an instance of the Outlook.Application object and bind to the MAPI namespace. We then use this line of code and the GetDefaultFolder method to make a connection to the Inbox folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Now for the good stuff. The first thing we do is use this line of code to return a collection of all the items (that is, all the email messages) found in the Inbox folder:

Set colItems = objFolder.Items

And you’re right: we don’t want all the items in the Inbox, do we? Instead, we want only those emails that were sent by kenmyer@fabrikam.com. That’s what this line of code is for:

Set colFilteredItems = colItems.Restrict("[SenderEmailAddress] = 'kenmyer@fabrikam.com'")

All we’re doing here just grabbing a subset of the items found in the Inbox. To do that we call the Restrict method, passing the method two parameters:

· [SenderEmailAddress], the property we want to filter on. Note that the property name must be enclosed in square brackets.

· kenmyer@fabrikam.com, the criteria to be applied to the filter. In this case, we want the get back only emails where the SenderEmailAddress is equal to kenmyer@fabrikam.com.

Ah, that’s a good question: can we include multiple criteria in a filter? You bet we can. For example, here’s a filter that returns emails sent by kenmyer@fabrikam.com or by packerman@contoso.com:

Set colFilteredItems = colItems.Restrict _
    ("[SenderEmailAddress] = 'kenmyer@fabrikam.com' OR [SenderEmailAddress] = 'packerman@contoso.com'")

One thing to keep in mind here is the fact that, because we are dealing with email addresses, Outlook security might kick in when we run this script. That means that a dialog box might pop up telling us that someone (or something) is trying to access email addresses; in turn, we’ll have to use that dialog box to explicitly grant the script access to the data. For more information, see our original article on filtering email messages.

After applying the filter we then set up a For Each loop to walk through all the items in the filtered collection. In this sample script all we do is echo back the value of the Subject property; however, you can echo back the value of any (or all) of the properties of the MailItem object.

And there you have it, SJS: that should be enough to put a smile on anyone’s face.

We hope.

Speaking of which, the trip to the gym turned out to be somewhat anticlimactic: by the time the Scripting Guy and Scripting Son finished playing basketball the angry man had left the building. Was the Scripting Guy who writes this column shaken by this experience? What, are you kidding? The Scripting Guy who writes this column doesn’t know the meaning of the word fear.

It’s just a coincidence that he sent the Scripting Son out to start the car.


How Can I Get a List of All the

How Can I Get a List of All the .PST Files on a Computer?

Hey, Scripting Guy! How can I get a list of all the .pst files on a computer – along with their size and their location – and then save that information to a comma-separated values file?

-- DC

Hey, DC. If you hang around baseball fields long enough sooner or later you’ll hear someone use he expression “tailor-made.” Typically what they are referring to is not some player’s uniform but a ground ball that is doomed to be turned into a double play. Something that’s tailor-made can’t be anything but a double-play; for better or worse (depending on the team you’re rooting for) the grounder was perfectly designed to be turned into two quick outs.

Why do we mention this? (We mean besides the fact that at least one of the Scripting Guys spends far more time thinking about baseball than he does thinking about scripting.) Well, it turns out that this question is tailor-made for WMI: not only can WMI search any entire file system for any files that have a .pst file extension, but WMI can also retrieve folder information and file size as well. Tailor-made!

Just to prove it, here’s a script that retrieves information about all the .pst files on a computer. Note that this particular script displays information to the screen; we’ll show you how to save this data to a comma-separated values (CSV) file a little later on:

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Extension = 'pst'")

For Each objFile in colFiles
    Wscript.Echo objFile.Drive & objFile.Path
    Wscript.Echo objFile.FileName & "." & objFile.Extension
    Wscript.Echo objFile.FileSize
    Wscript.Echo
Next

Just a few things to point out here. After connecting to the WMI service we use ExecQuery to return a collection of all the files that have an Extension of pst. Note that we don’t include the period in the file extension: it’s pst rather than the .pst. That’s just the way WMI does things.

After retrieving this collection we create a For Each loop and echo the desired property values. This is a little bit tricky because of the way WMI stores naming information for a file. For example, suppose we have this file:

c:\documents and settings\kmyer\local settings\application data\microsoft\outlook\archive.pst

The naming information is stored like this in WMI:

WMI Property | Value
Drive | C:
Path | \documents and settings\kenmyer\local settings\application data\microsoft\outlook\
FileName | archive
Extension | pst

What does this mean? Well, it means that to display folder and files names the way we’re used to seeing them we have to combine these property values before echoing them to the screen. To echo the folder name we combine the Drive and Path properties, like so:

Wscript.Echo objFile.Drive & objFile.Path

To display the file name we combine the FileName and Extension properties:

Wscript.Echo objFile.FileName & "." & objFile.Extension

Note. Yes, we could have done this instead:

Wscript.Echo objFile.FileName & ".pst"

 We took a more generic approach simply to show you how to construct file names in WMI.

To display the file size we – oh, well, we just echo the value of the FileSize property. Hurray for FileSize, huh?

Now, what about saving this data to a CSV file? We’ll show you a script that does that; if you need more information about saving data to a CSV file take a look at this portion of the Microsoft Windows 2000 Scripting Guide.

Here’s a script that retrieves information about .pst files and then saves that data to C:\Scripts\PST_Data.txt:

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile Where Extension = 'pst'")

If colFiles.Count = 0 Then
    Wscript.Quit
End If

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.CreateTextFile("C:\Scripts\PST_Data.txt")

For Each objFile in colFiles
    objTextFile.Write(objFile.Drive & objFile.Path & ",")
    objTextFile.Write(objFile.FileName & "." & objFile.Extension & ",")
    objTextFile.Write(objFile.FileSize & vbCrLf)
Next

objTextFile.Close

Notice how we write to the text file. We begin by writing the folder information followed by a comma. That gives us this:

c:\documents and settings\kmyer\local settings\application data\microsoft\outlook\,

Next we add the file name, followed by another comma. Our text file will thus look like this:

c:\documents and settings\kmyer\local settings\application data\microsoft\outlook\,archive.pst,

Finally we tack on the file size followed by a carriage-return linefeed (equivalent to pressing ENTER on the keyboard). We use the carriage return linefeed because we’re done with the first .pst file and we want information for the next .pst file to be shown on a separate line. Our text file now looks like this:

c:\documents and settings\kmyer\local settings\application data\microsoft\outlook\,archive.pst,68741

What’d we tell you: tailor-made.

Note that – depending on the size of your file system – this script might take 30 seconds or so to complete. If you know that your .pst files are likely to be found only on a particular drive or in a particular folder you might be able to speed that up a bit by modifying your WQL query. For example, this query searches only drive C: for .pst files:

Set
colFiles = objWMIService.ExecQuery _
("Select * from CIM_DataFile Where Extension = 'pst'
AND Drive = 'C:'")


How Can I Get a List of All the Senders’ Email Addresses in an Outlook Folder?

Hey, Scripting Guy! How can I get a list of all the senders’ email addresses in an Outlook folder?

-- CW

Hey, CW. You know, you must get email from a whole different set of people than we Scripting Guys do. For example, in looking through the Scripting Guys Inboxes it appears as though all our mail comes from the following sources:

· Service representatives pointing out that our bank/eBay/PayPal account is about to expire, and noting that we need to log on to a specified Web site in order to prevent any interruption in service.

· Evangelist Frank Radebe, the first son of the late Dr. John Radebe; Mr. Greg Stevenson, staff of Kleinwort Benson; Rev. Jones De; and Ms. Angela Dikko Emile, the daughter of Professor Emile Boga Doudou, all of whom need our help transferring money from an African bank to a U.S. bank. (But not as a favor to them, mind you; they’re more than willing to pay us for our trouble.)

· Pharmaceutical salespeople who might be sampling their wares more than doctors recommend: “Tts h'erbal solution what hasnt no side effect, but has 100% guaranted resultss.”

· Beautiful, sexy girls who got our names from a mutual friend and are just dying to meet the male Scripting Guys.

Note. OK, those last ones could be legitimate.

Well, they could be.

In other words, we Scripting Guys don’t have much need to get a list of all the people who send us email. (OK, that h’erbal solution might come in handy, but other than that …) Like we said, though, you might hang around with a different crowd than we do. If that’s the case, then a script like this one might be just the thing you need:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set colItems = objFolder.Items

For Each objItem in colItems
    Wscript.Echo objItem.SenderEmailAddress
Next

Yes, it is a nice little script, isn’t it? (We’re guessing that’s why Angela Dikko Emile came to us for help; she probably figured that if we could write a script like that then transferring money from an African bank to a U.S. bank would be a snap.) As you can see, we start out simply enough, defining a constant named olFolderInbox and setting the value to 6; in a minute or two we’ll use that constant to tell the script which Outlook folder we want to work with.

After defining the constant we create an instance of the Outlook.Application object, then use the GetNamespace method to bind to the MAPI namespace. (That’s actually the only namespace we can bind to, but we still have to bind to it. Kind of like having to fill out an income tax return even though they’ve already taken all your money.) After that we use the GetDefaultFolder method to bind us to the Inbox folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

See: we told you we’d use the constant olFolderInbox to help us connect to the Inbox, didn’t we?

After connecting to the Inbox folder we can then use this line of code to retrieve a collection of all the items (i.e., all the email messages) found in that folder:

Set colItems = objFolder.Items

At this point all we have to do is set up a For Each loop and loop through the collection of email messages; for each message in that collection we simply echo back the value of the SenderEmailAddress property. Keep in mind, however, that when you try to access the SenderEmailAddress the following Outlook security dialog box is going to pop up onscreen:

You’ll need to select the Allow access for check box, pick a time period, and then click Yes before the script will be allowed to retrieve anyone’s email address. (Yes, it’s a bit of a hassle. However, it does help prevent you from doing things like inadvertently notifying all the people who have sent you email that their PayPal account is about to expire.)

And that’s it: in a matter of seconds the sender email addresses for all the items in your Inbox will be displayed onscreen.

Like we said, this script retrieves information from your Inbox folders. What if you’d like to pull the list of email addresses from a subfolder of the Inbox? To tell you the truth, we weren’t sure if we could do that at first. However, after just one application of the miracle h’erbal solution (“100% guaranted resultss!”) we came up with this:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)

Set objSubfolder = objFolder.Folders("Test")

Set colItems = objSubFolder.Items

For Each objItem in colItems
    Wscript.Echo objItem.SenderEmailAddress
Next

In this script we bind to – and retrieve email addresses from – a subfolder of the Inbox, a subfolder named Test. How do we do that? Well, after binding to the Inbox we use this line of code to connect us to the Test subfolder:

Set objSubfolder = objFolder.Folders("Test")

All we have to do then is make sure we retrieve the collection of emails from this subfolder:

Set colItems = objSubFolder.Items

Good question: that works if the folder is a subfolder on the Inbox. But what if the folder we’re interested in is on the same level as the Inbox? How the heck do we access a folder like that? Well, we’ll tell you, but you’ll have to wait until tomorrow’s column.

Unless, of course, it turns out that we really did win $5 million in the Euro lottery. Tune in tomorrow to find out.


How Can I Get a List of Appointments for a Specific Month?

Hey, Scripting Guy! How can I get a list of my appointments for a specific month (for example, all my appointments for December, 2007)?

-- HG

Hey, HG. We’ll get to your question in just a moment. Before we do that, however, the Scripting Guy who writes this column wants to take a little time and enjoy the last few days of August (and, in effect, the last few days of summer). And what a summer it’s been here in the Seattle area; this past weekend, for example, the temperature nearly reached 70 degrees Fahrenheit! (And no, we are not making that up.) At one point the Scripting Guy who writes this column actually had the back door to his house propped open, although that changed the moment the wind started blowing and the rain started falling. But that was all right: needless to say, after dealing with daytime temperatures of 68 degrees Fahrenheit, well, a little breeze and a little rain felt pretty good.

The important thing is that, after a relatively tough winter (which really wasn’t all that tough, as long you didn’t mind being without power for a 6-day stretch) the weather gods obviously felt sorry for those of us who live in Seattle. “Those poor Seattle-area people,” they must have thought. “We gave them such a lousy winter; how can we make it up to them? Oh, wait, we know: we’ll give them a summer where the temperature rarely climbs above 70 degrees and the sun hardly ever shines. That should make everyone happy.”

And, of course, it did.

Note. The whole Seattle area seems to be under a curse of some kind: we’re spending billions for dollars to build a light rail system that doesn’t actually go anywhere; we’re stuck serving as the home of the Seattle Supersonics (although that, at least, appears to be temporary); and now we’re forced to endure both a lousy winter and a lousy summer. What did we ever do to deserve this?

Hey, wait a minute: who said, “You gave the world Microsoft, didn’t you?”

Oh, well. As it turns out, that tiny glimmer of light that might have been a tiny of sliver of sunshine has now disappeared behind our omnipresent gray clouds. With that in mind, let’s turn the lights on, crank up the heat a little, and conclude the summer with a bang: by writing a script that retrieves a list of appointments scheduled for the month of December, 2007.

You know, a script like this one:

Const olFolderCalendar = 9

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderCalendar)

Set colItems = objFolder.Items

strFilter = "[Start] >= '12/1/2007' AND [Start] <= '12/31/2007'"
Set colFilteredItems = colItems.Restrict(strFilter)

For Each objItem In colFilteredItems
    If objItem.Start > Now Then
        Wscript.Echo "Meeting name: " & objItem.Subject
        Wscript.Echo "Meeting date: " & objItem.Start
        Wscript.Echo "Duration: " & objItem.Duration & " minutes"
        Wscript.Echo "Location: " & objItem.Location
        Wscript.Echo
    End If
Next

As you can see, we start out by defining a constant named olFolderCalendar and setting the value to 9; we’ll use that constant to tell the script which Outlook folder we want to connect to. (As a matter of fact, we do want to connect to the Calendar folder. Good guess!) After defining the constant, we create an instance of the Outlook.Application object, then use the GetNamespace method to bind to the MAPI namespace (which – in case you’re wondering – happens to be the only namespace that we can bind to).

Note. So if the MAPI namespace is the only namespace we can bind to, then do we even have to bind to it? Why don’t we just automatically connect to the MAPI namespace? Well, that’s because … because … well, just because, OK?

After we’ve made the connection to the MAPI namespace, we then call the GetDefaultFolder method to hook us up to the Outlook calendar (note that we pass GetDefaultFolder the constant olFolderCalendar that we created at the start of the script):

Set objFolder = objNamespace.GetDefaultFolder(olFolderCalendar)

From there, we can get a collection of all our appointments by doing nothing more complicated than creating an object reference to the folder’s Items property, like so:

Set colItems = objFolder.Items

That was pretty easy, wasn’t it? But party’s over: now we’re going to have to do a little bit of work. (But don’t worry; it truly is just a “little bit” of work.) As you no doubt recall, we aren’t interested in a collection of all our appointments; we’re only interested in those appointments scheduled to take place during the month of December, 2007. Therefore, and in order to restrict our collection to appointments scheduled for the month of December, 2007, we need to apply a Filter to the collection, something we do with these two lines of code:

strFilter = "[Start] >= '12/1/2007' AND [Start] <= '12/31/2007'"
Set colFilteredItems = colItems.Restrict(strFilter)

So what’s going on here? Well, in the first line we’re setting the criteria for the Filter: we only want those appointments where the value of the Start property (note that property names must be enclosed in square brackets) is greater than or equal to December 1, 2007 (12/1/2007) and where the value of the Start property is less than or equal to December 31, 2007. The net effect? We’ll only get back appointments that take place (i.e., that start) in December, 2007.

In the second line of code, we simply call the Restrict method, passing along the filter we created (represented by the variable strFilter). That’s going to give us back a brand-new collection, one consisting only of those appointments that take place in December, 2007. From there all we have to do is set up a For Each loop to loop through this sub-collection, doing nothing more inside that loop than echoing back some of the key appointment property values (such as meeting name, date, duration, and location):

For Each objItem In colFilteredItems
    If objItem.Start > Now Then
        Wscript.Echo "Meeting name: " & objItem.Subject
        Wscript.Echo "Meeting date: " & objItem.Start
        Wscript.Echo "Duration: " & objItem.Duration & " minutes"
        Wscript.Echo "Location: " & objItem.Location
        Wscript.Echo
    End If
Next

That’s all there is to it.

Now, one thing to keep in mind here is that this script will not necessarily show any recurring appointments that take place in December, 2007; that’s because Outlook treats recurring appointments slightly different than it does other meetings. If the first occurrence of a recurring appointment takes place in December 2007 then that first occurrence will be returned by the script. However, if the first occurrence for the recurring appointment took place earlier then December, 2007, well …. For HG this wasn’t a problem; she was specifically interested in “one-off” meetings and appointments. However, if that is a problem for you (that is, if you’d like to get a list of recurring appointments as well), take a look at the Office Space article Retrieving a List of Recurring Appointments from Microsoft Outlook and see if that helps.

Office Outlook 2007: If you run this script against Outlook 2007, you will see some recurring meetings that fall in December 2007. However, you’ll see only those recurring meetings where the first occurrence hasn’t happened yet. In addition, the meeting date (objItem.Start) will show the first date in the recurring appointment. For example, if you have a recurring meeting the first Friday of every month starting in October 2007, the meeting will show up in your output for December meetings (since there will be a meeting scheduled for Friday, December 7) but the meeting date of your output will show October 5. If the first occurrence of that meeting was in the past (say, June 1, 2007) you won’t see the December occurrence.

And yes, we know: some of you are interested in retrieving a list of the actual days and dates for recurring appointments (as opposed to a note that simply says that this meeting occurs every Tuesday from now until January 1, 2008). That’s something we’ll probably address in a future Hey, Scripting Guy! But not today; today isn’t the future.

Or at least we hope that today isn’t the future. Things are depressing enough around here as they are. Oh, well; tomorrow the temperature is expected to reach 74 degrees Fahrenheit. Talk about a heat wave, eh?

Editor’s Note: The Scripting Guy who writes this column went to the dentist today. We hope that explains any small hint of grouchiness you might have detected in today’s column. We’re not sure how to explain yesterday’s column…or the day before…or….


How Can I Get Access to a Mail Folder That Isn’t a Subfolder of My Outlook Inbox?

Hey, Scripting Guy! How can I access a specified mail folder in my Outlook Mailbox, a folder that isn’t a subfolder of my Inbox?

-- RT

Hey, RT. As you recall, in our last exciting episode – what do you mean you don’t recall any exciting episodes when it comes to this column? Come on: this is Hey, Scripting Guy!, the Kingda Ka of the Scripting World! Remember that episode where we showed people how they could create 150 copies of a single file? You’re telling us that didn’t have you on the edge of your seats?

OK, true. But what about that episode where we – well, come to think of it, that really wasn’t all that exciting, was it? But then there was the time that we – well, no: that was useful, but it wasn’t exactly a thrill-a-minute. Let’s see what we have here: adding leading zeroes to all the lines in a text file; renaming a local user account on a Windows XP computer; counting the number of words in a text file – you know, maybe you’re right. Let’s start over.

Hey, RT. As you recall, in our last less-than-exciting episode we promised to show you how to access a Microsoft Outlook folder that wasn’t a subfolder of your Inbox. What do we mean by that? Well, consider the following Outlook folder structure:

As you can see, the Mailbox includes a number of email folders, folders like Art Submissions, Blogs, Europe, and Fixes Required. As you can see, none of these folders are subfolders of the Inbox; in fact, in the great Outlook folder structure, they all exist at the exact same level as the Inbox folder. That’s great, except for one thing: how the heck do you access a folder that isn’t a subfolder of the Inbox?

Here’s how:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

strFolderName = objInbox.Parent

Set objMailbox = objNamespace.Folders(strFolderName)

Set objFolder = objMailbox.Folders("Europe")

Set colItems = objFolder.Items

For Each objItem in colItems
    Wscript.Echo objItem.Subject
Next

Granted, if this was an exciting column we’d let you enjoy the thrill and adventure of trying to figure out for yourself how this script works. But seeing as how this isn’t an exciting column we’ll go ahead and explain it all for you.

To begin with, we define a constant named olFolderInbox and set the value to 6. Now, don’t panic: we know that we aren’t interested in the Inbox or any of the subfolders of the Inbox. We’ll explain why we need this constant in just a second.

After defining the constant we create an instance of the Outlook.Application object and use the GetNamespace method to bind to the MAPI namespace (the only namespace we can bind to). We then use this line of code to bind to the Inbox folder:

Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

No, we didn’t forget: there’s a reason we start out by binding to the Inbox. In this script we’re trying to retrieve information from the Europe folder, which happens to be a subfolder of the Mailbox – Ken Myer folder. In order to get to the Europe folder we need to first bind to Mailbox – Ken Myer; we can bind to the Europe folder only after we’ve done that. We need to work our way through the folder structure, one level at a time.

So then why do we start off by connecting to the Inbox folder? Well, in this case we know the name of the mailbox folder: Mailbox – Ken Myer. That’s great. But suppose you share this script with someone else. In that case, the odds are pretty slim that their mailbox will also be named Mailbox – Ken Myer. What we’re trying to do here is develop a script that can connect to any mailbox folder, without having to hardcode the mailbox name into the code.

Make sense? The only problem with that is the fact that we don’t know of any way to directly retrieve the mailbox name. (We’re not saying that there isn’t such a way; we just don’t know what it is.) However, we do know that the mailbox name can be retrieved by binding to the Inbox and then grabbing the value of the Parent property. Because the Inbox is a child folder of the mailbox, the Inbox Parent will be – tah-dah! – the mailbox. That’s what this line of code is for:

strFolderName = objInbox.Parent

Once we know the name of the mailbox (which we stash inside the variable strFolderName) we can then use this line of code to create an object reference to the mailbox folder:

Set objMailbox = objNamespace.Folders(strFolderName)

Was that worth the effort? You bet it was; after all, once we’re connected to the mailbox we can then bind to any subfolder found inside that mailbox folder. Want to connect to the Europe folder? Then just do this:

Set objFolder = objMailbox.Folders("Europe")

See how that works? We simply reference the mailbox Folders collection, specifying the name of the folder (Europe) that we want to connect to. In turn, that gives us an object reference (objFolder) that points to the Europe folder.

From that point on it’s a piece of cake. To retrieve a collection of all the items (email messages) in the Europe folder we use this line of code:

Set colItems = objFolder.Items

And from there we simply set up a For Each loop to loop through all the items in the collection. In this little sample script all we do is echo back the Subject of each message, although once you have this collection you can do pretty much anything you want with it.

Like what? Well, you know the Scripting Guys: all our suggestions will be dull and boring. You guys can probably come up with something way more exciting. Now, if you’ll excuse us, we’re going to have a glass of warm milk and then take a little nap. We don’t want to get ourselves too worked up, you know.


How Can I Get Total Size and Number of Items in an Outlook Folder?

Hey, Scripting Guy! How can I get the total number of items – and the total size – of all the items in my Inbox and Sent Items folders?

-- CS

Hey, CS. You know, one of the Scripting Guys has a foolproof method for determining this information. What does he do? Nothing; instead, he just sits around and waits for the inevitable email from the Exchange admin telling him that his mailbox has once again exceeded its size limit and that he will no longer be able to send or receive mail until he gets rid of some stuff. At that point he knows the total size of the Inbox (too big) as well as the total number of items (too many).

Of course, it’s a good rule of thumb never to pattern your life after any of the Scripting Guys. With that in mind, here’s another way to determine the number of items (and total size) in both your Inbox and Sent Items folders:

Const olFolderInbox = 6
Const olFolderSentMail = 5

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)
Set colItems = objFolder.Items
Wscript.Echo "No. of items in Inbox: " & colItems.Count

For Each objItem in colItems
    intSize = intSize + objItem.Size
Next

Wscript.Echo "Size of Inbox: " & Int(intSize / 1024) & " KB"

intSize = 0

Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)
Set colItems = objFolder.Items
Wscript.Echo "No. of items in Sent Mail folder: " & colItems.Count

For Each objItem in colItems
    intSize = intSize + objItem.Size
Next

Wscript.Echo "Size of Sent Mail folder: " & Int(intSize / 1024) & " KB"

Yes, we know: it does look a little complicated, doesn’t it? But don’t worry; we’ll walk you through the code and explain everything to you. After all, that’s what we’re here for.

Well, that and to clean up the offices of the real Microsoft employees after they leave for the day. But you know what we mean.

To begin with, we define a pair of constants, which we’ll use later to indicate which folders we want to look at:

Const olFolderInbox = 6
Const olFolderSentMail = 5

We then use these two lines of code to create an instance of the Outlook.Application object and to bind to the MAPI namespace (the only namespace you can bind to):

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

At this point we should mention that our script assumes that Outlook is already running. If it’s not, you’ll have to make a minor modification to the script in order to start Outlook and to log on. We won’t go into that in today’s column; that’s because we have an Office Space column lying around somewhere that explains how to start Outlook from scratch.

Oh: turns out that column is right here.

After connecting to Outlook we next need to bind to the Inbox and determine the number of items found there. That’s easy; in fact, it takes just three lines of code:

Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)
Set colItems = objFolder.Items
Wscript.Echo "No. of items in Inbox: " & colItems.Count

As you can see, we begin by calling the GetDefaultFolder method, passing along our constant olFolderInbox; this binds us to the Inbox folder. We then use the second line of code to return a collection of all the items found in the Inbox. As it turns out, Outlook collections all include a Count property, which tells us the number of items in the collection: in order to know the number of items in the Inbox all we have to do is echo the value of the Count property. And that’s exactly what we do in line 3.

Needless to say, retrieving the number of items in a folder is easy; determining the total size of the folder is a tiny bit more complicated. That’s because there is no Size property that tells us the size of a folder; instead, we have to calculate the size ourselves.

Hey, relax: it’s not that big of a deal. Each item in the Inbox (e.g., each mail message) does have a Size property, a property that expresses the size of the item in bytes. To determine the total size of the Inbox folder all we have to do is add up the sizes of the individual items in the folder. We can do that by setting up a For Each loop to walk through the collection, keeping a running total of the folder size as we go (a tally stored in a variable we named intSize). You know, something that looks like this:

For Each objItem in colItems
    intSize = intSize + objItem.Size
Next

After we’ve walked through the entire collection we’ll know the size of the Inbox folder. Although we could echo back that value in bytes, we decided to get a little fancy and divide the value by 1024; that gives us the size in kilobytes (KBs). That’s what we do here:

Wscript.Echo "Size of Inbox: " & Int(intSize / 1024) & " KB"

Granted, this construction looks a little weird: Int(intSize / 1024). But all we’re doing is taking the size of the Inbox in bytes (a value stored in the variable intSize) and dividing it by 1024. We’re then taking that value (the size of the Inbox in kilobytes) and using the Int function to drop the decimal point. That way we get back an Inbox size of, say, 11,318 KB rather than 11,318.462811 KB.

All we have to do now is set the value of intSize back to 0 and then start the process all over again, this time by binding to the Sent Items folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)

Etc. etc. etc.

Granted, a script like this one lacks the personal touch of a threatening email from the Exchange admin. On the other hand, it does give you slightly more precise information than “too big” and “too many.” We’ll leave it up to you to decide which method will work best for you.


How Can I List All the Meetings Scheduled By a Specified Person?

Hey, Scripting Guy! How can I get a list of all the upcoming meetings that have been scheduled by a specific person (namely, my manager)?

-- GH

Hey, GH. Before we tackle this question we’d like to reassure anyone who looked out their window recently and saw pigs flying; that’s to be expected. Likewise, any of our readers who happen to be residents of Hades might have been alarmed by the recent cold snap down there. Trust us; there’s nothing to be alarmed about. Pigs flying, hell freezing over – those things are bound to happen when the Scripting Son draws a walk in his last at-bat of the high school baseball season.

That’s right, after 18 games and 60-some at-bats the Scripting Son finally drew a walk, and in his last at-bat of the season to boot. Not that this is particularly unusual. Three years ago, for example, he was on an all-star team that toured Japan. During that time he played in 14 games and walked once: in his final at-bat. That same year he went the entire regular season (nearly 40 games) without walking at all; oddly enough, in his first playoff game, he then walked three times. Last year the Scripting Son played in 60 games and walked four times. Etc., etc.

Needless to say, the Scripting Son doesn’t like to walk.

So what difference does this make to you, GH? Quite a bit, believe it or not. After all, the Scripting Guy who writes this column has always said, “Write a script that retrieves a list of upcoming appointments that have been scheduled by a specific person? When pigs fly!”

Well, in that case:

Const olFolderCalendar = 9

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderCalendar)

Set colItems = objFolder.Items

strFilter = "[Organizer] = 'Ken Myer'"
Set colFilteredItems = colItems.Restrict(strFilter)

For Each objItem In colFilteredItems
    If objItem.Start > Now Then
        Wscript.Echo "Meeting name: " & objItem.Subject
        Wscript.Echo "Meeting date: " & objItem.Start
        Wscript.Echo "Duration: " & objItem.Duration & " minutes"
        Wscript.Echo "Location: " & objItem.Location
        Wscript.Echo
    End If
Next

Before we explain how this works we need to go turn up the heat; for some reason it seems a little colder here than it usually is.

OK, that’s better. As you can see, we start out by defining a constant named olFolderCalendar and setting the value to 9; we’ll use this constant to tell Outlook which folder (the Calendar folder) we want to work with. Next we create an instance of the Outlook.Application object, then connect to the MAPI namespace (which happens to be the only namespace we can connect to). We then use the GetDefaultFolder method to bind to the Calendar folder in Outlook:

Set objFolder = objNamespace.GetDefaultFolder(olFolderCalendar)

That was pretty easy, wasn’t it?

Our next step is to create an object reference to the folder’s Items property:

Set colItems = objFolder.Items

What does that do for us? That grabs a collection of all our appointments (that is, everything in the Calendar folder) and stashes it in a variable named colItems.

And yes, you’re right: we’re not interested in all the items in the Calendar folder, are we? Instead, we’re only interested in upcoming meetings that have been organized by our manager, Ken Myer. Somehow we need to filter our collection. But how?

Well, here’s one suggestion: why not apply a filter?

strFilter = "[Organizer] = 'Ken Myer'"
Set colFilteredItems = colItems.Restrict(strFilter)

As you can see, in the first line of code we assign a filter value to the variable strFilter. Filters are made up of two parts: a property name (enclosed in square brackets) and a property value (in this case, a value enclosed in single quote marks, because we are dealing with a string). We want to limit our data to meetings arranged by Ken Myer; that is, meetings where the Organizer property is equal to Ken Myer. Hence the two parts of our filter: [Organizer] and ‘Ken Myer’.

In line 2, we then call the Restrict method to apply this filter to our collection. That’s going to create a new collection (named colFilteredItems) that contains information only about those meetings organized by Ken Myer.

That’s almost what we need. However, this sub-collection will contain all the meetings organized by Ken Myer, including those that have already taken place. Because GH is only interested in upcoming meetings, we need to weed out the meetings that have already taken place.

In theory, we could do that by making a fancier filter. However, filtering on dates can be a bit complicated; therefore, we decided to take the easy way out. Rather than apply a double filter – one that limits returned data to meetings organized by Ken Myer, provided that those meetings haven’t been held yet – we applied a filter that returns all the meetings organized by Ken Myer. We then set up a For Each loop to walk through the complete collection of meetings. And what’s the first thing we do in that loop? Check to see if the meeting has already been held:

If objItem.Start > Now Then

If the meeting’s Start time is later than the current date and time (which we can determine using VBScript’s Now function) that means that the meeting hasn’t taken place yet. Therefore, we go ahead and echo back the meeting’s name, start time, duration, and location; that’s what this block of code is for:

Wscript.Echo "Meeting name: " & objItem.Subject
Wscript.Echo "Meeting date: " & objItem.Start
Wscript.Echo "Duration: " & objItem.Duration & " minutes"
Wscript.Echo "Location: " & objItem.Location

And then we loop around and repeat the process with the next meeting in the collection.

One thing to watch out for here. Because of the way Outlook stores recurring appointments, those appointments might not show up in your output. That’s due, in large part, to the fact that the start date for a recurring appointment might be long-since past. Because of that, you might need to use two scripts to make sure you get the desired information: the script we just showed you, and a second script designed to retrieve the recurring appointments organized by Ken Myer. What might that second script look like? It might look a little like this:

Const olFolderCalendar = 9

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderCalendar)

Set colItems = objFolder.Items

strFilter = "[IsRecurring] = TRUE AND [Organizer] = 'Ken Myer'"

Set colFilteredItems = colItems.Restrict(strFilter)

For Each objItem In colFilteredItems
    Set objPattern = objItem.GetRecurrencePattern
    If objPattern.PatternEndDate > Now Then
        Wscript.Echo "Meeting name: " & objItem.Subject
        Wscript.Echo "Duration: " & objItem.Duration & " minutes"
        Wscript.Echo "Location: " & objItem.Location
        Wscript.Echo "Recurrence type: " & objPattern.RecurrenceType
        Wscript.Echo "Start time: " & objPattern.StartTime
        Wscript.Echo "Start date: " & objPattern.PatternStartDate
        Wscript.Echo "End date: " & objPattern.PatternEndDate
        Wscript.Echo
    End If
Next

Of course, you could always combine the two scripts into a single script. But we’ll let you take care of that yourself.

Note. You say you don’t understand how the preceding script works? Then you might want to take a look at our Office Space article on retrieving recurring appointments.

Meanwhile, the Scripting Son is getting ready for his summer ball season, a season in which he’ll play another 50-to-60 games. Will he draw many walks during this season? Probably not; at least when it comes to baseball the Scripting Son is impatient, he’s impetuous, and he just goes out there and acts without thinking.

Wonder where he learned all that from ….


How Can I List All the Members of a Microsoft Outlook Distribution List?

Hey, Scripting Guy! How can I list all the members of a Microsoft Outlook distribution list?

-- KH

Hey, KH. You know, that’s an interesting question. As it turns out, you can – ow! Hey, let go! Ow! Stop it! Come on, give us a chance; we were just about to tell them! Ow!

Um, excuse us. It has come to our attention that, due to a … production error … yesterday’s column inadvertently made a number of less-than-flattering references to the Scripting Editor. (Ow! Come on; that really hurts! We were just about to get to that part.) The Scripting Guys deeply regret any misconceptions that might have arisen due to that column. Needless to say, we have nothing but the utmost respect for the Scripting Editor, who is not only – by far – the smartest and most competent member of the team, but is a truly kind and caring human being as well. As such, we know that the Scripting Editor will accept our humble and sincere apology; we hope that the rest of you will, too.

There; we said it. Now will you let us go? After all, we can’t very well write today’s column if you keep twisting our arms like that.

Ow! You know, that last twist was totally uncalled for.

No, that’s OK; we’ll stop complaining. You’re absolutely right: we’d prefer that you didn’t give us something to complain about.

Now, where were we? Oh, right: listing all the members of a Microsoft Outlook distribution list. Here you go, KH; try this baby on for size:

Const olFolderContacts  = 10

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderContacts)

Set objList = objFolder.Items("Approved Vendors")

For i = 1 to objList.MemberCount
    Set objMember = objList.GetMember(i)
    Wscript.Echo objMember.Name & ", " & objMember.Address
Next

As you can see, listing all the members of an Outlook distribution list is actually pretty easy. We begin by defining a constant named olFolderContacts and setting the value to 10; that tells the script to look for the distribution list in the Contacts folder (which, by default, is where distribution lists tend to live). We then use these three lines of code to create an instance of the Outlook.Application object and bind us to the MAPI namespace and the Contacts folder:

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderContacts)

At this point we’re ready for action. Let’s assume that we want to list the members of the Approved Vendors distribution list. To do that, our first step is to use this line of code to connect to the Approved Vendors list:

Set objList = objFolder.Items("Approved Vendors")

Nothing special there; we’re simply creating an object reference to the distribution list in question. If we wanted to connect to a different distribution list we’d simply reference the name of that list when we create the object reference:

Set objList = objFolder.Items("Northwest Region Team")

Once we make the connection we can then go ahead and list all members. To do that we set up a For Next loop that runs from 1 to the number of people who are members of the list. How are we supposed to know how many people are on the list? Beats us. But, then again, we don’t have to know; instead, we can let Outlook figure that out for us by using the MemberCount property:

For i = 1 to objList.MemberCount

Inside this loop we use the GetMember method to retrieve the individual members of the list, one at a time, and referencing each member by index number (the counter variable i):

Set objMember = objList.GetMember(i)

Each time we call GetMember we’ll get back an AddressEntry object. In turn, that enables us to echo back the values of the Name and Address properties:

Wscript.Echo objMember.Name & ", " & objMember.Address

And that’s going to give us output similar to this:

Ken Myer, kenmyer@fabrikam.com
Pilar Ackerman, pilarackerman@contoso.com
Jonathan Haas jhaas@wingtiptoys.com

Ow! Stop it; we were just going to tell them that! Give us a chance, for crying out loud.

No, ma’am; we were definitely not complaining about you. No way.

As the Scripting Editor … politely … reminded us, any time you try and retrieve information about a distribution list’s membership a dialog box will pop us asking you if you want to allow access to that information. If you say “Yes” then the script will retrieve and display the requested data. If you say “No,” or if you ignore the dialog box altogether, then nothing will be retrieved and nothing will be displayed. The moral of that story? You can’t run this script in unattended fashion; instead, you’ll need to start the script and respond to the dialog box. At that point you can let the script run by itself, although at that point it will probably take only a few seconds for the script to complete its work. (Depending, of course, on the number of people who are part of the distribution list.)

Because of that you might prefer to list the all the members of all your distribution lists in one fell swoop. Hey, no problem. Assuming that all your distribution lists reside in the Contacts folder this should do the trick:

Const olFolderContacts  = 10

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderContacts)
Set objItems = objFolder.Items

For i = 1 to objItems.Count
If TypeName(objItems.Item(i)) = "DistListItem" Then
Set objList = objItems.Item(i)
Wscript.Echo objList.DLName
For j = 1 to objList.MemberCount
Set
objMember = objList.GetMember(j)
Wscript.Echo
objMember.Name & ", " & objMember.Address
Next
Wscript.Echo
End If
Next

In conclusion, we’d like to reiterate that everything we said in today’s column we said of our own free will, and no one – least of all our beloved Scripting Editor – threatened us in any way, shape, or form. Thank you.

Note to the Scripting Editor. OK, we said it; now can Dean have his kids back? Oh, come on: you said if we included the above disclaimer he could get both of them back. Well, you’re the boss. But he’s not going to be very happy about this.


How Can I Randomly Select an Email From an Outlook Mail Folder?

Hey, Scripting Guy! How can I randomly select an email from an Outlook mail folder?

-- TV

Hey, TV. You know, for a brief moment last night the Scripting Guy who writes this column thought he had achieved every father’s dream. The Scripting Dad and the Scripting Son went to the gym to play basketball, and when they were done the Scripting Son said, “Hey, Dad. Can I ask you something? It’s for my school project.”

Now, to all outward appearances, the Scripting Guy who writes this column remained as cool and implacable as he always is. Inside, however, he was very excited. After all, the Scripting Son had recently been given an assignment to write about his hero. Obviously the Scripting Son – like so many other people throughout the world – had no doubts about who his hero was: the Scripting Guy who writes this column.

“Sure,” said the Scripting Dad, trying to suppress the giant smile that was building up inside him. “Ask me anything you want.”

“OK,” said the Scripting Son. “What’s your favorite Dr. Seuss book?”

“What? Dr. Seuss book? I don’t know. Green Eggs and Ham?”

“OK, thanks.”

And that was it; as it turns out, the Scripting Son was working on a second school project, one involving statistics and opinion polls. He needed the Scripting Dad’s help simply because he had to ask 100 people what their favorite Dr. Seuss book was.

For his other school project, the one where he had to write about his hero, the Scripting Son chose Vladimir Guerrero of the California Angels. In other words, he chose a baseball player (a good baseball player, but a baseball player nevertheless) as his hero. A baseball player rather than a Scripting Guy?!? Say it isn’t so!

Note. Yes, the Scripting Guy who writes this column knows that the team is no longer called the California Angels; they’re now the Los Angeles Angels of Anaheim. On the other hand, the team changes its name about as often as – well, the team changes its name an awful lot, so it’s just a matter of time before they’re the California Angels again.

Now that you mention it, the Scripting Guy who writes this column was going to say that the team changes its name “as often as a woman changes her hairstyle.” However, seeing as how the Scripting Editor is a woman (or at least purports to be) he knew that saying something like that would be a major mistake. Although now that he thinks about it, “as often as a woman changes her mind” would have been good, too.

Before you ask, no, changing that to “as often as a man changes his mind” wouldn’t work. After all, the Scripting Guy who writes this column has never known a man to change his mind, largely because men never let silly things like facts get in their way.

To make a long story short, it turns out that the Scripting Dad isn’t the Scripting Son’s hero. (Although the Scripting Dad doesn’t remember Vladimir Guerrero helping pay for the Scripting Son’s summer baseball team.) But maybe he can be your hero, TV. He’s probably not going to help pay for your summer baseball team (sorry), but he will give you a script that lets you randomly select an email from an Outlook mail folder:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

strFolderName = objInbox.Parent

Set objMailbox = objNamespace.Folders(strFolderName)
Set objFolder = objMailbox.Folders("2007 Scripting Games")

Set colItems = objFolder.Items
intHighNumber = colItems.Count

intLowNumber = 1

Randomize
intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

Wscript.Echo colItems(intNumber).SenderName

OK, so maybe that doesn’t look very heroic. But that’s OK; you’ll probably change your mind as soon as we explain how the script works. To begin with, we define a constant named olFolderInbox and set the value to 6; that’s going to tell the script which Outlook folder we want to connect to. (More on that in a second.) We then use this block of code to create an instance of the Outlook.Application object:

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

Some of you might be wondering why we connected to the Inbox rather than to the Mailbox folder; after all, in many cases Outlook mail folders aren’t subfolders of the Inbox they’re subfolders of the Mailbox folder. For example, suppose we want to use the following Outlook setup and work with the 2007 Scripting Games folder:

As you can see, 2007 Scripting Games is a subfolder of Mailbox, not Inbox. So then why did we connect to the Inbox folder?

Don’t ask us; why don’t you go ask Vladimir Guerrero?

No, wait; Vlad’s not going to help you, not unless you have the bases loaded and need somebody to whack one off the centerfield fence. We connected to the Inbox simply because we can’t connect to the Mailbox folder without specifying the folder name. That’s a bit of a problem, because the folder isn’t actually named Mailbox; in this case, for example, it’s named Mailbox – Greg Stemp. We could have hard-coded that name into the script, but then every single person who tried to run this piece of code would need to go in and change that value to reflect the name of their Mailbox folder. That didn’t seem like much fun, so we opted to connect to the Inbox, then use this line of code to retrieve the name of the Inbox’s Parent folder:

strFolderName = objInbox.Parent

As you might expect, the Parent folder for the Inbox just happens to be the Mailbox folder.

Once the name of the Mailbox folder is safely stashed in the variable strFolderName we then execute these two lines of code:

Set objMailbox = objNamespace.Folders(strFolderName)
Set objFolder = objMailbox.Folders("2007 Scripting Games")

In line 1 we make a connection to the Mailbox folder; in line 2 we make a connection (with the object reference objFolder) to our target folder: 2007 Scripting Games. And now we’re ready for action.

In order to randomly select an email we need to pick a number between 1 and the number of items in the folder. (That should make sense; if we have 77 items in the folder we can’t randomly choose item number 1918.) To determine the number of items in the 2007 Scripting Games folder we use these two lines of code to grab a collection of all those items and then store the value of the collection’s Count property in a variable named intHighNumber:

Set colItems = objFolder.Items
intHighNumber = colItems.Count

Again, let’s assume there are 77 items in the folder. In that case, we’re going to randomly select a number between something and 77. What’s something? Well, most likely that’s going to be 1 (thus making our choice a number between 1 and 77). With that in mind we assign the value 1 to a variable named intLowNumber:

intLowNumber = 1

With our numeric range established we next call the Randomize function, an act that “seeds” our random number generator. After that we use this crazy-looking line of code to generate a random integer value between 1 and 77 (or however many items we have in our folder):

intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

Note. We won’t bother explaining this line of code today. If you’d like a little more information about generating random numbers, well, we just happen to have a Hey, Scripting Guy! column on that very subject.

So what do we do with this random number now that we have it? Well, we can use it as an index number for our collection of all the items found in the 2007 Scripting Games folder (a collection named colItems). Suppose our random number is 13. That means that this line of code will echo back the value of the SenderName property for item 13 in the collection:

Wscript.Echo colItems(intNumber).SenderName

Note that, because we’re dealing with SenderName, Outlook is going to pop up a security dialog box and we’re going to have to explicitly give the script access to this property. Some properties (like SenderName and anything representing an email address) require you to go through this security procedure. Other properties (such as Subject) can be retrieved automatically. For more information, see our Office Space article on, well, that very subject.

Well, not entirely on that subject. But we do talk about it.

By the way, we should also note that, after all this, the Scripting Dad isn’t even sure that Green Eggs and Ham even is his favorite Dr. Seuss book. After all, there are the two Horton books (Horton Hears a Who and Horton Hatches the Egg). And then there’s the vastly-underrated Yertle the Turtle. (Which, as Lisa Simpson once noted, remains the best book ever written on the subject of turtle stacking.) In fact, as a father, there’s really only one Dr. Seuss book that the Scripting Dad finds a bit unsettling: Hop on Pop. Never, ever let your children read that one.


How Can I Save All My Contacts as VCards?

Hey, Scripting Guy! How can I save all my contacts as VCards?

-- ET

Hey, ET. You know, we have to tell you the truth: after nearly 800 Hey, Scripting Guy! articles the Scripting Guy who writes this column is beginning to get a little burnt-out. Fortunately, the Scripting Editor has often volunteered to do the column for him, and the Scripting Guy who writes this column has decided to take her up on that generous offer. Therefore, and without any further adieu, let’s turn today’s Hey, Scripting Guy! over to the Scripting Editor:

Hey, ET. Oh, ET: I get it; that was, like, the name of that Martian guy, right? That’s, like, really funny, you know, I mean, I can’t, like, wait to tell Cassandra.

Um, so, anyway, ET (tee-hee!), you know, it sounds like you want to save your contacts as …something … oh, wait, I know: you want to save your contacts as, like, VCards, right?. So, like, do you have any idea what VCards are, you know, I mean, are they, like, better than contacts, so, like, maybe that’s why you want to save your contacts as those card thingies? I mean, because I really like contacts, you know, like, my friend, Jessica, she has these contacts that can, like, make her eyes look like different colors, you know, like, on Monday, you know, maybe she has, like blue eyes, and then, like, on Tuesday or whatever she changes contacts and, like, suddenly it looks like she has green eyes, and, like, that is way cool, you know, and like Jessica’s boyfriend, Todd, Todd’s like, “Wow, it’s like I have a different girlfriend every day of the week!” and I’m thinking, I wish you did have a different girlfriend every day of the week, because Todd is a real hunk and I would totally go out with him except, like, Jessica’s my friend, you know, so I guess that wouldn’t be very cool, you know? So anyway, contacts are awesome but, like, maybe these VCards, whatever they are, well, like, maybe they’re better, you know?

OK, thank you, Scripting Editor. That helped; the Scripting Guy who writes this column suddenly feels much better and much more refreshed now. In fact, we’ll tell you what: why don’t you finish doing whatever Scripting Editors do, and we’ll show ET how to – um, yes, we get it, although we don’t believe the real ET was actually a Martian. Anyway, why don’t you go do whatever it is you do and we’ll show ET a script that can save all his contacts as VCards:

On Error Resume Next

Const olFolderContacts = 10
Const olVCard = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set colContacts = objNamespace.GetDefaultFolder(olFolderContacts).Items

For Each objContact In colContacts
    strName = objContact.FirstName & objContact.LastName
    strPath = "C:\Test\" & strName & ".vcf"
    objContact.SaveAs strpath, olVCard
Next

As you can see, ET, the script – sorry; just a second. Yes, those shoes are darling, just darling. And they look totally awesome with that sweater.

Totally.

Anyway, as we were saying, the script starts out by implementing the On Error Resume Next statement. Whenever we work with contacts we’re prone to getting somewhat-inexplicable errors from time-to-time, errors that, to be honest, don’t make a lot of sense to us; for example, the script might fail, complaining that a valid contact property (like FirstName) isn’t a valid property after all. We’re not sure if this is a generic problem or if it just has something to do with our copy of Outlook; however, tossing in the On Error Resume Next statement seems to take care of the problem, so we did that here just as a precaution.

Our first real bit of coding is to define a pair of constants: olFolderContacts, which tells the script which Outlook folder we want to work with; and olVCard, which tells the script the file format to use when saving the contact information. After defining the constants we create an instance of the Outlook.Application object, then use the following line of code to bind to the MAPI namespace (a required step, even though the MAPI namespace is the only namespace we can bind to):

Set objNamespace = objOutlook.GetNamespace("MAPI")

Once we’ve successfully connected to Outlook we can then use the following line of code and the GetDefaultFolder method to retrieve a collection of all the items found in the Contacts folder:

Set colContacts = objNamespace.GetDefaultFolder(olFolderContacts).Items

That was, like, way easy, you know?

Sorry; we couldn’t resist.

Actually, that was way easy and, best of all, the rest of the script is just as easy. Our next chore is to set up a For Each loop to loop through the collection of contacts. As everyone knows – well, OK, as most people know – VCard is just another file format; consequently, that means that we can’t save contact information as a VCard unless we specify the complete file path for this new file. We decided to create a file name based on the contact’s first name and last name; thus we use this line of code to assign the values of the FirstName and LastName properties to a variable christened strName:

strName = objContact.FirstName & objContact.LastName

If we have a contact named Ken Myer that’s going to result in a file name that looks like this:

KenMyer

Once we have a unique file name we can then construct the complete path:

strPath = "C:\Test\" & strName & ".vcf"

Nothing too fancy here: we’re just combing the folder path C:\Test\ with the value of the variable strName and .vcf, which represents the file extension (because VCards use a .VCF file extension). From there we go ahead and create a VCard for our first contact, something we do by calling the SaveAs method, passing the file path (the variable strPath) and the constant olVCard as the two method parameters:

objContact.SaveAs strpath, olVCard

Important. Because creating a VCard requires you to access the contact’s email address this script won’t just run the moment you kick it off; instead, Outlook is going to pop up a message informing you that the script is trying to access address data and ask if you want to allow this access. You must answer “Yes” (or “Allow”) in order for the script to run. And, sadly, there’s no way – short of reconfiguring your Exchange setup – to bypass or automate the process of answering this message box. Just something to keep in mind.

After that we loop around and repeat the process with the next contact in the collection. When all is said and done the folder C:\Test should include a bunch of files similar to these:

KenMyer.vcf
JonathanHaas.vcf
PilarAckerman.vcf

That should do it, ET. And now we’ll let the Scripting Editor have the final word on today’s column:

Um, like, you know?

Thank you, Scripting Editor; we couldn’t have said it better ourselves.

Editor’s Note: Given that the Scripting Editor didn’t sound like a ditzy teenager even when she was a teenager (which really wasn’t that long ago, by the way), we’re not sure where the Scripting Guy who writes this column got the preceding text. What we are sure of is that the Scripting Guy who writes this column will be apologizing profusely for painting the Scripting Editor in such a poor light, just as soon as he’s done washing her car…and cleaning her office…and walking her dog…and bringing her donuts for breakfast every morning for the rest of her life….


How Can I Save Emails That Are More Than One Month Old?

Hey, Scripting Guy! How can I save all the items in my Sent Items folder that are more than one month old?

-- RS

Hey, RS. You know, we’d like to answer this question; we really would. However, the Scripting Guy who writes this column is leaving on vacation tomorrow, which means that – being an important and valued employee of the Microsoft Corporation – he has an awful lot to do before he can take off. For example, he has to – well, never mind; they don’t let him do that anymore. But he does have to arrange for – no, scratch that; although the fire wasn’t really his fault, they don’t let him do that anymore, either. Of course, he still has to – you know what? Maybe, despite his busy schedule, he can answer this question for you after all:

On Error Resume Next

Const olFolderSentMail = 5
Const olMSG = 3

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)

dtmTargetDate = Date - 30

Set colItems = objFolder.Items
Set colFilteredItems = colItems.Restrict("[CreationTime] <'" & dtmTargetDate & "'")

For Each objMessage In colFilteredItems
    strName = objMessage.Subject
    strName = Replace(strName, ":", "")
    strName = Replace(strName,"/","")
    strName = Replace(strName,"\","")
    strName = Replace(strName,",","")
    strName = Replace(strName, Chr(34),"")
    strName = Replace(strName,Chr(39),"")
    strName = Replace(strName,"?","")

    strName = "C:\Test\" & strName & ".msg"
    objMessage.SaveAs strName, olMSG
Next

Let’s see if we can figure out how this script works. (Incidentally, the script isn’t anywhere near as complicated as it looks. And don’t worry: we’ll explain why it looks so complicated in a minute or two.)

As you can see, we start out by defining a pair of constants: olFolderSentMail (which represents the mail folder we want to access, the Sent Items folder), and olMSG (which represents the file format – Outlook message format – we want to use when saving each item in the folder). After that we create an instance of the Outlook.Application object, bind to the MAPI namespace, then use this line of code to connect to the Sent Items folder:

Set objFolder = objNamespace.GetDefaultFolder(olFolderSentMail)

Whew!

Our end goal is to go through the Sent Items folder and save a copy of each message that’s more than a month old. (Or, for our purposes, more than 30 days old.) How are we supposed to know whether or not a message is more than a month old? Actually, that’s pretty easy to determine: all we have to do is compare the message’s CreationTime property with the date from a month ago. If the CreationTime is less than that date, then the message must be more than one month old.

Of course, in order to do that we need to know exactly what was the date 30 days prior to today. Fortunately, we can determine that simply by subtracting 30 days from the current date, something we do here:

dtmTargetDate = Date - 30

Now we’re ready to retrieve a collection of all the sent mail items that are more than 30 days old. The first step in that process is to use this line of code to retrieve a collection of all the items in the Sent Items folder:

Set colItems = objFolder.Items

From there we apply a Filter to the collection, restricting the data to items where the CreationTime property is earlier (less than) the value of the variable dtmTargetDate:

Set colFilteredItems = colItems.Restrict("[CreationTime] <'" & dtmTargetDate & "'")

And no, we won’t spend any time talking about Outlook filters in today’s column; for more information, see our article Filtering Email Messages in Microsoft Outlook. The main things to note for now: you need to put square brackets around the property name ([CreationTime]) and, even though dtmTargetDate is a date-time value, you need to treat it like a string value; hence the crazy quotation marks:

"[CreationTime] <'" & dtmTargetDate & "'"

At this point we’d be done (pretty much), except for one thing. We’ve decided to use the value of the Subject property as the file name for each saved file. That’s fine, except that email Subject lines often include characters (such as colons) that can’t be included in file names:

RE: The TechNet Script Center

Because of that, we can’t save a message until we remove these illegal characters. How are we going to do that? We’re glad you asked that question.

To begin with, we set up a For Each loop to loop through all the items in our collection; inside that loop, we assign the value of the Subject property to a variable named strName. That’s what these two lines of code are for:

For Each objMessage In colFilteredItems
    strName = objMessage.Subject

We then use a series of Replace commands to replace invalid file name characters with, for our purposes, nothing. For example, this command removes any colons from the variable strName:

strName = Replace(strName, ":", "")

If you take a closer look at the For Each loop, you’ll see that we have similar commands to remove periods, commas, single quote marks (chr(39)) and double quote marks (chr(34)). Of course, there might be other characters you need to remove in order to create valid file names; if that’s the case, well, we’ve left it up to you to add in additional Replace commands as needed.

After we’ve cleaned up the value of the variable strName we then use this line of code to construct a file path for our new file:

strName = "C:\Test\" & strName & ".msg"

This gives us a file path similar to the following (note that the colon following the RE has been removed):

C:\Test\RE The TechNet Script Center.msg

From there we can save the file by calling the SaveAs method, passing the file path and the file type (the constant olMSG) as the two method parameters:

objMessage.SaveAs strName, olMSG

And then it’s back to the top of the loop, where we repeat the process with the next item in the collection.

That should get you going, RS. And now, all kidding aside, it’s time for the Scripting Guy who writes this column to do what he really does each and every day: go to lunch with the other Scripting Guys. Sure, it’s a tough job. But someone has to do it.

Note. Incidentally, that’s not just a figure of speech: someone does have to do it. Or at least someone has to go with Scripting Guy Dean Tsaltas, who inevitably forgets either his card key or the way to the cafeteria.

Or both.


How Can I Start Outlook If It Isn’t Already Running?

Hey, Scripting Guy! How can I determine if Outlook is running and, if it isn’t, start it?

-- NA

Hey, NA. You know, this past weekend the Scripting Guy who writes this column found himself wandering through the local shopping mall, a turn of events which left him very uneasy, almost fearful. Why? Well, the last time he’d been in the mall (during the height of the Christmas shopping rush) he’d been meandering along when he was approached by a young woman who spoke with a very heavy French accent. Due to both the accent and the noise inside the mall, the Scripting Guy who writes this column had difficulty understanding her at first; he assumed, however, that the woman was asking for directions or something. Because of that he tried focusing very hard on what the woman was saying. She must have mistaken this concentration for intense interest, because the next thing he knew she had poured salt all over his hands and was pantomiming that the Scripting Guy should now thoroughly “wash” his hands with the salt.

“Do you know which place on Earth has the lowest elevation?” she asked, a question which caught the Scripting Guy off guard. What, she needs directions to the Dead Sea?

“The Dead Sea?” replied the Scripting Guy.

“Oh,” she said, a little disappointed. “No one ever gets that right. Yes, the Dead Sea, and this is salt taken directly from the Dead Sea. If you rub it all over your hands and then wash it off you’ll be amazed how smooth your skin will be. Your hands will be as soft as a baby’s bottom.”

To her credit, she was right: once he got all the salt off, the Scripting Guy’s hands were as soft as a baby’s bottom. That was great, too, except for one problem: the Scripting Guy who writes this column doesn’t want his hands to be as soft as a baby’s bottom. Although he doesn’t consider himself a wimp, he apparently comes off as a wimp; for example, any time he plays basketball or football the first thing people try to do is run right over the top of him. Obviously, people aren’t all that impressed with his manliness to begin with; the last thing he needs is baby-soft hands. But, at least for a few hours, he had them.

We don’t know if you’re interested in having soft hands, NA; if you are, Dead Sea salt seems to do the trick. But if all you care about is a script that can determine whether or not Outlook is running and, if not, start it, well, then this should do the trick:

Const olFolderInbox = 6

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'outlook.exe'")

If colItems.Count = 0 Then
    Set objOutlook = CreateObject("Outlook.Application")
    Set objNamespace = objOutlook.GetNamespace("MAPI")
    objNamespace.Logon "Default Outlook Profile",, False, True
    Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)
    objFolder.Display
End If

We hope you can read that, NA; as it turns out, it’s hard to type with really soft hands. (Maybe that’s why babies don’t do much typing.) As you can see, we start out by defining a constant named olFolderInbox and setting the value to 6; we’ll use this constant when (or, more precisely, if) we open Outlook. Following that we connect to the WMI service on the local computer, then use this line of code to return a collection of all the processes on the computer whose Name property has the value outlook.exe:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'outlook.exe'")

Note. Good question: could you run this script against a remote computer? Well, no, not really. You can easily determine whether or not Outlook is running on the remote machine, but you’ll run into problems when you try to start Outlook on that remote computer. Why? Because, for security reasons, processes started remotely always run in an invisible window; you’ll be able to start Outlook but you won’t be able see Outlook onscreen. Are there ways to work around this issue? Well, sort of; take a peek at this Hey, Scripting Guy! column for one suggestion.

When we call the ExecQuery method we get back a collection of all the processes running on the computer, or at least those processes that have the name Outlook.exe. To determine whether or not Outlook is running all we need to do is examine the value of the Count property, which tells us the number of items in the collection. If the Count is equal to anything other than 0 that means Outlook is already running and our job is finished.

But what if the Count is 0? In that case, Outlook isn’t, running; consequently, we need to use this block of code to start the application:

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")
objNamespace.Logon "Default Outlook Profile",, False, True
Set objFolder = objNamespace.GetDefaultFolder(olFolderInbox)
objFolder.Display

This bit of code begins by creating an instance of the Outlook.Application object, then binds to the MAPI namespace, something you have to do even though this is the only namespace you can bind to. From there we call the Logon method, passing the following four parameters in order to ensure that we log on to the correct Outlook profile:

Parameter | Description
Profile | Name of the Outlook profile we want to connect to. This sample script uses “Default Outlook Profile”.
Password | Optional password to be used when connecting. This parameter exists primarily for backwards compatibility purposes and will almost always be left blank.
ShowDialog | Specifies whether the profile selection dialog box should be shown. We’ve already indicated that we want to use Default Outlook Profile, so there’s no reason to show this dialog box. Thus we set the value to False.
NewSession | Specifies whether a new Outlook session should be created. Because Outlook isn’t running we obviously need to create a new session. Thus we set this parameter to True.

The rest is easy: we use the GetDefaultFolder method to connect to the Inbox (using – at last! – the constant olFolderInbox) and then we call the Display method to make the Inbox, and the rest of Outlook, visible on screen. That’s all we have to do.

Incidentally, even though she was trying to sell him “beauty products” the Scripting Guy who writes this column actually felt sorry for the woman with the French accent; she couldn’t have picked a person less likely to buy – let alone use – Dead Sea salt. (At least not on his hands; maybe on his French fries.) “OK, so perhaps you aren’t concerned with smooth hands,” she said. “But let me ask you another question: what facial care products do you currently use?”

“Water.”

“No, no, I mean besides water.”

“Uh, soap?”

“Excuse me; I think someone over there has a question. Thank you for your time.”


How Can I Tell If Any of My Contacts Have a Birthday This Month?

Hey, Scripting Guy! How can I tell if any of my contacts have a birthday this month?

-- VG

Hey, VG. You sly fox, you: how did you know that the Scripting Guy who writes this column is celebrating his birthday?!? And no, you don’t need to send him a present; after all, he writes this column because he loves his work, not because he expects people to shower him with gifts on his birthday. Heaven forbid.

But, if you insist, send your gifts here:

The Scripting Guy Who Writes That Column
Microsoft Corporation
Building 42/4039
One Microsoft Way
Redmond, WA 98052

And yes, cash will be fine. Thanks!

Editor’s Note: Just kidding everyone. It’s not even his birthday (read on). He tries to get free desserts from restaurants like this too.

Of course, now we feel bad that we didn’t get you anything for your birthday, VG. Tell you what; how about a script that can tell you if any of your contacts have a birthday this month? Sound good? Hang on a second; we have to zip over to Scripts ‘R Us. We’ll be right back.

Whew; we got the last one they had in stock. Hope this fits, VG:

On Error Resume Next

Const olFolderContacts = 10

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set colContacts = objNamespace.GetDefaultFolder(olFolderContacts).Items

For Each objContact In colContacts
    If Month(objContact.Birthday) = Month(Date) Then
        Wscript.Echo objContact.FullName, objContact.Birthday
    End If
Next

Before we explain how this script works we should note that the assumption here is that all your contacts are in the same folder. If that’s not the case, that is, if you have subfolders in your main contacts folder, well, then this script will be far from foolproof: it will return information about the contacts in the main folder, but not for those contacts in any subfolders. If you have subfolders then you’ll need to write a recursive function that can access the information found in those subfolders. And how do you do that? Beats the heck out of us. But you can find an example of a recursive function that works with Microsoft Outlook in the Hey, Scripting Guy! archive.

As for the script itself, we start things off with the On Error Resume Next statement. That’s something we typically don’t use in our scripts, but we occasionally encountered an error when dealing with contact birthdays. We’re not totally sure why we got the error, but, seeing as how On Error Resume Next took care of things, we decided to just accept things as they were and not worry too much about it. If we ever go back and look into this a little closer we’ll let you know.

But don’t hold your breath waiting.

After enabling error handling we define a constant named olFolderContacts; we’ll use this constant to tell the script which Outlook folder we want to work with. We then use these two lines of code to create an instance of the Outlook.Application object and to bind to the MAPI namespace:

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

We should also point out that this script assumes that Outlook is already up and running. What are you supposed to do if Outlook isn’t already up and running? Beats the heck out of us. But you can find an example of a script that determines whether or not Outlook is already running (and, if not, start it) in the Office Space archive.

Now, in theory, we could probably write a complicated filter that would automatically weed out contacts that don’t have a birthday this month. To tell you the truth, however, that seemed like more trouble than it was worth; as near as we could tell it was just as fast (and way easier) to individually check each contact’s birthday. Therefore, our next step is to use this line of code to retrieve a collection of all the contacts in the Contacts folder:

Set colContacts = objNamespace.GetDefaultFolder(olFolderContacts).Items

What are we going to do with that collection? Funny you should ask. To begin with, we’re going to set up a For Each loop to walk through the entire collection. For each contact in that collection we’ll use this line of code to determine whether the contact has a birthday in the current month:

If Month(objContact.Birthday) = Month(Date) Then

All we’re doing here is using the command Month(objContact.Birthday) to determine the numeric value of the month in which the contact was born (1 for January, 2 for February, 3 for March, etc.). We then compare that with the current month; that’s what Month(Date) tells us. If the values match then the contact has a birthday this month. In turn, we echo back the contact name and birthday:

Wscript.Echo objContact.FullName, objContact.Birthday

If the two months don’t match then we simply loop around and check the next contact in the collection.

When we’re all done we should get back a report similar to this (assuming that the script is run sometime in March):

Jonathan Haas 3/28/1977
Ken Myer 3/21/1938
Pilar Ackerman 3/14/1968

Note. Well, what do you know: Ken Myer and Scripting Guy Jean Ross were born on the exact same day in the exact same year! Who would have guessed it!?!

One thing to watch out for here. If you haven’t specified a birthday for a contact Outlook automatically assigns that person a birthday of 1/1/4501. (Interestingly, that’s the same year that we expect Scripting Guy Jean Ross to finally stop complaining about our good-natured jab about her age. [Editor’s Note: Actually, Jean thinks it wouldn’t be all bad for people to believe she was born in 1938; she’d get senior discounts, and a lot of comments on how great she looks for her age – not that she doesn’t get those comments anyway.]) What that means is that, as far as this script is concerned, anyone without a designated birthday will appear to have a birthday in January. Unless, of course, you run this modified script, which simply ignores anyone with a birthday of 1/1/4501:

On Error Resume Next

Const olFolderContacts = 10

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set colContacts = objNamespace.GetDefaultFolder(olFolderContacts).Items

For Each objContact In colContacts
    If objContact.Birthday <> #1/1/4501# Then
        If Month(objContact.Birthday) = Month(Date) Then
            Wscript.Echo objContact.FullName, objContact.Birthday
        End If
    End If
Next

OK, we need to come clean with you here: the Scripting Guy who writes this column had his birthday on December 18th, a birthday he shares with baseball immortal Ty Cobb and actor Brad Pitt. (The Scripting Guy who writes this column likes to think he combines the traits of both of these legends: he looks exactly like Ty Cobb and plays baseball exactly like Brad Pitt.) Does that mean that you shouldn’t send him a birthday present after all? Well, you know, it’s never too early to get a jump on his next birthday ….


How Can I Write a Script That Accesses All the Subfolders in My Outlook Inbox?

Hey, Scripting Guy! How can I write a script that accesses all the subfolders in my Outlook Inbox?

-- MB

Hey, MB. You know, after you’ve written a daily scripting column for nearly two years (have we mentioned the Hey, Scripting Guy 500th column celebration lately?) you learn that certain subjects are strictly off-limits. Discuss religion in a scripting column? Not a good idea. Politics? Don’t even think about it. The fact that you should never, ever put ice cream on top of cake? Forget it; making a statement like that is bound to create controversy.

Note. As long as we’re on the subject, you shouldn’t dip chocolate chip cookies in milk either, at least not if you intend to drink the milk afterwards. (Editor’s Note: In the opinion of this editor, which is really the only opinion that counts, that statement is completely inaccurate. We apologize to all of you who know how to eat cookies properly – soggy to the point where you can just barely get it to your mouth without it falling into the milk.)

If we were smart (yes, yes we know: that’s a big if), we’d add recursive functions and subroutines to the list of things that we should never talk about. It’s not that recursive functions and subroutines aren’t useful; they are. (In fact, we have no idea how to answer your question without using recursion.) No, the problem isn’t that recursion is useless, the problem is that recursion is an absolute nightmare to try to explain. And, let’s face it, when you’re a Scripting Guy you have enough nightmares as it is. (Such as editors chasing after Scripting Guys with soggy chocolate chip cookies. Or just editors.)

On the other hand, it wouldn’t be very good form to start answering a question and then never finish answering it. So, we’ll tell you what, MB: we’ll give you a script that can access all the subfolders in your Inbox, but we won’t promise that we’ll be able to explain the workings of the script in nitty-gritty detail, at least not in today’s column. Deal? Deal.

Note. And you can get a brief introduction to recursion by taking a peek at the Microsoft Windows 2000 Scripting Guide.

Let’s start out easy. (OK, relatively easy.) Here’s a script that simply returns the names of all the folders and subfolders found in your Outlook Inbox:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

Wscript.Echo objInbox.Name

GetSubfolders(objInbox)

Sub GetSubfolders(objParentFolder)
    Set colFolders = objParentFolder.Folders
    For Each objFolder in colFolders
        Set objSubfolder = objParentFolder.Folders(objFolder.Name)
        Wscript.Echo objFolder.Name
        GetSubfolders objSubfolder
    Next
End Sub

Note. We should point out that this script assumes that Microsoft Outlook is already running. If that isn’t true (or at least isn’t something you should assume), then check out the Office Space article Starting a Script Regardless of Whether or Not Microsoft Outlook is Already Running to find out how to write a script that starts Outlook.

You’re right: the script does start off simple enough, doesn’t it? We begin by defining a constant named olFolderInbox and setting the value to 6; we’ll use this constant to tell the script which Outlook folder we want to bind to. We then use these two lines of code to create an instance of the Outlook.Application object and to bind to the MAPI namespace (which happens to be the only namespace we can bind to):

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

After we’ve connected to the MAPI namespace we then call the GetDefaultFolder method and bind to the Inbox:

Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

And after that we simply echo back the name of the Inbox:

Wscript.Echo objInbox.Name

Piece of cake. (Albeit without ice cream on top of it.)

Unfortunately, though, this is where things start to get a little bit tricky. Our next step is to call the recursive subroutine GetSubfolders, passing along the object reference to the Inbox as the subroutine parameter:

GetSubfolders(objInbox)

Here’s what the GetSubfolders subroutine looks like:

Sub GetSubfolders(objParentFolder)
    Set colFolders = objParentFolder.Folders
    For Each objFolder in colFolders
        Set objSubfolder = objParentFolder.Folders(objFolder.Name)
        Wscript.Echo objFolder.Name
        GetSubfolders objSubfolder
    Next
End Sub

Before we do anything else we should point out that, even though we passed the object reference objInbox, GetSubfolders appears to receive something named objParentFolder. Don’t worry about that: objParentFolder is just a variable name we apply to anything that gets passed to the subroutine. (See, we told you this was going to start getting a little weird.) If that bothers you, don’t sweat it: instead, check out the Sesame Script column Functions, Subroutines, and How to Call Them From Other Scripts for more details.)

Inside the subroutine the first thing we do is use this line of code to create a collection of all the top-level subfolders found in the Inbox:

Set colFolders = objParentFolder.Folders

Is it important to say “top-level” subfolders instead of just subfolders? Yes, it is. Suppose our Inbox looks like this:

Which of these folders do you suppose are part of the Folders collection? That’s right: only Sample and Test; that’s because they’re the only top-level subfolders in the Inbox. What about Test 2? Well, as a sub-subfolder Test 2 is actually part of the Folders collection for the Test folder: to get at sub-subfolders we need to access the Folders collection for all the top-level folders. That’s why we have to use a recursive function: we need to look at a folder, then look at any folders within that folder, then look at any folders within those folders.

Etc.

After retrieving the Folders collection we set up a For Each loop to walk through all the items in that collection. Inside that loop we start out by creating an object reference that binds us to the first folder in the collection:

Set objSubfolder = objParentFolder.Folders(objFolder.Name)

After connecting to that folder we can echo back the folder name; we can also do this:

GetSubfolders objSubfolder

Yes, we’re calling the subroutine GetSubfolders from inside the GetSubfolders subroutine. Why? Well, if the first folder in the collection is Sample we now need to see whether Sample has any sub-subfolders. This is the part where people’s eyes begin to glaze over, but that’s OK; there’s nothing wrong with simply having faith that this works. It all sounds extremely complicated, and it is. But fortunately VBScript takes care of the hard stuff for us.

When we’re all done we’ll get back a list of every folder (top-level or not) found in the Inbox:

Inbox
Test
Test 2
Sample

Of course, you didn’t just want the folder names, you wanted to be able to access all the items (e.g., all the mail messages) found in those folders. Hey, no problem. We won’t explain this revised script in any detail; we’ll just note that we use the Items collection to retrieve all the items in a folder and then, for each item, we echo back the Subject line. Obviously you can modify this script to echo back the sender’s name, the message text, or anything else you want to echo back.

Here’s the script:

Const olFolderInbox = 6

Set objOutlook = CreateObject("Outlook.Application")
Set objNamespace = objOutlook.GetNamespace("MAPI")

Set objInbox = objNamespace.GetDefaultFolder(olFolderInbox)

Wscript.Echo objInbox.Name

Set colItems = objInbox.Items

For Each objItem in colItems
    Wscript.Echo objItem.Subject
Next

GetSubfolders(objInbox)

Sub GetSubfolders(objParentFolder)
    Set colFolders = objParentFolder.Folders
    For Each objFolder in colFolders
        Set objSubfolder = objParentFolder.Folders(objFolder.Name)
        Wscript.Echo objFolder.Name

        Set colItems = objFolder.Items

        For Each objItem in colItems
            Wscript.Echo objItem.Subject
        Next

        GetSubfolders objSubfolder
    Next
End Sub

OK. You know, that wasn’t as bad as we thought it would be. Maybe tomorrow we will tackle the question: is there a God?

Or, then again, maybe we won’t.


How Can I Apply a New Template to a PowerPoint Presentation?

Hey, Scripting Guy! My company has just created a new PowerPoint template, and has decided that we need to use this template for all our presentations. How can I apply this new template to all the presentations in my C:\Presentations folder?

-- OI

Hey, OI. You know, this morning the Scripting Guy who writes this column did what he does pretty much every weekday morning: stood around the kitchen and waited for the Starbucks iCup coffee maker to brew him a “bean-to-cup, on-demand” cup of coffee.

Note. In case you’re not familiar with the machine, iCup is short for “Interactive Cup Brewer,” apparently to distinguish it from non-interactive coffee makers. (No one likes those.) Interestingly enough, when the Scripting Guy who writes this column was in elementary school, the term iCup was the basis for one of those hilarious-at-the-time grade school jokes: “Spell iCup.” (Spell it and see for yourself.)

You know, come to think of it, that’s still pretty funny, isn’t it?

At any rate, as the Scripting Guy who writes this column waited for the iCup to interactively brew him a cup of coffee, he spent some time perusing the notices tacked up on the bulletin board. The one that caught his attention was an ad for a local dog boarding facility; what he found interesting is that the facility charges by the pound: it’s $20 a day for dogs who weigh between 30 and 49 pounds; $25 a day for dogs who weigh between 50 and 79 pounds; and so on. That’s fine, except that the Scripting Guy who writes this column was struck by a terrifying thought: what if hotels started charging their customers by the pound? Considering the fact that he and fellow Scripting Guy Jean Ross are headed for Barcelona in November, and considering the fact that he’ll need to stay in a hotel on that trip, well …. We don’t need to tell you that he immediately set down that second doughnut.

OK, granted, he picked it right back up again. But at least the notion gave him pause for thought.

Note. In case you’re wondering, back in the old days Microsoft employees who wanted a cup of coffee walked into the kitchen, set their cup under the spigot for the coffee urn, and pulled the handle; two seconds later they had a cup of coffee. Now you walk into the kitchen, set your cup on the iCup, and punch the buttons; 60 seconds later you have a cup of coffee.

Well, unless you’re like the typical Microsoft employee, who removes the cup after 55 seconds. And yes, that does cause coffee to splatter all over the counter. Unfortunately, no one seems to have picked up on that.

Inspired by the dog boarding place, the Scripting Guy who writes this column toyed with the idea of charging by the pound for today’s script, a script that can apply a new template to all the PowerPoint presentations in a folder. However, today’s script is practically weightless: as it turns out, this is a task you can accomplish with just a few lines of code. Therefore, today’s script is free. As for tomorrow’s, well, we’ll see.

Note. Never mind. The Scripting Editor just told us, “No, we won’t see. Tomorrow’s column, and all our columns, will be free.”

And yes, she’s always like that. Probably drinks too much coffee.

Editor’s Note: Maybe, but at least she doesn’t have to worry about a per-pound charge at hotels. Even after eating a doughnut. Or two.

Let’s start out by showing you a script that applies a new template to a single PowerPoint presentation. We’ll explain how that scripts works, then show you a modified version that can apply a template to all the presentations in a folder. Here’s the first script:

Set objPPT = CreateObject("PowerPoint.Application")
objPPT.Visible = True

Set objPresentation = objPPT.Presentations.Open("C:\Presentations\Test.ppt")

objPresentation.ApplyTemplate _
    ("C:\Program Files\Microsoft Office\Templates\Presentation Designs\Ocean.pot")

objPresentation.Save

objPPT.Quit

As you can see, we start out by creating an instance of the PowerPoint.Application object. We then set the Visible property to True, enabling us to see this instance of PowerPoint on screen. Admittedly, you might think that it’s silly to set the Visible property to True; after all, if everything goes according to plan PowerPoint is only going to be onscreen for a split second. (It will pop up, the template will be changed, and then we’ll save the file and close the application.) For some reason, however, PowerPoint wants to be visible when we go to change the template; if it’s not, the script will crash with the following error:

C:\Scripts\test.vbs(4, 1) Microsoft Office PowerPoint 2003: Presentations.Open :
Invalid request.  The PowerPoint Frame window does not exist.

Fortunately, setting the Visible property to True takes care of that problem.

As soon as PowerPoint is up and running we call the Open method to open the file C:\Presentations\Test.ppt:

Set objPresentation = objPPT.Presentations.Open("C:\Presentations\Test.ppt")

In case you’re dying to know, the first slide in this presentation looks like this:

Of course, we don’t want the slide to look that; we want to apply a different template to the presentation. That’s what this line of code is for:

objPresentation.ApplyTemplate _
    ("C:\Program Files\Microsoft Office\Templates\Presentation Designs\Ocean.pot")

As you can see, all we have to do is call the ApplyTemplate method, passing the complete file path to the template file as the sole method parameter. Now take a look at slide 1, after ApplyTemplate has had a chance to work its magic:

Not bad, eh?

After that we call the Save method to save the modified presentation, then call the Quit method to dismiss our instance of PowerPoint. At that point, our work is done.

Well, unless our work involves applying this same template to all the presentations in a folder. Without going into any detail, here’s a script that does just that, applying the Ocean.pot template to all the PPT files in the folder C:\Presentations:

strComputer = "."

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

Set colFileList = objWMIService.ExecQuery _

    ("ASSOCIATORS OF {Win32_Directory.Name='C:\Presentations'} Where " _

        & "ResultClass = CIM_DataFile")

Set objPPT = CreateObject("PowerPoint.Application")

objPPT.Visible = true

For Each objFile In colFileList

    If objFile.Extension = "ppt" Then

        Set objPresentation = objPPT.Presentations.Open(objFile.Name)

            objPresentation.ApplyTemplate _

                ("C:\Program Files\Microsoft Office\Templates\Presentation Designs\Ocean.pot")

        objPresentation.Save

        objPresentation.Close

    End If

Next

objPPT.Quit

And now our work is done.

Incidentally, if any of you know of a good dog boarding kennel in Barcelona, well, please drop us a line. Admittedly, it’s more traditional to stay in a hotel when attending a trade show. But take a look at the description for Mazzu's Canine & Feline Luxury Hotel in Philadelphia:

Personal suite, platform bed, comforter, toys, TV/DVD, two walks, one 40-minute jaunt to the dog park, feedings, unlimited bottled water, climate-controlled facility, daily maid service, 24-hour on-site care.

And, for just $25 more, they’ll toss in a filet mignon dinner delivered to your room. That’s way better service than what you get at a people hotel.

Or at least at the people hotels that Microsoft is willing to pay for.


How Can I Configure PowerPoint to Print Handouts Instead of Slides?

Hey, Scripting Guy! How can I configure the print options in PowerPoint so that, by default, handouts are printed rather than slides?

-- OG

Hey, OG. You know, whoever said “Lightning never strikes twice in the same place” didn’t reckon on the Scripting Guys, did they? After all, less than a year after winning the Spanish lottery the Scripting Guys have done it again: this time, we’ve won $1 million in the MS-WORD LOTTO LOTTERY!

Thank you; we appreciate that. To tell you the truth, we’re pretty excited about this, too; after all, $1 million is almost as much money as the Scripting Guy who writes this column makes in a year. To get a whole bunch of money for doing absolutely nothing? That’s – well, OK, that’s a reasonably good description of his regular job. But getting a $1 million bonus on top of that is pretty darn nice.

Now, we know what you’re thinking: you’re thinking that this might be some kind of scam or something. That hardly seems possible, however; after all, the email did come from “Microsoft Worldwide Cooperation.” (Which, interestingly enough, marks the first time the words “Microsoft” and “cooperation” have ever occurred in the same sentence.) As if that wasn’t enough, the email includes a security code – MSW/JUN/SS06 – specifically designed to “prevent scam.” Oh, and this: we claim our prize not by contacting some shady, fly-by-night operation in Africa, but by directly contacting Sir Terry Woggan of London, England. (Who, oddly enough, is misidentified in the email as the UK’s Foreign Minister. As everyone knows, Sir Terry is really a radio personality and a commentator for the Eurovision Song Contest. Which makes him way more important than the Foreign Minister.)

Anyway, now being flush with money as well as success, the Scripting Guys are feeling pretty good; in fact, we’d like to share some of our newfound fortune with all of you. By giving away some, or even all, of our $1 million windfall? Heck no; where would you get an idea like that? We have something much more valuable to share with everyone; we have a script that can change the print options in PowerPoint, configuring things so that, by default, handouts (three per page) are printed rather than slides:

Const ppPrintOutputThreeSlideHandouts = 3

Set objPPT = CreateObject("PowerPoint.Application")
objPPT.Visible = True

Set objPresentation = objPPT.Presentations.Add
Set objSlide = objPresentation.Slides.Add(1,1)

Set objOptions = objPresentation.PrintOptions
objOptions.OutputType = ppPrintOutputThreeSlideHandouts

To paraphrase the commercial, “Configuring print options in PowerPoint? Priceless.”

As you can see, we start things out by defining a constant (with the delightful name ppPrintOutputThreeSlideHandouts) and assigning it the value 3; we’ll use this constant to tell PowerPoint which print option we’d like to set as the default. That leads to an obvious question: are there other values we could assign as the default print option? Of course there are; in fact, here’s a list of constants and their associated values:

Constant | Value
ppPrintOutputBuildSlides | 7
ppPrintOutputFourSlideHandouts | 8
ppPrintOutputNineSlideHandouts | 9
ppPrintOutputNotesPages | 5
ppPrintOutputOneSlideHandouts | 10
ppPrintOutputOutline | 6
ppPrintOutputSixSlideHandouts | 4
ppPrintOutputSlides | 1
ppPrintOutputThreeSlideHandouts | 3
ppPrintOutputTwoSlideHandout | 2

After we define the constant we next create an instance of the PowerPoint.Application object and then set the Visible property to True; that gives us a running instance of PowerPoint that we can see on screen. How exactly do we create an instance of the PowerPoint.Application object and set the Visible property to True? Why, by running these two lines of code, of course:

Set objPPT = CreateObject("PowerPoint.Application")
objPPT.Visible = True

In order to change the print options we need a PowerPoint presentation; that’s because the print options are tied to a specific presentation rather than being global options tied to PowerPoint itself. Hence the next two lines of code, in which we create a new presentation (using the default template) and then add a single slide to that presentation:

Set objPresentation = objPPT.Presentations.Add
Set objSlide = objPresentation.Slides.Add(1,1)

Of course, you don’t have to create a new presentation. Alternatively, you can open an existing presentation and change the print options for that presentation. For example, here’s a script that opens the file C:\Scripts\Test.ppt and modifies the print options:

Const ppPrintOutputThreeSlideHandouts = 3

Set objPPT = CreateObject("PowerPoint.Application")
objPPT.Visible = True

Set objPresentation = objPPT.Presentations.Open("C:\Scripts\Test.ppt")

Set objOptions = objPresentation.PrintOptions
objOptions.OutputType = ppPrintOutputThreeSlideHandouts

Speaking of modifying print options, that’s what we do next; we create a reference to the PrintOptions object, then assign the constant ppPrintOutputThreeSlideHandouts to the OutputType property:

Set objOptions = objPresentation.PrintOptions
objOptions.OutputType = ppPrintOutputThreeSlideHandouts

That’s all we have to do; if we now click the Print button or programmatically call the PrintOut method we’ll end up with handouts (three per page) rather than slides. Which is exactly what we wanted to end up with.

Note. Are there other print options you can configure besides OutputType? You bet there are; see the Microsoft PowerPoint VBA Language Reference for details.

Anyway, we hope that answers your question, OG. As for the Scripting Guys, once the initial excitement over winning the MS-WORD LOTTO LOTTERY began to fade we started to have second thoughts about this whole thing. After all, Microsoft, of all people, giving money away? Hmmm ….


How Can I Run a PowerPoint Slide Show From a Script?

Hey, Scripting Guy! How can I run a PowerPoint slide show from a script?

-- IB

Hey, IB. Talk about vindication! A couple years ago, when the Scripting Guy who writes this column first started messing around with Microsoft Office, he wrote a simple little script that could run a PowerPoint slide show. The other Scripting Guys laughed at him. “That was a waste of time,” they said. “What use are you ever going to have for a script that runs a PowerPoint slide show?” Well, other Scripting Guys: who’s laughing now?

Well, OK, technically they’re all still laughing at him. And, come to think of it, they were all laughing at him before he wrote a script that could run a PowerPoint slide show. Hmmm, wonder if that means anything ….

While we ponder this situation feel free to read a magazine or go get a sandwich. Or, even better, take a gander at a script that can run a PowerPoint slide show:

On Error Resume Next

Const ppAdvanceOnTime = 2
Const ppShowTypeKiosk = 3
Const ppSlideShowDone = 5

Set objPPT = CreateObject("PowerPoint.Application")
objPPT.Visible = True

Set objPresentation = objPPT.Presentations.Open("C:\Scripts\Test.ppt")

objPresentation.Slides.Range.SlideShowTransition.AdvanceOnTime = TRUE
objPresentation.SlideShowSettings.AdvanceMode = ppAdvanceOnTime
objPresentation.SlideShowSettings.ShowType = ppShowTypeKiosk

objPresentation.SlideShowSettings.StartingSlide = 1
objPresentation.SlideShowSettings.EndingSlide = objPresentation.Slides.Count

Set objSlideShow = objPresentation.SlideShowSettings.Run.View

Do Until objSlideShow.State = ppSlideShowDone
    If Err <> 0 Then
        Exit Do
    End If
Loop
objPresentation.Saved = True
objPresentation.Close
objPPT.Quit

This is a crazy-looking script, but there’s really not that much to it; for the most part all we’re doing is setting various property values. We’ll briefly run through the properties that we used in configuring the slide show, but these are by no means all the properties (nor have we used all the available options for these properties). For more information, see the Microsoft PowerPoint VBA Language Reference on MSDN.

We start out simply enough, defining three constants:

· ppAdvanceOnTime. Setting this value to 2 causes the slide show to run according to preset timings. We could have configured the slide show so that it would advance only when the user clicked the mouse button, but we assumed that, if you want to automate the process of opening up the slide show, you probably want to automate the actual presentation of the slides as well.

· ppShowTypeKiosk. Setting this value to 3 causes the slide show to display in “kiosk” mode (in which it fills the entire screen, including the desktop toolbar).

· ppSlideShowDone. Setting this value to 5 causes the slide show to continue until every slide has been shown.

After defining the three constants we create an instance of the PowerPoint.Application object and then set the Visible property to True; we then use the Open method to open the file C:\Scripts\Test.ppt. With the presentation open we set several properties of the slide show, including AdvanceOnTime and AdvanceMode (which, taken together, cause the presentation to run automatically), and ShowType, which displays the presentation in kiosk mode. If you choose to type these values into your script (as opposed to copying and pasting) notice that you have to use two different objects to configure the three values.

Note. So what if you don’t want to use preconfigured slide timings? No problem; just add this property:

objPresentation.Slides.Range.SlideShowTransition.AdvanceTime = 2

The AdvanceTime property specifies the number of seconds each slide should appear on screen. Setting this value to 2 causes each slide to stay onscreen for two seconds before the presentation automatically moves on to the next slide. Incidentally, you can use this value to override any preconfigured slide timings.

That leaves us with just two properties – StartingSlide and EndingSlide – that we need to configure. (OK, these are actually optional, but we tossed them in because they’re good properties to know about.) We want to start the presentation with slide 1 and end the presentation with the last slide in the show; therefore we set the value of StartingSlide to 1 and the value of EndingSlide to the total number of slides in the show (objPresentation.Slides.Count):

objPresentation.SlideShowSettings.StartingSlide = 1
objPresentation.SlideShowSettings.EndingSlide = objPresentation.Slides.Count

What if we wanted to show just a portion of the show, say, slides 18-31? All you had to do was ask:

objPresentation.SlideShowSettings.RangeType = 2
objPresentation.SlideShowSettings.StartingSlide = 18
objPresentation.SlideShowSettings.EndingSlide = 31

As you can see, we simply specified the appropriate starting and ending slides. In addition to that, we also had to set the RangeType to 2; that tells PowerPoint that we’re using a specified range of slides as opposed to all the slides in the presentation.

After all the properties are configured we’re ready to run the slide show. To do that we first create an instance of the SlideShowSettings.Run.View object. That can be done with a single line of code:

Set objSlideShow = objPresentation.SlideShowSettings.Run.View

As soon as we have the View object we can set up a Do Loop that runs until the slide show has completed:

Do Until objSlideShow.State = ppSlideShowDone
    If Err <> 0 Then
        Exit Do
    End If
Loop

Don’t leave this block of code out; if you do, the presentation will momentarily flash onscreen, then just as quickly disappear. This little block of code keeps PowerPoint running – and visible – until the entire presentation has been viewed.

Oh, yeah: there is an If Then statement in the middle of the loop, isn’t there? Why? Well, suppose someone is watching your slide show and they press the Escape key on the keyboard. Assuming we left out the If Then statement that would do two things. First, it will create an error: the script is supposed to keep displaying slides until all the slides have been shown, only now the presentation has disappeared. That’s a problem, and an error occurs. Second, that would also drop the user into PowerPoint itself, a move that defeats the whole idea of an auto-run slide show.

Therefore, we use the If Then statement to constantly monitor the value of the VBScript Err object. If an error occurs (that is, if the value of Err does not equal 0) we immediately exit the Do loop. (There’s no point in staying in the loop if there aren’t any more slides to display.) In turn exiting out of the loop causes us to execute the last three lines in the script, lines that we will explain in just a second:

objPresentation.Saved = True
objPresentation.Close
objPPT.Quit

OK, so what does happen when an error occurs, or when the slide show finishes and we exit the Do loop at the end? Well, as we noted, we run those last three lines of code. In line 1 we set the Saved property to True; that simply assures PowerPoint that the presentation has been saved (it hasn’t, but PowerPoint will take our word for it), and keeps the application from popping up a dialog box asking if we want to save changes to the file. In the last two lines we close the file Test.ppt and then terminate PowerPoint. Sorry, show’s over, folks!

In other words, if a user tries to prematurely end the slide show (or when the slide show finishes as scheduled) the file gets closed, PowerPoint gets terminated, and the script ends.

Hope that helps, IB. And we hope you’ll agree that the Scripting Guy who writes this column is anything but crazy.

Well, OK, sure, he won’t get one of those super-saver cards at the grocery store, even though it would definitely save him money. But that’s about as crazy as he gets.

Other than the fact that, when he eats a sandwich he has to eat it from left-to-right; he can’t go from right-to-left or – heaven forbid! – take a bite right out of the middle. (It goes without saying that the peanut butter must be on the bottom slice and the jelly on the top slice; no upside-down sandwiches for him!) And he always has to sleep with his head facing the door. And if he’s watching TV and his team starts to lose he’ll get up and sit in a different chair. And ….


How Can I Add a Blank Line Between the Existing Paragraphs in a Word Document?

Hey, Scripting Guy! How can I add a line between the existing paragraphs in a Word document?

-- GB

Hey, GB. You’ll have to forgive us if we seem a little distracted this morning; shocking as this might seem, the Scripting Guy who writes this column may have made a monumental mistake. In yesterday’s column he joked about an email he received that told him that he had won the “Spainsh” lottery. That sounded like a classic Internet scam, and he treated it as such.

But maybe he shouldn’t have done that. After all, the same day that column was written the same Scripting Guy who made fun of the Spanish lottery received a letter in the mail inviting him to join “upscale people such as yourself” in a private jet club, a club where membership entitles you to a certain number of hours per year flitting about the world in a private jet. “Frequent flyers such as you can surely appreciate the benefits of a private jet over commercial air travel,” noted the letter. As if they even had to ask a question like that!

Note. In case you’re wondering, the most airplane trips the Scripting Guy who writes this column has ever taken in a single year is three: one to Pasadena to watch the Huskies beat Purdue in the Rose Bowl; one to Hawaii for a family vacation; and one to Orlando to speak at a conference. This year he hasn’t flown in a plane at all. But still, he probably does qualify as a frequent flyer, at least compared to, say, Charlemagne or King Henry the VIII (or the Scripting Editor).

At first the Scripting Guy who writes this column figured this was just junk mail; high-class junk mail, perhaps, but junk mail nonetheless. But then he got to thinking: how did this private jet company get his name and address? Why did they keep referring to his “lavish lifestyle” and appreciation for the finer things in life? How did they know that his time is far more valuable than mere money?

As you might expect, he then began to worry. After all, what if the Spanish lottery routinely forwards the names of all their winners to private jet clubs and butler placement agencies and the like? What if the Scripting Guy who writes this column really did win one million euros in the Spanish lottery only to throw it all away by making fun of the lottery folks?

That should be a fun thing to go home and try to explain to the family.

But hey: easy come, easy go. Besides, at his current salary, and depending on the exchange rate, it should only take him 300 years or so to make a million euros anyway.

Well, assuming, of course, that his next few performance reviews go a bit better than his last one did.

The bottom line is this: having thrown away a million euros, it looks as though the Scripting Guy who writes this column will have to continue writing this column for a little longer. (But probably just for a few months; as he noted yesterday, he wins the Spanish lottery on a fairly regular basis.) That’s bad news for the Scripting Guy who writes this column, but it’s good news for you, GB; after all, you get a script that can add a line between existing paragraphs in a Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc =
objWord.Documents.Open("C:\Scripts\Test.doc")

Set colParagraphs = objDoc.Paragraphs
colParagraphs.LineUnitAfter = 1

To begin with, we should note that there are probably a couple different ways we could have approached this problem. For example, we could have done a search-and-replace and simply replaced all paragraph returns with two paragraph returns. (For more information on writing search-and-replace scripts, see this previous Hey, Scripting Guy! column.) In the end we decided to modify the paragraph format settings so that an additional line would be automatically tacked on to the end of each paragraph in the document. That’s an approach we haven’t used before and, as you just saw, it doesn’t require us to write very much code. (Needless to say, we don’t get paid by the word.)

We should also point out, GB, that we’re assuming you have a Word document that looks something like this:

As you can see, the paragraphs are all jammed together. We want to space things out a bit by adding a blank line between each of these paragraphs.

To do that we start out by creating an instance of the Word.Application object and then setting the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. We then use this line of code to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Believe it or not, that’s the hard part. As soon as the document is open we can use this line of code to retrieve a collection of all the paragraphs in Test.doc:

Set colParagraphs = objDoc.Paragraphs

From there we simply set the value of the LineUnitAfter property to 1, which causes 1 blank line to appear after each paragraph in the document:

colParagraphs.LineUnitAfter = 1

Just that easy, just that quick.

But, you interject, what if we wanted 2 blank lines to appear between each paragraph in the document? That’s no problem; one way to do that would be to set the LineUnitAfter property to 2:

colParagraphs.LineUnitAfter = 2

Etc., etc. Alternatively, we could also assign a value to the LineUnitBefore property, which would cause a blank line to automatically appear before each paragraph. Whatever floats your boat, right?

Well, at least for those of you who have a boat.

In case you’re wondering, here’s what Test.doc looks like after we run our script:

Meanwhile, we’re still debating whether we should go ahead and join the private jet club anyway, Spanish lottery or no Spanish lottery. The letter we received didn’t mention the actual price of a membership, which is usually a bad sign. On the other hand, though, the offer did include a coupon for $250 off the in-flight catering for our first flight. As you can imagine, it’s awfully hard to pass up an offer like that. (To tell you the truth, we’re not convinced we could even eat $250 worth of food during a single flight. But we’re willing to give it a try.)


How Can I Add a Hyperlink to a Word Document?

Hey, Scripting Guy! How can I add a hyperlink to a Word document?

-- BA

Hey, BA. Interesting question; in thinking about common tasks for scripting Microsoft Office applications we never even considered adding a hyperlink to a document. (Needless to say, that’s why we rely on people like you to let us know about the tasks people really want to script.) As it turns out, it’s pretty easy to add a hyperlink to a Microsoft Word document; in fact, we were able to do it with just a few lines of code:

Set objWord = CreateObject("Word.Application")
objWord.Visible = TRUE
Set objDoc = objWord.Documents.Add()
Set objRange = objDoc.Range()
Set objLink = objDoc.Hyperlinks.Add _
    (objRange, " http://www.microsoft.com/technet/scriptcenter ", , , "Script Center")

The first three lines in this script are just boilerplate code. We start out by creating an instance of the Word.Application object. We then set the Visible property to TRUE because, as a demonstration script, we want to make sure you can see what’s happening to the document. Next we create a new document, which gives us a blank piece of paper to work on. In line 4, we create an instance of the Range object; we’ll use this to tell Word that we want to create the hyperlink at the current cursor location. If you’ve done any scripting in Word, this should all look very familiar.

That brings us to this line of code, which actually creates the hyperlink:

Set objLink = objDoc.Hyperlinks.Add _
    (objRange, " http://www.microsoft.com/technet/scriptcenter ", , , "Script Center")

What we do here is call the Add method to add a new link to the Hyperlinks collection. When we do this, we need to pass the Add method a few parameters:

Parameter | Description
objRange | Hyperlink “anchor,” which indicates the spot in the document where we want to create the hyperlink. In this sample script, we’re creating the hyperlink right at the start of the document.
http://www.microsoft.com/technet/scriptcenter | URL we want to link to.
blank | Enables us to link to a bookmark or other specified section within the HTML document. We aren’t linking to a bookmark in this script, so we leave this parameter blank.
Blank | Optional tooltip that appears when you hold the mouse over the hyperlink. We aren’t using a tooltip in this sample script, so we leave this parameter blank.
Script Center | Text for the hyperlink (that is, the text you see in the document, which may or may not be the destination URL).
Blank | Enables us to specify the window or frame in which the HTML document opens. We don’t specify a target in this sample script, but if we wanted to open the Web page in a new window we could specify “_blank” as the target.

When we finish running the script we should have a new hyperlink that looks – and functions – just like this one:

Script Center

That should do the trick. Give it a try and see what happens.


How Can I Add a New Item to the Microsoft Word AutoCorrect List?

Hey, Scripting Guy! How can I add a new item to Word’s AutoCorrect list?

-- OK

Hey, OK. To begin with, we should point out that we have no way of knowing whether your initials really are O and K; after all, they could just as easily be TR, and you could actually be Tony Romo, quarterback of the Dallas Cowboys, writing to us under an assumed name. (Which, if you value your reputation, is the only kind of name you should use when writing to the Scripting Guys.) If that’s the case, then we want you to know that we have nothing against you personally. (Granted, we’ll never be convinced that you deserved to make the Pro Bowl this year, but that’s not really your fault.) Having said that, however, it’s also true that we do have all sorts of things against the Dallas Cowboys. Because of that we’re actually quite happy that your fumbled snap helped ensure that the Cowboys lost to the Seattle Seahawks in the opening round of the NFL playoffs. Thanks, man!

Note. Yes, we know: every religion ever created tells us that we should love thy neighbor. On the other hand, most religions are pretty old and were established long before there was such a thing as the Dallas Cowboys. If someone started a new religion today things might be different.

Besides, it’s not like we’re the only ones who don’t like the Cowboys; heck, even many of the Cowboy players don’t seem to like the Cowboys. The Scripting Guys are just trying to go along with the crowd.

Anyway, if it turns out that you are Tony Romo, well, yes, we are reveling in your misfortune, and we do feel a little bad about that. (Not much, mind you, but a little.) But while Saturday’s defeat means that you won’t get a Super Bowl ring this year, here’s something equally valuable: a script that adds a new item to Word’s AutoCorrect list:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set colEntries = objWord.AutoCorrect.Entries
colEntries.Add "Fabirkam", "Fabrikam"

And no, this script isn’t four lines long because we refused to write more than four lines of code for the Dallas Cowboys. On the contrary: we have just four lines of code simply because all we need is four lines of code. That’s all it takes to add an item to the AutoCorrect list.

Note. So does that mean that if we did need more than four lines of code we would have gone ahead and done that extra work? Uh, sure, sure we would have. Hey, you know how the Scripting Guys feel about doing extra work.

As you can see, the script starts out by creating an instance of the Word.Application object, then sets the Visible property to True; that gives us a running instance of Word that we can see on screen. Note that you don’t have to make Word visible in order to add an item to the AutoCorrect list; if you want, go ahead and leave out the line of code that sets the Visible property to True. If you elect to do that just be sure to make this the last line of the script:

objWord.Quit

And yes, that is important: if you omit the Quit method Word will not terminate when the script ends, but will, instead, continue to run on forever and ever.

Sort of like the Dallas Cowboys’ playoff losing streak, which has now reached 10 years and counting.

Not that we’re counting, mind you.

As soon as Word is up and running we use this line of code to create an object reference to the AutoCorrect.Entries collection:

Set colEntries = objWord.AutoCorrect.Entries

From there all we need to do is call the Add method to add a new item to the collection:

colEntries.Add "Fabirkam", "Fabrikam"

As you can see, the Add method takes two parameters: Fabirkam (the common misspelling that we want to correct) and Fabrikam (the corrected spelling). From now on anytime we type Fabirkam Word will autocorrect the spelling the Fabrikam.

What if we change our mind and decide that we don’t want Fabirkam in the AutoCorrect list after all? No problem; here’s a simple little script that binds to the AutoCorrect.Entries collection, looks for an entry with the NameFabirkam, then uses the Delete method to remove that item from the collection:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set colEntries = objWord.AutoCorrect.Entries

For Each objEntry in colEntries
    If objEntry.Name = "Fabirkam" Then
        objEntry.Delete
    End If
Next

And yes, we have no doubt the Dallas Cowboys wish it was as easy to undo the actions of a football play as it is to undo the actions of the script. But the Scripting Guys would never say something like that; after all, the Cowboys probably feel bad enough without us rubbing it in.

Note. Besides, now the Seahawks have to travel to Chicago to take on the Bears, which means that the Cowboys shouldn’t worry too much: we’re likely to get our comeuppance soon enough. (But, hey, you never know, right?) Go Seahawks!


How Can I Add a Page X of Y Footer to a Microsoft Word Document?

Hey, Scripting Guy! I saw your article on adding page numbers to the footer of a Word document. However, I need my page numbers to be in the format "Page X of Y," where X is the current page number and Y is the total number of pages. How can I do that?

-- JS

Hey, JS. You know, several years ago the grocery store by the Scripting House included one of those claw-type arcade games; you know, the kind where you maneuver this big claw around and try to grab a prize. Back then the Scripting Son was infatuated with the idea of winning one of the big prizes. For awhile the Scripting Dad indulged his son, but it wasn’t long before Dad got tired of wasting a dollar each time they went to the store; after all, despite his best efforts the Scripting Son never even came close to winning any prize, let alone a really cool prize.

“This game is impossible,” the Scripting Dad told the Scripting Son. “I’m not wasting any more money on it. You can’t win; there’s no way.”

“No, Dad, there’s a trick to it, and I figured out what the trick is,” replied the Scripting Son. “Please, let me have just one dollar? I know I can do it.”

But the Scripting Dad remained resolute. After all, if the Scripting Dad says something is impossible then it must be impossible. End of discussion.

We mention this simply because the Scripting Dad also believed it was impossible to programmatically add a Page X of Y footer to a Word document. This question has come up before, and the Scripting Guy who writes this column couldn’t figure out how to do. And if the Scripting Guy who writes this column can’t figure out how to do something then it must be impossible. End of discussion.

Except, as it turns out, there’s a trick to it. And once you figure out the trick, well, guess what? You really can add a Page X of Y footer to a Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()

Set objRange = objDoc.Sections(1).Footers(1).Range

Set objTemplate = objDoc.AttachedTemplate
objTemplate.AutoTextEntries("Page X of Y").Insert objRange

So what is this trick? We’ll explain that in just a second. Before we do that, however, let’s talk about the rest of the script. We start out by creating an instance of the Word.Application object and setting the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. After using the Add method to add a new, blank document to our instance of Word, we then use this line of code to create a Range object corresponding to the first footer in the first section of our document:

Set objRange = objDoc.Sections(1).Footers(1).Range

And you’re right: because we don’t have any other sections in the document (let alone any other footers) that means this Page X of Y footer will apply to the entire document.

Here comes the tricky part. Our next step is to create an instance of the Template object; we do that by binding to the document’s AttachedTemplate property:

Set objTemplate = objDoc.AttachedTemplate

And then we insert one of the template’s AutoTextEntries; to be a more specific, we insert the Page X of Y entry:

objTemplate.AutoTextEntries("Page X of Y").Insert objRange

Needless to say, this is way easier than we thought. We bind to the AutoTextEntries collection and call the Insert method, specifying Page X of Y as the autotext to be entered. We also have to specify where we want the text entered. Where do we want this text entered? You got it: objRange, the object reference specifying the document footer.

And that, believe it or not, is all we need to do.

OK, that’s not true; with this kind of footer you probably want the text center-aligned. If that’s the case, then just tack this line of code onto the end of the script:

objRange.ParagraphFormat.Alignment = 1

And that is all we need to do.

Incidentally, you might be interested in knowing how the Scripting Guy who writes this column finally solved this riddle. He knew it was possible to do a Page X of Y footer using the Word GUI. Therefore, he started a macro recording and went ahead and used the GUI to add the desired footer. When he was done he edited the macro in order to view the macro code. After wading through the VBA code he found this:

NormalTemplate.AutoTextEntries("Page X of Y").Insert Where:=Selection. _
        Range, RichText:=True

That was the hint he was looking for. That’s something to remember when writing scripts that interact with Microsoft Word: any time you get stuck try recording a macro that carries out the desired action. The VBA code will often be a little obtuse, but it will usually point you in the right direction.

· So does the Scripting Dad feel bad that he didn’t give the Scripting Son a dollar and let him try to win a really cool prize? No, not really. For one thing, the Scripting Son has found plenty of other ways to pry money from the Scripting Dad. For another, the prizes you could win back then really weren’t all that cool. For example, did the claw game offer you the chance to win a Dr. Scripto bobblehead doll or a Scripting Guys gift pack? What, are you kidding? No, there are only two ways you can win a really cool prize like those:

If you’re attending TechEd 2007 swing by the CMP Media Booth (booth number 1301), find the Scripting Guys, get a copy of Dr. Scripto’s Fun Book, and enter the drawing for a Dr. Scripto bobblehead doll. If you aren’t sure who the Scripting Guys are, just look for a cheapskate in a baseball hat. That’s the Scripting Dad.

Even if you’re not attending TechEd 2007 you have a shot at winning a cool prize by entering the TechEd Challenge. Don’t delay, though; that contest ends June 30th.

So is there a trick to winning the TechEd Challenge? You bet there is. However, the Scripting Editor always gets a little nervous when we say things like, “Send us $100 and we’ll rig the contest and make sure you win.” Therefore, we won’t say that.

But it is something to think about, isn’t it? (Editor’s Note: No.)


How Can I Add Centered Page Numbers to the Footer of a Word Document?

Hey, Scripting Guy! I need to put the page number (centered) in the footer of a Word document. Do you have any suggestions on how I can do that?

-- AW

Hey, AW. To tell you the truth, the Scripting Guy who writes this column no longer makes suggestions. That’s because of an incident that occurred a few years ago, when someone he works with sent out a blanket email about their latest project, adding, “Please let me know if you have any suggestions on how we can make this even better.” Being a bit naïve in the ways of the world, our hero actually had a couple suggestions, so he helpfully forwarded those along.

You got it: big mistake. After being chewed out by the person who sent the email (“A lot of people put a lot of time and effort into this project and all you can do is find things that are wrong with it!”) and that person’s manager and his manager (“You should know better than to give her any suggestions”), this particular Scripting Guy vowed never to make another suggestion as long as he lived, a vow he has stuck to for nearly three years now.

Also a vow which, interestingly enough, hasn’t seemed to upset anyone else in the least. No one seems to miss his help at all.

So, no, AW, we don’t have any suggestions for you; we don’t do suggestions. Instead, the best we can do is offer you a script that puts the page number (centered) in the footer of a Word document:

Const
wdAlignPageNumberCenter = 1

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

objDoc.Sections(1).Footers(1).PageNumbers.Add(wdAlignPageNumberCenter)

Yes, it is a simple little script, isn’t it? We start out by defining a constant named wdAlignPageNumberCenter and setting the value to 1; this tells the script that we want the page number centered within the footer. What if we change our mind and decide that we don’t want the page number centered? That’s OK; we can use any of the following constants and their values:

Constant | Value
wdAlignPageNumberCenter | 1
wdAlignPageNumberInside | 3
wdAlignPageNumberLeft | 0
wdAlignPageNumberOutside | 4
wdAlignPageNumberRight | 2

After defining our constant we use these three lines of code, which create a visible instance of the Word.Application object and then give us a new, blank document to work with:

Set
objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

Once we’ve done that we can add the page numbers with a single line of code:

objDoc.Sections(1).Footers(1).PageNumbers.Add(wdAlignPageNumberCenter)

What we’re doing here is creating a PageNumbers object tied to the first footer in the first, and only, section of our document; that’s what objDoc.Sections(1).Footers(1) is all about. At the same time, we’re calling the Add method to add page numbers to the footer, passing along the constant wdAlignPageNumberCenter to indicate that we want the page numbers centered. And that, as they say, is that.

Of course, there are plenty of other options we can apply to page numbers, far more than we can cover in this one column. (For more information, check out the Microsoft Word VBA Language Reference on MSDN.) Just to whet your appetite, however, we’ll give you one example. The following script defines a constant named wdPageNumberStyleLowercaseRoman and then assigns that constant to the NumberStyle property:

Const
wdAlignPageNumberCenter = 1
Const wdPageNumberStyleLowercaseRoman = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

objDoc.Sections(1).Footers(1).PageNumbers.Add(wdAlignPageNumberCenter)
objDoc.Sections(1).Footers(1).PageNumbers.NumberStyle = wdPageNumberStyleLowercaseRoman

What this will do is – sorry, what was that? Wow; you’re right: that displays page numbers as lowercase Roman numerals. Very perceptive of you.

Hey, wait a second: you guys aren’t cheating and reading ahead are you? That’s what we thought.

Here, by the way, are the other allowed page number styles. Bear in mind that, depending on your language settings, some of these might not make much sense (or make much difference in your document):

Constant | Value
wdPageNumberStyleArabic | 0
wdPageNumberStyleArabicFullWidth | 14
wdPageNumberStyleArabicLetter1 | 46
wdPageNumberStyleArabicLetter2 | 48
wdPageNumberStyleHanjaRead | 41
wdPageNumberStyleHanjaReadDigit | 42
wdPageNumberStyleHebrewLetter1 | 45
wdPageNumberStyleHebrewLetter2 | 47
wdPageNumberStyleHindiArabic | 51
wdPageNumberStyleHindiCardinalText | 52
wdPageNumberStyleHindiLetter1 | 49
wdPageNumberStyleHindiLetter2 | 50
wdPageNumberStyleKanji | 10
wdPageNumberStyleKanjiDigit | 11
wdPageNumberStyleKanjiTraditional | 16
wdPageNumberStyleLowercaseLetter | 4
wdPageNumberStyleLowercaseRoman | 2
wdPageNumberStyleNumberInCircle | 18
wdPageNumberStyleNumberInDash | 57
wdPageNumberStyleSimpChinNum1 | 37
wdPageNumberStyleSimpChinNum2 | 38
wdPageNumberStyleThaiArabic | 54
wdPageNumberStyleThaiCardinalText | 55
wdPageNumberStyleThaiLetter | 53
wdPageNumberStyleTradChinNum1 | 33
wdPageNumberStyleTradChinNum2 | 34
wdPageNumberStyleUppercaseLetter | 3
wdPageNumberStyleUppercaseRoman | 1
wdPageNumberStyleVietCardinalText | 56

And, yes, now that you mention it sometimes it is hard to go through the day without making any suggestions. After all, this Scripting Guy has been making suggestions for 40 years, and never once has anyone taken him up on one of those suggestions. That’s like Cal Ripken’s record for consecutives games played in Major League baseball: it’s inevitable, but you still hate to see a streak like that come to an end. (Although, like boxer Rocky Marciano, we suppose there’s also merit in retiring with a perfect record.)


How Can I Add Multiple Tables to a Word Document?

Hey, Scripting Guy! How can I add multiple tables to a Word document?

-- KH

Hey, KH. You know, if the Scripting Guys have one failing (hey, we said if) it’s this: we’re simple guys with simple tastes. Create a Microsoft Word document with multiple tables? No, sir; that’s not for us, not at all. One table per document is plenty, thank you very much.

Of course, we do realize that there are a lot of wild and crazy people out there, daredevils willing to try just about anything, up to and including adding multiple tables to a single Word document. With those people in mind, we tossed together a sample script that adds two tables to a Word document. Use it at your own risk, KH:

Const END_OF_STORY = 6

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.TypeText "Table 1"
objSelection.TypeParagraph()

Set objRange = objSelection.Range
objDoc.Tables.Add objRange, 1, 2
Set objTable = objDoc.Tables(1)

objTable.Cell(1, 1).Range.Text = "This is cell 1."
objTable.Cell(1, 2).Range.Text = "This is cell 2."
objSelection.EndKey END_OF_STORY
objSelection.TypeParagraph()

objSelection.TypeText "Table 2"
objSelection.TypeParagraph()

Set objRange = objSelection.Range
objDoc.Tables.Add objRange, 1, 2
Set objTable = objDoc.Tables(2)

objTable.Cell(1, 1).Range.Text = "This is cell 1."
objTable.Cell(1, 2).Range.Text = "This is cell 2."

objSelection.EndKey END_OF_STORY
objSelection.TypeParagraph()

In the relatively small amount of time we spent on this (too much excitement angries up the blood, you know) we decided that the key to adding multiple tables to a single document was this: after adding the first table you need to move the cursor to the end of the document, type a paragraph return, and then add the second table. (We weren’t brave enough to try adding a third table, but it should work the same way.) That’s the approach we took; let’s see how it actually works.

In the script itself we start off by creating a constant named END_OF_STORY; as the name implies, we’ll use this constant to move the cursor to the end of the document. After defining the constant we create an instance of the Word.Application object and set the Visible property to True; that gives us a running instance of Word that we can see onscreen. We use the Add method to add a new, blank document, and then position the cursor at the beginning of that document simply by creating an instance of the Word Selection object:

Set objSelection = objWord.Selection

Good guess: it’s now that the fun begins. Our first task is to create a table header; to do that we simply type the phrase Table 1 followed by a paragraph return:

objSelection.TypeText "Table 1"
objSelection.TypeParagraph()

At this point we’re ready to insert our first table. We aren’t going to explain all of the following code in detail; if you’ve never added a table to a Word document you might want to take a look at the Office Space article Creating Tables in Microsoft Word. For now we’ll simply note that we begin by creating an instance of the Range object (with the range consisting of the current cursor location). After that we add a table with 1 row and 2 columns, and then create an object reference (objTable) to that table (which, because it’s the first table in the document, is given the index number 1). That all sounds complicated, but it requires only three little lines of code:

Set objRange = objSelection.Range
objDoc.Tables.Add objRange, 1, 2
Set objTable = objDoc.Tables(1)

And once we have a table we can then use these lines of code to type a little bit of text in each of the two table cells:

objTable.Cell(1, 1).Range.Text = "This is cell 1."
objTable.Cell(1, 2).Range.Text = "This is cell 2."

All that effort gives us a Word document that looks like this:

We agree: that should be enough for anyone. If it’s not, though, then here’s the secret to adding a second table to the document:

objSelection.EndKey END_OF_STORY
objSelection.TypeParagraph()

Granted it doesn’t look very impressive, but it works. All we’re doing here is calling the EndKey method to move the cursor to a new spot in the document. Which spot? The very end of the document, as indicated by the constant END_OF_STORY. We then use the TypeParagraph() method to simulate pressing the ENTER key on the keyboard. That simply positions the cursor on a new, blank line, enabling us to add a new table header and, following that, a new table.

In fact, that’s what this block of code does: it adds a header for Table 2, and then inserts the second table in the document:

objSelection.TypeText "Table 2"
objSelection.TypeParagraph()

Set objRange = objSelection.Range
objDoc.Tables.Add objRange, 1, 2
Set objTable = objDoc.Tables(2)

objTable.Cell(1, 1).Range.Text = "This is cell 1."
objTable.Cell(1, 2).Range.Text = "This is cell 2."

About the only thing to take note of here is this line of code:

Set objTable = objDoc.Tables(2)

As you can see, this time around we’re creating an object reference that points to a table with the index number 2. Why? Because this is the second table in the document. If we add a third table, we have to create an object reference to a table with an index number of 3: Set objTable = objDoc.Tables(3). And if we add a fourth table – well, let’s not even go there, if you know what we mean. But you get the idea.

After we add the new table and type a little text into the table cells we once again position the cursor at the end of the document:

objSelection.EndKey END_OF_STORY
objSelection.TypeParagraph()

And what does all that give us? This:

Which we believe is exactly what you were hoping to get.

Disclaimer. In the interests of full disclosure we should note that we did not actually test this multiple-table script ourselves; instead, we had our stunt doubles test it for us. Not that we were scared or anything, but ….


How Can I Add the Last Lines from a Group of Text Files to a Word Document?

Hey, Scripting Guy! How can I take the last line in a group of text files and paste that information into a Word document?

-- JV

Hey, JV. You know, one thing we try to do in this column is focus on practical system administration tasks. We’ll deviate from that focus every now and then, but for the most part we try to address questions we feel a lot of people would be interested in. And, to be honest, when we first read this question, we didn’t believe it fell into that category.

And then we started thinking (something that we do do on occasion, following roughly the same schedule as Halley’s Comet). One thing we realized is that lots of log files include a bunch of detailed information and then, on the last line, have some sort of summarization. If you had a bunch of log files and you wanted to put together a summary report of some kind, one way to do that might be to grab the last line of each log file and then throw all those summary lines into a Word document. We don’t know if this is the kind of situation you’re facing, but we were so excited to have actually thought of something useful that we decided to go ahead and answer the question.

And here’s our answer:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

strComputer = "."

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

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

For Each objFile In colFileList
    strFilePath = objFile.Name
    Set objTextFile = objFSO.OpenTextFile(strFilePath, ForReading)
    Do Until objTextFile.AtEndOfStream
        strLine = objTextFile.ReadLine
    Loop
    strMessage = strMessage & strLine & vbCrLf
    objTextFile.Close
Next

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

Set objSelection = objWord.Selection
objSelection.TypeText strMessage

OK, so it’s a little long, but remember, we have several tasks to perform here: get all the files in a folder, open them up, grab the last line, start up Microsoft Word, and then paste all those lines into our Word document. Being able to do that in 20-some lines of code doesn’t seem too terribly bad.

The script starts out by defining a constant named ForReading and then assigning this constant the value 1; we’ll use ForReading each time we open and read a text file. Next we connect to the WMI service on the local computer. Which brings up an important question: does this script have to run on the local computer? And the answer is: not necessarily; after all, WMI can grab a collection of files off a remote computer just as easy as it can off the local computer.

Unfortunately, though, the FileSystemObject – the object we use to read the text files – doesn’t handle remote machines quite as nicely. We can still use it, but only if we reference the administrative share when opening the file. For example, to open the file Log1.txt in C:\Logs on the remote computer atl-fs-01 we’d have to use a path like this:

\\atl-fs-01\C$\Logs\Log1.txt

That’s doable, but you’ll have to add in some code to construct a path similar to that.

Note. OK, you win. We can’t do that in today’s column, but sometime in the next couple days we’ll show you how to add code like that to this script.

Now, let’s see, where were we … oh, right. After connecting to the WMI service we then use this query to retrieve a list of all the files in the C:\Logs folder:

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

We should add that we’re assuming you want to read all the files in the folder. If that’s not the case, then you’ll need to add an additional line or two to filter out the files you don’t want. (For example, you might check the file extension and read the file only if it has a .log extension.)

As you might expect, the WMI query returns a collection of all the files in the C:\Logs folder; we then set up a For Each loop to walk through that collection. For each file in the collection we use this line of code to retrieve the file name (which is the WMI equivalent to the file path):

strFilePath = objFile.Name

Next we call the OpenTextFile method to open the file for reading. We then have this little block of code:

Do Until objTextFile.AtEndOfStream
    strLine = objTextFile.ReadLine
Loop

What are we doing here? Well, the FileSystemObject is not the most sophisticated text-parser in the world. For example, it can read a file only from the top to the bottom; there’s no way to tell it, “Just read the last line in this file.” Therefore, we have to read the file line-by-line. Each time we read a line we store the contents of that line in a variable named strLine (replacing whatever was previously in the variable). When we read the last line of the file (which is the line we want), the contents of that line (and only that line) will be stored in strLine.

Note. Yes, it sounds a bit kludgy, and it is. Fortunately, though, while the FileSystemObject is not very sophisticated, it is pretty fast: in practical terms, it can read a file line-by-line just about as fast as it could jump to the bottom of the file and read just the last line.

Now that we have the last line of the file tucked away in the variable strLine, what next? Well, how about this line of code:

strMessage = strMessage & strLine & vbCrLf

Here we’re taking that last line and adding it to a variable named strMessage. Notice the equation: we’re assigning strMessage the value of whatever happens to be in strMessage plus the value of strLine plus a carriage return-linefeed (vbCrLf). In other words, suppose our first file has this as its last line:

File 1

The first time through the loop, strMessage will have the same value. Now, suppose the second file has the last line File 2. In that case, the second time through the loop strMessage will equal this:

File 1
File 2

As you can see, we’re simply constructing a list, in memory, of all the last lines as we go.

After adding the contents of strLine to the variable strMessage, we then close the first file, loop around, and repeat the process with the next file in the collection.

Once we’ve read all the files it’s time to start up Word. These three lines of code start an instance of the Word.Application object, make that instance visible onscreen, and then give us a new, blank document to work with:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

All that’s left now is to create an instance of the Word Selection object and then use the TypeText method to type our text into the document:

Set objSelection = objWord.Selection
objSelection.TypeText strMessage

And what text are we typing? That’s right: the value of strMessage, the variable containing the last lines of each of our text files. Which means that, technically we’re not pasting that information into Word. But the net result is exactly the same. And considering the fact that we thought of this all by ourselves, well ….

In testing this script we ran into a file that, when placed into Word, ended up with non-printing characters between each character in the line. To be honest, we didn’t have the time to investigate this more fully. However, if you run into that problem this revised version will largely fix things: it uses Word’s CleanString method to remove non-printing characters from the string. You might find the spacing to be a bit off, but the resulting line will still be easier to read.

Here’s the revised script, which includes only one additional line of code, the line that calls the CleanString method:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

strComputer = "."

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

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

For Each objFile In colFileList
    strFilePath = objFile.Name
    Set objTextFile = objFSO.OpenTextFile(strFilePath, ForReading)
    Do Until objTextFile.AtEndOfStream
        strLine = objTextFile.ReadLine
    Loop
    strMessage = strMessage & strLine & vbCrLf
    objTextFile.Close
Next

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
strMessage = objWord.CleanString(strMessage)

Set objSelection = objWord.Selection
objSelection.TypeText strMessage


How Can I Add the Last-Saved Date to the Footer of a Microsoft Word Document?

Hey, Scripting Guy! How can I put the date that a Microsoft Word document was last saved in the footer of that document? Oh: and how can I center that footer while I’m at it?

-- GH

Hey, GH. Well, the best way to go about – say, wait a second here: that sounds suspiciously like two questions, one about putting the last-saved date in the footer, the other about centering that footer. You’re not trying to trick the Scripting Guys and get a two-for-the-price of one deal, are you?

If you are, well, that’s OK; don’t worry about it. After all, now that the 2007 Winter Scripting Games are complete the Scripting Guys have nothing else to do anyway.

Well, other than finish testing all the entries and then total up the final results. And draw names and mail out 250 Dr. Scripto bobblehead dolls and 20 copies of Windows PowerShell in Action. And put together a couple of follow-up articles on the Games and how people went about solving the various challenges. And ….

Editor’s Note: Anyone else getting the feeling that the Scripting Guy who writes this column is getting a little whiney these days?

You know, that does sound like a lot of work, doesn’t it? But any time there’s work to be done there’s one thing you can rely on the Scripting Guy who writes this column to do: spend his time answering your questions about Microsoft Word footers and letting Scripting Guy Jean Ross take care of all that Scripting Games stuff. Here’s an answer to your two questions, GH. That takes care of our end of the bargain. And we’re sure that Jean will work day and night in order to complete her responsibilities as well.

Here’s the script:

Const wdFieldSaveDate = 22
Const wdAlignParagraphCenter
= 1

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()

Set objRange
= objDoc.Sections(1).Footers(1).Range
objDoc.Fields.Add objRange, wdFieldSaveDate

objRange.ParagraphFormat.Alignment = wdAlignParagraphCenter

objDoc.SaveAs("C:\Scripts\Test.doc")

Thanks, and see you tomorrow.

What’s that? Explain how this script works? Oh, come on, GH. After all, we already wrote the script; shouldn’t Jean have to do something here, too?

OK, fine. As you can see, we start out by defining a pair of constants: wdFieldSaveDate (with a value of 22), which we’ll use to insert the date that the document was last saved; and wdAlignParagraphCenter (with a value of 1), which we’ll use to center the footer. We then use the following block of code to create an instance of the Word.Application object, make this instance visible onscreen, and then create a new document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()

Now we’re ready to add the last-saved date to the footer of this new document. To begin with, we use this line of code to create an object reference to the first (and, in this case, only) footer in the document:

Set objRange = objDoc.Sections(1).Footers(1).Range

Once we have an object reference to the footer range we can call the Add method of the Fields collection and add the last-saved date:

objDoc.Fields.Add objRange, wdFieldSaveDate

In case you’re wondering (and you probably are), adding the last-saved date to a document using a script works very similar to the process of adding that value from within Word itself. If you’re working in Microsoft Word you can add the last-saved date to a document by (in Word 2003) selecting Field from the Insert menu and then choosing SaveDate. Here we’re doing essentially the same thing, calling the Add method and then passing along two parameters: the object reference to the location where we want to add the field; and the constant representing the field we want to add. How did we know that we need to use the constant wdFieldSaveDate, with a value of 22, in order to add the last-saved date to the footer? To tell you the truth, we didn’t; that’s why we looked this up in the Microsoft Word VBA Language Reference (look for wdFieldType).

So much for question 1; that’s all we had to do to add the last-saved date to the footer. Now let’s tackle question 2, something we can take care of with a single line of code:

objRange.ParagraphFormat.Alignment = wdAlignParagraphCenter

As you can see, we’re again working with the footer range (note the object reference objRange). This time, though, we’re accessing the ParagraphFormat object and assigning a value to the Alignment property. How did we know that the constant wdAlignParagraphCenter, with a value of 1, would give us centered alignment. That’s right: we didn’t. (Hint: You’ll rarely go wrong by assuming that the Scripting Guys don’t know much of anything.) Once again, however, it’s the Microsoft Word VBA Language Reference to the rescue (look for wdParagraphAlignment).

In the very last line of the script we then use the SaveAs method to save the document as C:\Scripts\Test.doc. We do that simply so you can verify that the script worked; after all, if the document has never been saved then it won’t have a last-saved date.

We hope that answers both your questions, GH. Which reminds us: we actually did do two things here, didn’t we? Looks like Jean’s not only going to have to finish up with the 2007 Scripting Games, but she’ll have to take care of the 2008 Scripting Games as well. But hey, fair is fair, right?

Editor’s Note: Maybe with Jean taking over everything the 2008 Scripting Games won’t have another Event 8 episode like we saw this year. We won’t mention who came up with that one.


How Can I Apply a Theme to a Microsoft Word Document?

Hey, Scripting Guy! How can I apply a theme to a Microsoft Word document?

-- YP

Hey, YP. You know, things finally seem to be getting back to normal here in the Seattle area. The weather over this past weekend was in the mid-50s, which is where it should be this time of year. (Actually it should be in the mid-80s, but ….) The University of Washington men’s basketball team, marked by injuries and inconsistency all season long, finally put it all together and closed the regular season by whipping 17th-ranked USC and No. 2-rated UCLA. The Seattle Supersonics are complaining that they need a brand-new $500 million arena, just a few years after almost $200 million was spent revamping their current home (Key Arena) to their exact specifications.

Oh: and we’re now 5 games into Major League Baseball’s exhibition season and the Seattle Mariners already have 5 losses. Like we said, everything’s back to normal.

And if everything is back to normal then surely the Scripting Guys must have a script that shows you how to apply a theme to a Microsoft Word document. You know, a script just like this one:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection
objSelection.TypeText "Here is a bulleted list."
objSelection.TypeParagraph()
objSelection.TypeParagraph()

Set objRange = objDoc.Paragraphs(1).Range
objRange.Style = "Normal"

Set objRange = objDoc.Paragraphs(3).Range
objRange.ListFormat.ApplyBulletDefault

objSelection.TypeText "Item 1"
objSelection.TypeParagraph()
objSelection.TypeText "Item 2"
objSelection.TypeParagraph()
objSelection.TypeText "Item 3"
objSelection.TypeParagraph()

Set objRange = objDoc.Paragraphs(6).Range
objRange.Style = "Normal"

objSelection.TypeParagraph()
objSelection.TypeText "No longer in a bulleted list."

objDoc.ApplyTheme "Balance"

Whatever you do, don’t panic: this script is nowhere near as complicated as it might look. In fact, as you’re about to see, once you have a document up and running you can apply a theme using a single line of code. Obviously we have way more than one line of code in this script, but that’s simply so we can create a document that has some text and a bulleted list; we need those elements to make it obvious the effect our new theme has.

In other words, don’t panic because there’s no need to panic. The Scripting Guys have everything under control.

Just like we always do.

We start out by using these two lines of code to create a running instance of Microsoft Word that we can see onscreen:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Once we have Word up and running we then use the following two lines of code to create both a new document and an instance of the Word Selection object:

Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

Note. Why do we need an instance of the Word Selection object? That’s easy: we need the Selection object so we can start typing text into the document.

After that we have a big section of code that types some text, adds a bulleted list, and then tacks a bit more text on to the end of the document; that’s what all this is for:

objSelection.TypeText "Here is a bulleted list."
objSelection.TypeParagraph()
objSelection.TypeParagraph()

Set objRange = objDoc.Paragraphs(1).Range
objRange.Style = "Normal"

Set objRange = objDoc.Paragraphs(3).Range
objRange.ListFormat.ApplyBulletDefault

objSelection.TypeText "Item 1"
objSelection.TypeParagraph()
objSelection.TypeText "Item 2"
objSelection.TypeParagraph()
objSelection.TypeText "Item 3"
objSelection.TypeParagraph()

Set objRange = objDoc.Paragraphs(6).Range
objRange.Style = "Normal"

objSelection.TypeParagraph()
objSelection.TypeText "No longer in a bulleted
list."

We’re not going to take the time to explain everything that takes place here; if you’re interested in understanding how this all works (and, in particular, how to add a bulleted list in a Word document), well, that’s what this Office Space article is for. Suffice to say that the preceding code gives us a Word document that looks like this:

What’s that? No, this isn’t a boring old Word document, it – well, OK, maybe it is a little boring. But, then again, that’s why we included this final line of code in the script:

objDoc.ApplyTheme "Balance"

All we’re doing here is calling the ApplyTheme method and changing the document theme to Balance. As you can see, calling the ApplyTheme method can be as simple as passing the name of the theme folder to the method. Note that we said you had to pass the name of the theme folder, which is not necessarily the name of the theme as seen in the Word UI. For example, in Word, if you click Format and then click Theme you’ll see (depending on your setup) a theme named Blueprint. However, to apply that theme you use this line of code:

objDoc.ApplyTheme "Blueprnt"

We specifyBlueprnt as the theme because that’s the name of the folder where the Blueprint theme is stored.

Note. Good question: where do you find these folders? This can vary depending (among other things) on the version of Office you have, but here’s a good place to start your search: C:\Program Files\Common Files\Microsoft Shared\THEMES11. Incidentally, you can use themes that are stored in locations other than the default Themes folder; in that case, you just need to specify the complete folder path.

Here’s something else to keep in mind. Themes actually have three different formatting options:

· Vivid Colors

· Active Graphics

· Background Image

You can individually enable or disable any of these options by passing the appropriate three-digit value along with the theme folder name. To enable an option, set the appropriate digit to 1; to disable an option, set the appropriate digit to 0. For example, to enable all three options for the Blueprnt theme use this line of code:

objDoc.ApplyTheme "Blueprnt 111"

What if you only wanted to enable Active Graphics? In that case you’d set the Active Graphics option value (the second digit) to 1 and the other two digits to 0:

objDoc.ApplyTheme "Blueprnt 010"

If you don’t specify a three-digit value then only Active Graphics and Background Images are enabled. In other words, the default value is equivalent to this:

objDoc.ApplyTheme "Blueprnt 011"

And what will our document look like after we apply the Balance theme? With any luck it will look just like this:

What’s that? You say you don’t necessarily want to change the theme that’s been applied to a document, you’d just like to know which theme has been applied to a document? No problem:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Wscript.Echo objDoc.ActiveTheme
Wscript.Echo objDoc.ActiveThemeDisplayName

Oh, we almost forgot: here’s something else that’s going on in Seattle. Right now the city is conducting a special election on replacing the Alaskan Way Viaduct (a much-traveled section of highway). The ballot includes two options: a tunnel (which the state of Washington refuses to pay for) and a new elevated roadway (which the city of Seattle refuses to build). And Seattle being Seattle, voters aren’t asked to choose between one option or the other: instead, they can vote for both options. Everyone can be a winner! Best of all, the two options favored by a lot of people – repair the existing viaduct or build a plain old “surface” road – aren’t even on the ballot. But that’s OK; after all, this election (which will cost $1 million to conduct) is only advisory; no one has to pay any attention to the results anyway.

Just like we said: everything is back to normal.


How Can I Boldface a Specific Word Throughout a Microsoft Word Document?

Hey, Scripting Guy! How can I boldface a specific word throughout a Microsoft Word document?

-- SB

Hey, SB. You’ll probably find this hard to believe, but when you work with scripts and scripting all day you tend to get a little jaded. You say you have a script that automatically backs up and clears all the event logs on all your computers? How nice. You have a script that monitors folders and lets you know when new files are added? Been there, done that. You have a script that can bring the dead back to life? Hey, who doesn’t?

For some reason, though, SB, your question piqued our interest. Maybe it’s because we’re always looking for a script that uses Microsoft Word; scripting with Word is actually kind of fun. Or maybe it’s because this request offered a bit of a challenge: although we were pretty sure that we could write a script that would boldface specific words in a document we’d never actually tried it. Or maybe it’s because all the dead people we brought back to life have been pretty insistent that we go through their documents and boldface specific words. You know, things like the phrase I am too alive.

Oh, well. Regardless of what motivated us, here’s what we came up with:

Const wdReplaceAll  = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

objSelection.Find.Text = "Fabrikam"
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE

objSelection.Find.Replacement.Font.Bold = True

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

As you can see, not only was this a fun script to write but – as an added bonus – it was an easy script to write, too. After defining a constant named wdReplaceAll (we’ll talk about that in a moment), we create an instance of the Word.Application object and then set the Visible property to True; this gives us an instance of Microsoft Word that we can see on screen. We use the Open method to open the document C:\Scripts\Test.doc, then create an instance of the Word Selection object (which, by default, positions the cursor at the beginning of the document).

Now we’re ready to roll. What we want to do is boldface any instances of the word Fabrikam; hence we set the value of the Find object’s Text property to “Fabrikam” (in other words, that’s what we’re searching for). Next we set the Forward property to True; this ensures that our search will start at the beginning of the document and end at, well, the end of the document. Finally, we set the MatchWholeWord property to True; we do this just in case there happens to be some crazy construction like FabrikamCorporation in the document. If MatchWholeWord was set to False, then any instance of the string Fabrikam would be boldfaced. That would give us something similar to this: FabrikamCorporation. Which we probably don’t want.

Note. Yes, we are kind of zipping through the explanation here, aren’t we? But that’s OK: after all, we have an Office Space article that discusses the process of finding and replacing text in more detail.

All that gives us a mechanism for finding all instances of the word Fabrikam. But once we find them how do we boldface each of those instances? Here’s how:

objSelection.Find.Replacement.Font.Bold = True

Yes, it’s that easy. Here we use the Replacement object (a child object of the Find object) and specify that the Font.Bold property should be True. That’s all we have to do. If we wanted to un-boldface each instance of Fabrikam we would set the Font.Bold property to False. Needless to say, we could specify new replacement text, a new replacement font size, pretty much anything we want.

See why we like scripting with Microsoft Word so much?

After configuring the replacement object we then call the Execute method to start the find-and-replace operation:

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

And, no, we didn’t fall asleep with our finger on the comma key. Well, not this time anyway. As it turns out, there are tons of optional parameters available for the Execute method. For this script we didn’t care about any of those. However, because parameters have to be specified in order, we couldn’t just do something like this:

objSelection.Find.Execute wdReplaceAll

That would make the constant ReplaceAll (which tells Word we want to replace all instances of our search text) the first parameter passed to the Execute method, which would cause the script to fail. Hence all the “empty” parameters represented by the commas. And don’t worry: if that doesn’t make any sense to you, take a look at the documentation for the Execute method and you’ll see a list of all the parameters we skipped.

OK. That was fun, wasn’t it? In fact, we’d love to stay and chat about it some more, but Abe Lincoln and Genghis Khan are both out of iced tea. You know, writing a script to bring the dead back to life really wasn’t all it’s cracked up to be ….


How Can I Center-Align a Picture in a Word Document?

Hey, Scripting Guy! How can I center-align a picture in a Word document?

-- CD

Hey, CD. In light of the controversy surrounding World Series game 2 (was Detroit Tigers pitcher Kenny Rogers throwing “goopballs” or wasn’t he?) the Scripting Guys felt it was important to note that no illegal substances were used in the writing of today’s Hey, Scripting Guy! column: no pine tar, no Vaseline, not even any peanut butter. Nothing.

OK, true, there are cookie crumbs wedged between some of the keys on the keyboard. But those don’t count.

Note. If you’re not a fan of America’s national pastime we should make it clear that the sport has very strict rules against “doctoring” baseballs. Why do major league umpires throw out any baseball that has so much as a blemish on it? Because major league pitchers can take a baseball that has a scuff mark, a dab of Vaseline, or a little pine tar on it and then throw a pitch that is essentially unhittable: typically the ball will come in straight and then, swiftly and unexpectedly, dive straight into the ground. (According to the old cliché “like it rolled off a table.”) Because these pitches are well-nigh impossible to hit, the so-called “spitball” has been illegal for decades.

And yes, up until 1920 the spitball was legal; in fact, Scripting Guy Peter Costantini used to throw the spitter as a boy in New Jersey.

Of course, at the risk of tarnishing the spotless reputation of the Scripting Guys, the Scripting Guy who writes this column would have happily used an illegal substance when writing today’s column; he just couldn’t figure out how a dab of Vaseline would help him center-align a picture in Microsoft Word. As it turns out, the process isn’t all that difficult, but it’s not exactly intuitive, either, as you’re about to see.

Ethical question. So if the Scripting Guy who writes this column would have happily used an illegal substance in writing this column does that mean he would have used an illegal substance when pitching a baseball? Heavens no! After all, that would have been against the rules.

Besides, he could never really control the spin of the ball any time he tried.

At any rate, CD, here’s a perfectly natural, perfectly legal script that center-aligns a picture in Microsoft Word:

Const wdShapeCenter = -999995
Const wdWrapSquare
= 0

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

Set objShape
= objDoc.Shapes
objShape.AddPicture("C:\Scripts\Welder-small.jpg")

Set objShapeRange
= objDoc.Shapes.Range(1)
objShapeRange.Left = wdShapeCenter

objShapeRange.WrapFormat.Type = wdWrapSquare

No promises here, but we’ll try to explain this script the best we can. As you can see, we start out by defining a pair of constants, wdShapeCenter (and yes, -999995 is the real value we need to assign to the constant) and wdWrapSquare. We’ll use these two constants to center the picture (wdShapeCenter) and then to configure the picture word wrap (wdWrapSquare).

After defining the constants we create an instance of the Word.Application object and set the Visible property to True; that gives us a running instance of Microsoft Word that we can see on screen. We then use this line of code to create a new document we can work with:

Set objDoc = objWord.Documents.Add()

Our next step is to insert a picture into the document. To do that we create an instance of the document’s Shapes collection, then use the AddPicture method to insert the picture C:\Scripts\Welder-small.jpg. That’s what these two lines of code are for:

Set objShape = objDoc.Shapes
objShape.AddPicture("C:\Scripts\Welder-small.jpg")

OK; now it gets a tad bit convoluted.

As you doubtless know, CD, by default Word left-aligns any pictures you insert programmatically. To center the picture we use these two lines of code:

Set objShapeRange = objDoc.Shapes.Range(1)
objShapeRange.Left = wdShapeCenter

In the first line we create an instance of the document’s ShapeRange object. To do that we reference the Shapes collection and the Range property; in particular, we reference the first Range which, for our purposes, is just the first picture in the collection. That’s what the Range(1) is for. What if we had two pictures in our document and we wanted to center-align picture 2? Then we’d use code like this:

Set objShapeRange = objDoc.Shapes.Range(2)

After we have a ShapeRange object that references our picture we can then align the graphic by setting the value of the Left property to the constant wdShapeCenter. Alternatively, we could right-align the graphic by using the constant wdShapeRight and the value -999996. Or we could set the Left property to wdShapeLeft (with the value -999998); that, in turn, left-aligns the picture.

It’s weird, but that’s the way it works.

Note. In one of those astonishing coincidences that link baseball, scripting, and real life, -999998 is also the batting average compiled by the Scripting Editor during this past softball season. To tell you the truth, we’re not totally sure how she could even have a negative batting average. But statistics don’t lie.

The final line of code in the script is optional, but you might find it useful. By default any picture you programmatically insert into a Word document gets plastered on top of the text. In other words, if you start adding text to your document you’ll end up with something that looks like this:

It might be a little hard to see in this screenshot, but the picture covers up some of the text. Admittedly, with a Hey, Scripting Guy! article, covering up the text isn’t necessarily a bad thing. But we’re guessing you’d prefer to be able to still read the document you just added your graphic to.

To address that problem we use this line of code to set the wrap Type to square (a task which requires us to reference the WrapFormat object, which is a child object of the ShapeRange object):

objShapeRange.WrapFormat.Type = wdWrapSquare

Now when we add text to the document that text will wrap nicely around the graphic:

Much better.

In case you’re interested, here’s a list of other wrap Type constants and values:

Constant | Value
wdWrapInline | 7
wdWrapNone | 3
wdWrapSquare | 0
wdWrapThrough | 2
wdWrapTight | 1
wdWrapTopBottom | 4

Hope that helps, CD.

Although we hate to moralize, the Scripting Guys would like to close by stating we think it’s a shame that, in this day and age, anyone who does something really well is immediately accused of cheating, whether that’s by doctoring a baseball, taking steroids, or engaging in some other illicit practice. Sadly, that’s the price you pay for success these days.

Interestingly enough, the Scripting Guys have never once been accused of cheating.

Hmmm ….


How Can I Change an Existing Hyperlink in a Microsoft Word Document?

Hey, Scripting Guy! How can I change an existing hyperlink in a Microsoft Word document?

-- PH

Hey, PH. You know, we’re impressed that you had the guts to ask a question today; after all, today is Friday the 13th, a date many people dread, fearing that anything bad that could happen will happen. For example, people who suffer from Paraskevidekatriaphobia (a debilitating fear of Friday the 13th) would never ask the Scripting Guys a question on this day: they’d be too afraid that we’d give them a script that didn’t work, that we’d write a column that didn’t make sense, or that we’d blabber on and on about some arcane trivia that has nothing to do with scripting or with the question they asked.

Superstitious nonsense. Bah. Humbug!

Trivia note. Fridays have long been considered unlucky; according to tradition, Adam and Eve took their fateful bite of the apple on a Friday. (Although we should note that Peter Costantini, the oldest living Scripting Guy, says he remembers the two biting into the apple on a Wednesday morning.) Likewise, the number 13 has long been considered an unlucky number; many ancient cultures believed that it was unlucky to invite 13 people to dinner. (Which would definitely be true if you were the one cooking the dinner.) However, superstitions involving Friday the 13th seem to be a modern phenomenon; no references to Paraskevidekatriaphobia (or anything simlar) can be found prior to the 20th century.

But, like we said, this is all superstitious nonsense. As it turns out, PH, this is your lucky day; after all, we just happened to have a script that can change an existing link in a Microsoft Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("c:\scripts\test.doc")

Set colHyperlinks = objDoc.Hyperlinks

For Each objHyperlink in colHyperlinks
    If objHyperlink.Address = "http://www.microsoft.com/" Then
        objHyperlink.Address = "http://www.microsoft.com/technet/scriptcenter/"
    End If
Next

As you can see, we start out by creating an instance of the Word.Application object; we then set the Visible property to True, giving us a running instance of Microsoft Word that we can see onscreen. (Of course, you don’t have to see Word onscreen in order to change a hyperlink. For educational purposes, however, we’ve always found it better if people can watch as the excitement unfolds.)

With Word up and running we then use the Open method to open the document C:\Scripts\Test.doc. As it turns out, Test.doc happens to have several hyperlinks pointing to http://www.microsoft.com/. That, of course, is the Microsoft.com home page; why the heck would anyone want to go there? Consequently, we need our script to locate all instances of this particular hyperlink and change the target address to the Internet’s true hot spot: http://www.microsoft.com/technet/scriptcenter/.

Note. No, Google has not offered us $1.6 billion for the Script Center, at least not yet. But that’s probably because they know that the Scripting Guys would never sell.

So how do we go about changing these hyperlinks? Well, to begin with, we use this line of code to retrieve a collection of all the hyperlinks in the document:

Set colHyperlinks = objDoc.Hyperlinks

Once we have the collection in hand we set up a For Each loop to loop through each hyperlink in the collection. Inside that loop we use the following line of code to see if the Address property (the property representing the hyperlink target) is equal to http://www.microsoft.com/:

If objHyperlink.Address = "http://www.microsoft.com/" Then

If Address isn’t equal to http://www.microsoft.com/ we skip that hyperlink, loop around, and continue the process with the next item in the collection. If the hyperlink is equal to http://www.microsoft.com/, we then use this line of code to retarget the link to the Script Center instead:

objHyperlink.Address = "http://www.microsoft.com/technet/scriptcenter/"

And then we loop around and check the next link in the collection. When we’re all done all the links that used to point to the Microsoft.com home page will now point to the Script Center. (You can verify that by holding the mouse over each link and viewing the little target address popup that will appear.)

There you have it, PH. Best of all, it turns out that this is an even luckier day for you than we first thought. Not only did we manage to answer your question, but we did it quickly enough that we have time to relate even more historical facts for you. For example, you’re familiar with Noah’s Ark, right? According to legend, the rain began falling on a Friday. The destruction of the Tower of Babel? Occurred on a Friday. And then here’s the --

Note. Sorry, PH; it seems as though Friday the 13th really is an unlucky day. After all, we had several more pages of fascinating Friday facts, but the Scripting Editor seems to have inadvertently deleted them. Please accept our deepest apologies. We’ll see what we can do to retrieve all that information for you. It just wouldn’t be a Hey, Scripting Guy! without it.


How Can I Change File Locations for Microsoft Word?

Hey, Scripting Guy! In Microsoft Word, how can I change the file locations for User Templates and Workgroup Templates?

-- SB

Hey, SB. At least one of the Scripting Guys has always had a lifelong fear that he would write, say, the greatest novel ever written, except that no one would know about it or care about it. Well, not until he died, of course. At that point, the book would be discovered, it would rocket to the top of the best-seller list, and it would be the Scripting Heir who would become rich.

Note. To guard against this happening, this Scripting Guy has vowed never to write anything good. So far he’s managed to stick to that promise without any problem.

This same Scripting Guy has a similar fear regarding scripts for managing Microsoft Office. The Script Center actually has a number of scripts available in the Scripting for Office center that show you how to carry out administrative tasks such as changing file locations. Few people seem to use these, yet tons of people ask how they can write scripts for managing Microsoft Office. It’s like that great, undiscovered novel in the attic: if something were to happen to this Scripting Guy, the Scripting Heir would inherit all those scripts. Heaven forbid!

That’s why we decided to do this brief infomercial before answering our question. If you’re looking for scripts that carry out administrative tasks for Microsoft Office be sure to check out the Scripting for Office center. And if you see some 15-year-old kid nosing around there, remind him that his Scripting Dad is in tip-top shape and will probably live for another 100 years. Or more.

As for changing the file locations for User Templates and Workgroup Templates, well, this script ought to do the trick:

Const wdUserTemplatesPath = 2
Const wdWorkgroupTemplatesPath = 3

Set objWord = CreateObject("Word.Application")
Set objOptions = objWord.Options

objOptions.DefaultFilePath(wdUserTemplatesPath) = "C:\Templates"
objOptions.DefaultFilePath(wdWorkgroupTemplatesPath) =
"C:\Workgroup\Templates"

objWord.Quit

We begin by defining a pair of constants – wdUserTemplatesPath and wdWorkgroupTemplatesPath – that we’ll use to indicate the file locations we want to change. There are a number of other file locations that can be changed using a script; for a complete list (and for the constants and their associated values) see the wdDefaultFilePath enumeration in the Microsoft Word VBA Language Reference on MSDN.

We then create an instance of the Word.Application object as well as its child object, Options. You might notice in this script that we don’t set the Visible property to True like we do in most of our sample scripts. That’s because we don’t want Word to be visible onscreen; we want to open up Word in an invisible window, make our changes, and then close Word without anyone seeing a thing.

To change a file path all we need to do is change the value of the DefaultFilePath property, making sure to use one of our constants to indicate which file path we want to change. For example, to change the User Templates path to C:\Templates we use this line of code:

objOptions.DefaultFilePath(wdUserTemplatesPath) = "C:\Templates"

And to change the Workgroup Templates path to C:\Workgroup\Templates we use this line of code:

objOptions.DefaultFilePath(wdWorkgroupTemplatesPath) = "C:\Workgroup\Templates"

That’s all we have to do; we don’t even need to call a Save function of any kind. We change the file locations and then use the Quit method to close Word. (If we don’t do that, an invisible instance of Word will continue running in the background.)

And now that people know about the scripts for managing Microsoft Office, we do one other thing: sit back and wait for fame and fortune to roll in.


How Can I Change the Default File Save Format in Microsoft Word?

Hey, Scripting Guy! I’m running Word 2007, but most of my colleagues are still running Word 2003. Is there a way I can use a script to change the default file type when saving documents in Word? I’d prefer that, by default, documents are saved in Word 2003 format rather than Word 2007 format.

-- PJ

Hey, PJ. You know, we’re nearing the mid-point for the month of June, which means that time is beginning to run out for the TechEd Challenge. If you somehow missed the Challenge announcement, it’s a fun little competition involving a word puzzle very similar to those found in Dr. Scripto’s Fun Book, something the Scripting Guys handed out at TechEd. (Actually, now that we think about it, it’s exactly like one of the puzzles found in Dr. Scripto’s Fun Book.) The premise is simple: we give you a bunch of letters and you try to form as many VBScript functions as you can, using only the letters provided to you. The object of the game? Use as many letters as you can, thereby earning as high a score as you can.

And why would you even want to earn as high a score as you can? That’s easy: the top 5 scorers will all win a Scripting Guys Gift Pack, a veritable cornucopia of prizes including:

· A Dr. Scripto bobblehead doll

· A Scripting Guys T-shirt

· A Script Center coffee mug

· Your very own copy of Dr. Scripto’s Fun Book

· $150 million in cash (Editor’s Note: No, not this.)

Note. Needless to say, the $150 million in cash falls into the category of “while supplies last.” In all honesty, your Gift Pack probably won’t contain $150 million in cash. But we’ll see what we can do.

Scratch that: the Scripting Editor just informed us that we won’t “see what we can do.” Instead, we won’t do anything at all. So, sorry, no cash. Please direct all your angry emails to the Scripting Editor and not to the Scripting Guy who writes this column. After all, he’s the nice one who’s only trying to thank everyone for the support you’ve given us over the years.

And yes, we know: not everyone is good at word puzzles. (Did we mention that Scripting Guy Jean Ross tried the challenge herself and scored 116? We thought that was a pretty good score, at least until we saw the scores turned in by everybody else in the world. Wow.) But that’s OK; just send in an entry of some kind (even if you don’t get a single point) and you’ll be eligible to win a Dr. Scripto bobblehead doll. Sometime in July we’ll toss all the entries received into a hat, draw 10 names, then send each of those people a bobblehead.

We’ll also send each of those people $150 mil – um, never mind. We’ll just send them a bobblehead.

But don’t panic: you have until the end of the month, so there’s still plenty of time to enter. (If you’ve already entered, we’re going to start scoring entries – and posting results – either today or tomorrow.) In the meantime, we have another challenge that needs tackling: changing the default save format in Microsoft Word 2007.

Let’s start by taking a look at a script that displays Word onscreen and then changes the default save format. We’ll then show you a variation of this first script, a variation that makes this change without anything appearing to happen; under that script, Word starts, the default save format is changed, and then Word exits, all without anything appearing on screen.

But first things first:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

objWord.DefaultSaveFormat = "doc"

Believe it or not, that’s the entire script. (One of the interesting things about scripting is that tasks that sound hard are often ridiculously easy, whereas tasks that sound ridiculously easy are, well ….) Regardless, we really don’t do much work here. (Scripting Guys trivia: Interestingly enough, that exact phrase appears throughout our annual performance reviews.) We start off by creating an instance of the Word.Application object, and then setting the value of the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. We then use this single line of code to change the value of the DefaultSaveFormat property to doc, which just happens to represent the Word 2003 document format:

objWord.DefaultSaveFormat = "doc"

And that’s it. Suppose you start a new document and then go to save the file. Take a peek at the default file format that appears in the Save As dialog box:

Pretty slick, huh?

If you’d just as soon this happen automatically, without Word ever appearing on screen, then use this script instead:

Set objWord = CreateObject("Word.Application")

objWord.DefaultSaveFormat = "doc"
objWord.Quit

This is pretty much the same script, with two exceptions. First, Word runs in the background, without ever appearing onscreen; that’s because we never set the Visible property to True. Second, we call the Quit method at the end in order to terminate our instance of Word; if we don’t do that, Word will continue to chug away in the background. That probably won’t hurt anything, but ….

That’s all well and good, but what if you change your mind and would like to restore Word 2007 as the default save format? No sweat; just set the value of the DefaultSaveFormat property to docx:

Set objWord = CreateObject("Word.Application")

objWord.DefaultSaveFormat = "docx"
objWord.Quit

That’s all there is to it.

A couple quick notes here. First, you aren’t limited to using Word 2003 or Word 2007 as the default save formats. In Word 2007, bring up the Save As dialog box and then click on Save as type. Notice all the different file types available to you:

We haven’t tried every last one of these, but as far as we know you can set the default save format to any of these file types. Just set the DefaultSaveFormat property to the desired file extension, not including the period. (In other words, RTF, not .RTF.)

In addition, this same script works on earlier versions of Word; it’s not specific to Word 2007. Obviously on Word 2003 you won’t have the same set of file types to choose from, but you can use a script to change the default save format to the file types that are supported by Word 2003.

In closing, we should note that, as part of the TechEd Challenge, we announced that, for one day, we would rename the Script Center after anyone who achieved a perfect score of 140. We haven’t officially scored any of the entries yet, but it’s possible that one or more people have actually achieved a perfect score. If that’s true will we really rename the Script Center after each of these people? Well, for one day at least, yes, we will. Was that a reckless, immature, and irresponsible promise to make? In retrospect, yes, it was.

But don’t worry: Jean will be severely reprimanded for coming up with such an outrageous proposal. You just never know what crazy scheme she’s going to come up with next.


How Can I Change the Default Highlight Color for a Microsoft Word Document?

Hey, Scripting Guy! How can I change the default highlight color for a Microsoft Word document?

-- TW

Hey, TW. You know, any time the Scripting Guy who writes this column wants to terrify the Scripting Son, he tells the blood-curdling saga of the first TV the Scripting Dad remembers: a TV that showed everything in black-and-white only! Granted, that was a long time ago, and the Scripting Dad has only vague memories of that TV; not too long after he was born his Scripting Parents bought a color TV. But it was real nonetheless, and just the thought of having to watch everything in black-and-white is enough to give the Scripting Son the heebie-jeebies.

And no, the Scripting Dad doesn’t mention the fact that there were no remote controls in those days, either. It’s one thing to give the Scripting Son a little scare every now and then; it’s quite another thing to terrify and traumatize the poor kid.

Note. We also should point out that the fact that the Scripting Dad has only vague memories of this TV doesn’t mean that all of this happened hundreds of years ago. After all, the Scripting Dad has only vague memories of what he had for breakfast this morning, despite the fact that – Mondays through Fridays – he pretty much has the same thing for breakfast every morning.

The important thing here – beyond the fact that kids these days have no idea how good they really have it – is the fact that color is important, regardless of whether we’re talking about TVs or Microsoft Word documents. So can you change the default highlight color for a Word document? Well, as long as you start that document with a script you can:

Const wdBrightGreen = 4

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Add()

Set objOptions = objWord.Options
objOptions.DefaultHighlightColorIndex = wdBrightGreen

objWord.Visible = True

As you can see, we start out by defining a constant named wdBrightGreen and assigning it the value 4. Needless to say, we do that because we want to set the default highlight color to bright green. (Hey, when you start out life watching TV in black-and-white any color looks good, even bright green.) What if you consider bright green to be a bit garish? No problem; you can use any of the constants and values shown in the following table:

Constant | Value
wdAuto | 0
wdBlack | 1
wdBlue | 2
wdBrightGreen | 4
wdByAuthor | -1
wdDarkBlue | 9
wdDarkRed | 13
wdDarkYellow | 14
wdGray25 | 16
wdGray50 | 15
wdGreen | 11
wdNoHighlight | 0
wdPink | 5
wdRed | 6
wdTeal | 10
wdTurquoise | 3
wdViolet | 12
wdWhite | 8
wdYellow | 7

After defining the constant we use these two lines of code to create an instance of the Word.Application object and open a new, blank document:

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Add()

As soon as we have a document we’re ready to change the default highlight color. To do that we need to do two things: 1) create an instance of Word’s Options object, and 2) assign the constant wdBrightGreen to the Option object’s DefaultHighlightColorIndex property. That’s what we do here:

Set objOptions = objWord.Options
objOptions.DefaultHighlightColorIndex = wdBrightGreen

That’s all we have to do. If we make our instance of Word visible (which we do, in the last line of the script) and if you look on the toolbar you’ll see that the default highlight color is now bright green. How ‘bout that?

What the preceding script does is set the default highlight color; if you now manually highlight text in a Word document and then click the highlight button that text will be highlighted in bright green. However, if you want to highlight text programmatically you don’t need to set the default highlight color. Instead, you can use a script similar to this:

Const wdBrightGreen = 4

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Add()
objWord.Visible = True

Set objSelection = objWord.Selection
objSelection.TypeText "Here is text that is not
highlighted."
objSelection.TypeParagraph()

objSelection.Range.HighlightColorIndex = wdBrightGreen

objSelection.TypeText "And here's text
highlighted in green"
objSelection.TypeParagraph()

Let’s see if we can quickly run through this script and figure out what it does. We start out – again – by defining a constant named wdBrightGreen and setting the value of that constant to 4. We then create an instance of Word, add a new document, and then make that instance of Word visible onscreen. That’s all old hat by now. (Isn’t it?)

Our next step is to create an instance of the Word Selection object, then use the TypeText and TypeParagraphs() methods to type a paragraph (a paragraph of non-highlighted text) into our document. That’s what we do here:

Set objSelection = objWord.Selection
objSelection.TypeText "Here is text that is not highlighted."
objSelection.TypeParagraph()

What we want to do now is type in some text that is highlighted. To accomplish that feat all we need to do is use the Selection object’s Range object and set the value of the HighlightColorIndex property to wdBrightGreen:

objSelection.Range.HighlightColorIndex = wdBrightGreen

Until we tell the Range object something different, all the text we type from this point on will be highlighted in bright green. In fact, when our script finishes running our Word document will look like this:

Cool, huh? To turn highlighting off just set the HighlightColorIndex to 0:

objSelection.Range.HighlightColorIndex = 0

Hope that helps, TW. As for the Scripting Son, well, we’re going to go easy on him. We thought about telling him about bottles of Pepsi that couldn’t be opened by hand but required a bottle opener instead. Or maybe the fact that, way back when, you could only watch cartoons on Saturday mornings; there was no such thing as 24-hour cartoon channels. But that would be just plain mean, wouldn’t it?

Besides, he probably wouldn’t believe us anyway.


How Can I Change the Font Name and Size for an Entire Word Document?

Hey, Scripting Guy! How can I change the font name and size for an entire Word document?

-- CD

Hey, CD. You know, this question reminded us of how old we Scripting Guys are getting – well, all of us except for Jean, of course, who continues to get younger and more beautiful every day.

Note. No, that isn’t really true. However, because Jean’s the editor she’ll just edit this column in a way that makes it sound like she’s getting younger and more beautiful every day. With that in mind we decided to save her the trouble of having to rewrite the preceding paragraph. Don’t worry about rewriting everything to make yourself sound good, Jean: it’s already taken care of! (Editor’s Note: Thank you. And yes, the preceding paragraph really is true. Greg’s just jealous – he wishes he could look younger and more beautiful every day.)

At any rate, at least some of the Scripting Guys are old enough to remember the days when you had no choice but to use the same font and font size (typically Courier 10-point) throughout a document; back in those dark days of computing you didn’t have TrueType, ClearType, OpenType, PostScript or anything even remotely similar. Way back when, Frutiger Linotype and TW Cen MT Condensed Extra Bold were just a dream. Sigh ….

In other words, back then you couldn’t change the font name and size for an entire Word document. In today’s Golden Age of computing, however, all you need to do is run a script like this:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.TypeText "This is the first paragraph."
objSelection.TypeParagraph()
objSelection.TypeText "This is the second paragraph."
objSelection.TypeParagraph()
objSelection.TypeText "This is the third paragraph."
objSelection.TypeParagraph()

Set objRange = objDoc.Range()
objRange.Font.Name = "Arial"
objRange.Font.Size = 10

Yes, this is a very simple little script and, believe it or not, it’s even simpler than it might first appear; after all, about half the code is there just to type some text into our document. We start off by creating an instance of the Word.Application object, then setting the Visible property to True; that gives us a running instance of Microsoft Word that we can actually see on screen. We use the Add method to create a new document, then use this line of code to create a Selection object at the very beginning of the document:

Set objSelection = objWord.Selection

We should point out that we don’t have to create the Selection object in order to change the font name and size; we do it here simply because we’re creating a new document on-the-fly. If we were opening an existing document we could skip this line of code; in that case, we could also skip this block of code, which just adds some text to the document:

objSelection.TypeText "This is the first paragraph."
objSelection.TypeParagraph()
objSelection.TypeText "This is the second paragraph."
objSelection.TypeParagraph()
objSelection.TypeText "This is the third paragraph."
objSelection.TypeParagraph()

The fun really begins as soon as we have a Word document that includes some text (either because we created the document ourselves, as we did here, or because we opened an existing document). Probably the easiest way to change the font name and size for all the text in a document is to simply select the entire document and then make those changes to the selection. How hard is that going to be? Not hard at all; for example, selecting the entire document is as easy as calling the Range method:

Set objRange = objDoc.Range()

Note that we don’t pass any parameters to the Range method; doing so would actually limit our selection. (For example, we could tell the Range method to select only characters 1 through 100 in the document.) Calling Range without any parameters puts the entire document (and thus all of our text) into a Range object we called objRange. At that point we can then specify a new font name and font size using code like this:

objRange.Font.Name = "Arial"
objRange.Font.Size = 10

And there you have it; with any luck all the text in your document will now be 10-point Arial. Not bad for a bunch of old geezers, huh?

Oops; sorry, Jean. Not bad for three old geezers and one young and vibrant editor, huh?


How Can I Change the Paragraph Case in Microsoft Word?

Hey, Scripting Guy! We have a bunch of Word documents that, for some reason, have been formatted using nothing but lowercase letters. We know exactly how we want to reformat these documents: all the headings (which are in 14-point type) should be in title case and all the other paragraphs should be in sentence case. Of course, it’s pretty tedious to reformat these documents by hand; some of them are quite long. How can I use a script to change the paragraph case?

-- JH

Hey, JH. We don’t know if you pay much attention to public education here in the state of Washington (heaven knows that the students don’t seem to pay much attention to public education here in the state of Washington) but the state recently – and proudly – announced that 83.6 percent of the students in the class of 2008 had passed the WASL (Washington Assessment of Student Learning) and were on target to graduate. Congratulations guys; the champagne’s on us!

Of course, that 83.6 percent doesn’t include students who have already dropped out of school, and it doesn’t include students who are still in school but have fallen behind on their credits and will not graduate on time. Oh, and it doesn’t take into account the fact that over one-third of the students who took the WASL actually failed the math portion and thus, according to state law, are not allowed to graduate. (And yet still they couldn’t figure out how to get a 100% graduation rate.)

The Scripting Guys’ favorite part of all this is the math portion of the WASL. More than one-third of the students who took the math portion of the WASL failed. That’s nothing short of disastrous, especially in light of the fact that you have to pass all three parts of the WASL, including math, in order to graduate. So how do you suppose we dealt with an educational disaster of those proportions?

That’s easy: we simply changed the law and said, “Never mind; turns out that you can flunk the math portion of the WASL and graduate after all.” Problem solved.

Besides, who uses math in this day and age?

Judging by the number of bankruptcies, loan defaults, and foreclosures that occur each and every day, no one.

In the interest of full disclosure, we should note that none of the Scripting Guys passed the math portion of the WASL, either, although that’s due, at least in part, to the fact that they didn’t have the WASL back in those days. (Come to think of it, they didn’t have paper or pencils back in those days either.) However, the state legislature has decided to give us a break, provided we could write a script that can reformat paragraph case in a Microsoft Word document. As it turns out, we couldn’t. Fortunately for us, however, and in the spirit of the modern-day student, we were able to steal one off the Internet instead:

Const wdTitleWord = 2
Const wdTitleSentence = 4

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set colParagraphs = objDoc.Paragraphs

For Each objParagraph in colParagraphs
    If objParagraph.Range.Font.Size = 14 Then
        objParagraph.Range.Case = wdTitleWord
    Else
        objParagraph.Range.Case = wdTitleSentence
    End If
Next

As you can see, we (well, OK, whoever actually wrote this script) start out by defining two constants, wdTitleWord (with a value of 2) and wdTitleSentence (with a value of 4). What are we going to do with these two constants? Just a second; we need to start up Internet Explorer.

Um, as we were saying, we’ll use wdTitleWord to apply the title case to all the headings in the document; that’s going to capitalize the first letter of each word in those headings. Meanwhile, we’ll use wdTitleSentence to apply the sentence case to all the other paragraphs in the document. With sentence casing, the first word in a sentence (determined by closing punctuation marks like . or ! or ?) will be capitalized; all the other words in the sentence will remain lowercase.

Admittedly, this is not a foolproof solution. (But don’t blame us; we didn’t write it!) For example, suppose you have the following sentence in your document:

i live in new york city.

After we apply sentence casing, that same sentence will look like this:

I live in new york city.

That’s better, but it’s not perfect; after all, that should be New York City, not new york city. But this is about the best we can hope for.

After we define our constants we create an instance of the Word.Application object and set the Visible property to True; that gives us a running instance of Microsoft Word that we can see on screen. We then use this line of code (and the Open method) to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

In case you’re wondering, that document looks like this:

See all those lowercase letters everywhere you look? That’s what we need to fix.

In order to do that, our next step is to use this line of code to return a collection of all the paragraphs in our document:

Set colParagraphs = objDoc.Paragraphs

Once we’ve done that we then use this line of code to set up a For Each loop to walk us through each paragraph in that collection:

For Each objParagraph in colParagraphs

Still with us? Good. Because now we’re ready for the fun part.

Inside the loop, we use the following line of code to determine whether the paragraph (or, more correctly, the paragraph’s Range) uses a font size of 14 points:

If objParagraph.Range.Font.Size = 14 Then

Suppose the paragraph does use a 14-point font. Well, with this particular document, that can mean only one thing: this paragraph is a heading. Therefore, we set the value of the Range object’s Case property to the constant wdTitleWord, something that applies the title case to the paragraph:

objParagraph.Range.Case = wdTitleWord

And what if the paragraph doesn’t use a 14-point font? No problem; in that case we set the value of the Case property to the constant wdTitleSentence, which applies the sentence case to the paragraph:

objParagraph.Range.Case = wdTitleSentence

And then we loop around and repeat the process with the next paragraph in the collection. By the time we’re finished looping our document should look like this:

Which is just exactly what we want it to look like.

We hope that answers your question, JH. Incidentally, if you or anyone else out there is thinking about hiring some new employees sometime next summer, well, we’d strongly recommend choosing a high school graduate from the state of Washington. Would these new graduates do the job really well? Beats us. On the other hand, these kids almost all flunked math; that means you can pay them anything you want a get away with it:

“No, $11 a day is more than fair. Do you have any idea how big a number 11 really is? That’s what we thought.”

Just something to keep in mind.

Editor’s Note: If today’s column sounded familiar to you, you’re obviously a devoted reader of the Hey, Scripting Guy! column – and much farther from senility than the Scripting Guy who writes this column.


How Can I Clear All the Formatting From a Microsoft Word Document?

Hey, Scripting Guy! How can I clear all the formatting from a Microsoft Word document?

-- TF

Hey, TF. After a much-deserved holiday hiatus we are back, with the first official Hey, Scripting Guy! column for the year 2007. And yes, we know: technically we did have a column yesterday. As it turns out, however, that column was actually written a few weeks ago, before the Scripting Guy who writes this column left on vacation. Because of that, we don’t really consider it to be the first official column for the year 2007. If you looked at yesterday’s column we ask that you forget everything you read there.

And no, for most of you that won’t be much of a problem, will it?

Seeing as how this is the first official column for the year 2007, do we have plans to make today’s article extra-special and worthwhile? You bet we do; unfortunately, though, none of the other Scripting Guys wrote an extra-special , worthwhile article while the Scripting Guy who writes this column was away. Therefore, instead of anything extra-special you’re getting a simple little script that we scraped together at the last possible minute. Which, if nothing else, means that you don’t have anything to worry about: Hey, Scripting Guy! is still the same as it ever was!

Note. Speaking of which, did anyone watch the Fiesta Bowl game between Boise State and Oklahoma? Holy smokes! The stupid “Bowl Championship Series” aside, you simply have to love college football.

And yes, you do have to love it. That’s just the way it goes.

Now, where were we? Ah, yes: a script that removes all the formatting from a Microsoft Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

objDoc.Select

Set objSelection = objWord.Selection
objSelection.ClearFormatting

You can see why we chose this topic on our first day back to work: there really isn’t much to this thing at all. We start off by creating an instance of the ever-popular Word.Application object, then set the Visible property to True; that gives us a running instance of Microsoft Word that we can see on screen. As soon as Word is up and running we then use the Open method to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

As you might expect, Test.doc is a document absolutely rife with fancy formatting:

Remember, we did say fancy formatting, not beautiful formatting.

As it turns out, Microsoft Word includes a built-in command that enables you to remove all the formatting applied to the selected text. What does that mean for us? For one thing, that means that, in order to remove the formatting from the entire document we first need to select the entire document. That’s something we can do by calling the Document object’s Select method, which – when called without any additional parameters – selects the entire document:

objDoc.Select

After we’ve selected all the text we just need to do two more things: create an instance of Word’s Selection object, then call that object’s ClearFormatting method. That requires just two additional lines of code:

Set objSelection = objWord.Selection
objSelection.ClearFormatting

So what’s the net result? This is the net result:

Not bad for the first day back, if we do say so ourselves.

Speaking of the first day back, that’s a good question: is the Scripting Guy who writes this column excited about coming back to work after three weeks of lounging around watching football and basketball? Let’s put it this way: he’s every bit as thrilled about and dedicated to his job as he was before he went on vacation.

We’ll let you figure out what that means. Like they say, we report; you decide.


How Can I Convert 1,000 .RTF Files to Word Documents?

Hey, Scripting Guy! I have a folder of about 1,000 .RTF files. I need to convert all of these to Word documents (.DOC files). Can you help me with this?

-- LS

Hey, LS. Sure, we can help you. We’re not going to, but we could if we wanted to.

No, wait, come back: we’re just kidding. Not only can we help you, but we will help you. To prove it, let’s start by showing you a script that can open a single .RTF file (C:\Scripts\Test.rtf) and then create a Word version of this document:

Const wdFormatDocument = 0

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open("c:\scripts\test.rtf")
objDoc.SaveAs "C:\Scripts\test.doc", wdFormatDocument

objWord.Quit

See how easy that is? We begin by creating a constant named wdFormatDocument and assigning it the value 0; this happens to be the value that represents a standard Word document. We create an instance of the Word.Application object, then use the Open method to open the document C:\Scripts\Test.rtf.

So how do we convert this document to Word format? Well, technically, we don’t: we aren’t going to actually convert the document, we’re going to save a copy of it in Word format. When we’re done we’ll have two files: Test.doc and Test.rtf. If you don’t really want two files, you could add a couple lines of code and have the script delete Test.rtf.

Here’s the line of code that creates Test.doc. As you can see, we call the SaveAs method, passing as parameters the new file path (C:\Scripts\Test.doc) and the constant wdFormatDocument (which, again, tells Word to save the file as a Word document):

objDoc.SaveAs "C:\Scripts\test.doc", wdFormatDocument

We then call Word’s Quit method (to dismiss Word) and we’re done. Just that easy, just that quick.

Of course, while we might be done, you’re not: after all, you have an entire folder full of files to convert. Can you get a script to convert all those files for you? You bet:

Const wdFormatDocument  = 0

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

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

Set objWord = CreateObject("Word.Application")

For Each objFile in colFiles
    If objFile.Extension = "rtf" Then
        strFile = "C:\Scripts\" & objFile.FileName & "." & objFile.Extension
        strNewFile = "C:\Scripts\" & objFile.FileName & ".doc"
        Set objDoc = objWord.Documents.Open(strFile)
        objDoc.SaveAs strNewFile, wdFormatDocument
        objDoc.Close
    End If
Next

objWord.Quit

We won’t spend a lot of time detailing this script, but we will at least give you an overview of what it does. We begin by connecting to the WMI service and then retrieving a list of all the files found in the C:\Scripts folder. That’s what this crazy-looking query does:

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

We create an instance of the Word.Application object, and then set up a For Each loop and walk through the collection, checking each file to see if the Extension property is equal to RTF (note that’s just the letters RTF, without a period). Each time we find a .RTF file, we use this line of code to construct the file path and store it in the variable strFileName:

strFile = "C:\Scripts\" & objFile.FileName & "." & objFile.Extension

We then construct a name for our Word version of this file using this line of code:

strNewFile = "C:\Scripts\" & objFile.FileName & ".doc"

Got that? If we found a file named Test.rtf, the variable strFileName now holds this value: C:\Scripts\Test.rtf. Likewise, the variable strNewFile will now hold this value: C:\Scripts\Test.doc. We can then use the code we showed you at the beginning of this column, the code that opens a .RTF file and saves it as a .DOC file. The only difference? Instead of hard-coding in file names we use our variables strFile and strNewFile:

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open(strFile)
objDoc.SaveAs strNewFile, wdFormatDocument
objDoc.Close

Note that, after calling the SaveAs method, we use the Close method to get rid of Test.rtf but leave Word running. We then loop around and look for the next RTF file. After we’ve finished going through the entire collection, we exit the loop and then use the Quit method to terminate our instance of Word. We do it this way to keep from having to repeatedly start up Word, terminate it, and then restart when we find another .RTF file. That process would actually work, but would definitely slow the script down.

Speaking of which …. Based on both the number of files and the size of those files, this script can take a little while to run. In our tests, it took about 45 seconds to open and save three different .RTF files, each about 4 megabytes in size. So, plan accordingly.


How Can I Create a Table and Fill the First Column With a Range of Dates?

Hey, Scripting Guy! How can I create a table and automatically fill the first column in that table with a range of dates?

-- DG

Hey, DG. You know, back in the old days we would have been a bit nervous about answering this question. Why? Well, it seems like Word should have a built-in command for doing something like this; however, we couldn’t find one. (Not that we looked all that hard.) However, we did come up with a script that, while maybe not the most elegant solution in the world, will do the trick.

So why does that make us nervous? Well, every now and then we’ve answered a question like this one only to receive a barrage of email from people saying, “You know, instead of using 100 lines of crazy workaround code you could have done this in just a couple lines by calling the AutoCreateCalendar method.” That was a problem, not only because it hurt our feelings (no, not really; Scripting Guys don’t have feelings) but because everyone was right: we could havedone things better/faster/easier. Unfortunately – and for reasons we won’t bother discussing – there wasn’t much we could do with these better/faster/easier solutions we received.

Now, of course, we have the Community-Submitted Scripts Center. As you read today’s column, if you find yourself thinking, “Man, I have a much better way to solve this problem,” then by all means submit your solution to the Community Center. Trust us, the Scripting Guys do not believe that we have the answers to everyone’s problems.

Well, at least not to everyone’s scripting problems.

So what kind of solution did we come up with? This kind of solution:

Const NUMBER_OF_ROWS = 1
Const NUMBER_OF_COLUMNS = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

Set objRange = objDoc.Range()
objDoc.Tables.Add objRange, NUMBER_OF_ROWS, NUMBER_OF_COLUMNS
Set objTable = objDoc.Tables(1)

objTable.Cell(1, 1).Range.Text = "Date"
objTable.Cell(1, 2).Range.Text = "Notes"

dtmDate = #4/1/2006#
dtmMonth = Month(dtmDate)
i = 2

Do While True
    objTable.Rows.Add()
    objTable.Cell(i, 1).Range.Text = dtmDate
    dtmDate = dtmDate + 1
    If Month(dtmDate) <> dtmMonth Then
        Exit Do
    End If
    i = i +1
Loop

Before we start we should mention that very little of this code has to do with adding dates to a table; the vast majority of it involves creating the table in the first place. We won’t delve into the whys and wherefores of table creation today. But don’t despair: after all, we have an Office Space article that will fill you in on how to create a table in Microsoft Word.

Without going into any detail, the script starts out by creating a new, visible instance of the Word.Application object, then uses the Add method to open a blank Word document. We then add a new table (one row, two columns) to this document, putting the heading Date in column 1 and the heading Notes in column 2. (Why? Because DG needs a table that looks like this: the date in one column and space to type notes in the second column. We should also add that DG needs to do this for a month at a time, e.g., he needs a row in the column for each and every day in the month of April.)

That brings us to these three lines of code:

dtmDate = #4/1/2006#
dtmMonth = Month(dtmDate)
i = 2

In line one we simply assign the first day of the month (4/1/2006) to a variable named dtmDate. We then use the Month function to assign the month (April) of the starting date to a variable named dtmMonth. As you’ll soon see, we’ll use this variable to help determine when we’ve reached the end of the month.

Oh, yeah: what about line 3? In line 3 we assign the value 2 to the counter variable i. We’ll use this variable to keep track of which row number in the table we’re working with. We assign the variable the value 2 because that’s the row where we want to put the first day of the month; remember, we’ve already put headings in the first row of our table.

And now we run headlong into this block of code:

Do While True
    objTable.Rows.Add()
    objTable.Cell(i, 1).Range.Text = dtmDate
    dtmDate = dtmDate + 1
    If Month(dtmDate) <> dtmMonth Then
        Exit Do
    End If
    i = i +1
Loop

What we’re doing here is setting up a Do loop that will run until we reach the end of the month. Inside that loop we call the Add method to add a new row to the table; we then use this line of code to write the value of our starting date to column 1 of this new row:

objTable.Cell(i, 1).Range.Text = dtmDate

That’s pretty easy; now it gets just a tiny bit trickier. Having written April 1, 2006 to the table we now need to write April 2, 2006 to the next row in the table. To do that we start off by adding one day to the variable dtmDate; that makes the value of dtmDate equal to April 2, 2006:

dtmDate = dtmDate + 1

That’s good. But suppose dtmDate was equal to April 30, 2006. In that case, adding a day would make dtmDate equal to May 1, 2006, and we don’t want any May dates in our table. Instead, if we’ve added rows for each day in April we’re done. How do we handle that situation? Here’s how

If Month(dtmDate) <> dtmMonth Then
    Exit Do
End If

Here we’re checking to see if the month of our current date (dtmDate) is still equal to our starting month of April (which, as you recall, we stashed in the variable dtmMonth). If it is, then we’re going to increment the counter variable i by 1, loop around, and add a new row to the table. If the two months are different, however, that means we’re run out of days in April. In that case, we use the Exit Do command to exit the Do loop and the script comes to and end.

Will this really give us a table that includes a row for each day in the month of April? Give it a try and see for yourself. And if you can figure out a better way to accomplish this task, then please send your solution to the Community-Submitted Scripts Center. By our calculations that would make you one of just 950 million people who’ve figured out how to do something better/faster/easier than the Scripting Guys. Select company indeed!


How Can I Determine Which Version of Word is Installed on a Computer?

Hey, Scripting Guy! How can I determine which version of Word is installed on a computer?

-- RR

Hey, RR. Turns out that this is a trickier question than you might expect. That’s because WMI does only a so-so job of retrieving information about the software installed on a computer. In theory, you can use a script like this one to get back version information for Microsoft Office:

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

Set colApps = objWMIService.ExecQuery _
    ("Select * from Win32_Product Where Caption Like '%Microsoft Office%'")
For Each objApp in colApps
    Wscript.Echo objApp.Caption, objApp.Version
Next

So why not just use that script? Well, two reasons. First, this script uses the Like operator; that operator is available only on Windows XP and Windows 2003. (And, just to complicate things, the Win32_Product class is not installed by default on Windows 2003.) If you’re still running Windows 2000, you’re out of luck. Instead, the best you could do is retrieve all the instances of the Win32_Product class, then check each instance to see if the caption includes the term Microsoft Office.

Second, the preceding query returns information for any piece of software that has the term Microsoft Office somewhere in its name. You’ll get the scoop on Microsoft Office Professional Edition 2003 all right, but you’ll also get back information for applications like Microsoft Producer for Microsoft Office PowerPoint 2003. Consequently, you might have to further modify this script to ensure that you get back only information about the “real” Microsoft Office.

And before you ask: yes, version information is stored in the registry, but, sadly, it’s not always clear where in the registry to get this information. The location depends on the version of Office installed on a machine, as well as whether this was a clean install or an upgrade of an earlier version of Office. Which means that querying the registry to find out what version of Word is installed on a computer can also be a bit tricky, particularly in organizations where you are likely to have computers running different versions of Office.

So then how the heck do you determine the version of Word installed on a computer? Well, after giving it a bit of thought, we decided the best way was to just ask Word. Here’s a script that creates an instance of Microsoft Word, echoes back the version and build numbers, and then terminates that instance of Word:

Set objWord = CreateObject("Word.Application")
Wscript.Echo "Version: " & objWord.Version
Wscript.Echo "Build: " & objWord.Build
objWord.Quit

Four little lines of code, and pretty much guaranteed to work. On top of that, the instance of Word you create runs in an invisible window, so you won’t even see Word pop up on the screen. What more could you ask for?

If you want to find out the version of Word installed on a remote computer, just add that computer name as the optional second parameter passed to CreateObject. This modified script grabs the version of Word installed on the remote computer atl-ws-01:

Set objWord = CreateObject("Word.Application", "atl-ws-01")
Wscript.Echo "Version: " & objWord.Version
Wscript.Echo "Build: " & objWord.Build
objWord.Quit

Sure, it might seem like overkill, but ultimately this approach should prove easier and more reliable than using Win32_Product or trying to grab version information from the registry.


How Can I Extract Word Paragraphs That Use a Specific Style?

Hey, Scripting Guy! How can I save (to a separate file) all the paragraphs in a Word document that are formatted using a particular style, like Heading 1?

-- RN

Hey, RN. You know, when we first read this question we thought, “Interesting. But why would you ever want to do something like that?” And then one of the Scripting Guys remembered the many…happy…hours he spent one week going through Word documents and manually copying and pasting all the headings, to create a master list of topics. Once we had remembered that, we thought, “You know, it would have been really nice to have a had a script like this back then. But because we had to suffer through that chore without a script, why shouldn’t everyone have to suffer through that chore without a script?”

But then someone else made a good point: while the Scripting Guys probably deserve to suffer, most script writers don’t. Therefore, we decided to come up with a script that can pull out selected paragraphs from a Word document. Here’s a simple little script that checks each paragraph to see if it uses the Heading 1 style. If the paragraph uses that style, the script leaves it alone; if the paragraph doesn’t use that style, the script deletes it.

Why? Well, this seemed like the easiest way to handle the issue: instead of copying Heading 1 paragraphs from one document to another, we just delete everything that isn’t a Heading 1 from the document.

When we’re all done, we then use the SaveAs method to save the file under a separate file name. The net result? We still have the original document, plus we have a new document that contains only the paragraphs formatted as Heading 1.

Here’s the script:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set objSelection = objWord.Selection
Set colParagraphs = objDoc.Paragraphs

For Each objParagraph in colParagraphs
    If objParagraph.Style <> "Heading 1" Then
        objParagraph.Range.Select
        objSelection.Cut
    End If
Next

objDoc.SaveAs("C:\Scripts\Headings.doc")
objWord.Quit

We begin by creating an instance of the Word.Application object and then set the Visible property to True; we do that just so everything that happens will be visible on screen. We then use the Open method to open the file C:\Scripts\Test.doc.

Note. Needless to say, if you decide to try this script your results will be far more interesting if Test.doc actually includes a few paragraphs that use the Heading 1 style.

We create an instance of the Word Selection object and then use this line of code to create an object reference to our document’s Paragraphs collection:

Set colParagraphs = objDoc.Paragraphs

As you probably guessed, the Paragraphs collection contains all the paragraphs found in our document. Next we set up a For Each loop to loop through this collection. For each paragraph in the collection (and thus for each paragraph in the document) we use this line of code to see if the paragraph uses the Heading 1 style:

If objParagraph.Style <> "Heading 1" Then

If the paragraph does use the Heading 1 style, we simply loop back around and check the next item in the collection. But what if the paragraph doesn’t have the Heading 1 style (that if, what if our If statement is true)? In that case we use these two lines of code to select the paragraph and then cut it from the document:

objParagraph.Range.Select
objSelection.Cut

After we’ve looped through the entire collection – and deleted all the paragraphs that don’t use the Heading 1 style – we’ll be left with nothing but Heading 1 paragraphs. At that point all we have to do is call the SaveAs method and save the document under a new name (in this case, C:\Scripts\Headings.doc) and then exit Word. And that’s what we do with these two lines of code:

objDoc.SaveAs("C:\Scripts\Headings.doc")
objWord.Quit

And, yes, we do feel better for having shared this with the world. (Although we still would have liked to make you suffer a little. But just a little, mind you.)

Incidentally, if this whets your appetite for Microsoft Office scripting, be sure and check out the Office Space column.


How Can I Get a List of the Unique Words Used in a Microsoft Word Document?

Hey, Scripting Guy! How can I get a list of the unique words used in a Microsoft Word document?

-- RK

Hey, RK. Funny you should mention unique words. Last Saturday the Scripting Coach’s baseball team played in the city championship. Despite the importance of the game the team was missing two key players, and the Scripting Coach knew that meant that his infield would be a little weak defensively. What he didn’t realize was just how weak it would be. In the bottom of the second the opposition’s leadoff hitter singled. That would be the last ball that would leave the infield in that inning, yet the opposing team managed to score 6 runs. (How? By having seven consecutive batters reach base on an infield error). As you might expect, the Scripting Coach’s team never really recovered after that.

Needless to say, a number of words ran through the Scripting Coach’s head during that disastrous second inning, with many of those words being very … unique ….

Well, except to other baseball coaches, of course.

Ah, but you don’t want to hear about the city championship game, do you? (Which is good, because this Scripting Guy doesn’t want to talk about it, either.) You’d rather talk about a script that could produce a list of the unique words used in a Microsoft Word document. You know, a script similar to this:

Set objDictionary = CreateObject("Scripting.Dictionary")

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Sample.doc")

Set colWords = objDoc.Words

For Each strWord in colWords
    strWord = LCase(strWord)
    If objDictionary.Exists(strWord) Then
    Else
        objDictionary.Add strWord, strWord
   End If
Next

Set objDoc2 = objWord.Documents.Add()
Set objSelection = objWord.Selection

For Each strItem in objDictionary.Items
    objSelection.TypeText strItem & vbCrLf
Next

Set objRange = objDoc2.Range
objRange.Sort

Let’s see if we can figure out how this script works. As you can see, we start out simple enough: we just create an instance of the Scripting.Dictionary object. (Of course, everything starts simple, just like the second inning of the city championship game did.) In just a moment we’ll use the Dictionary object as a place to store all the unique words in the document. We then use these three lines of code to create a visible instance of the Word.Application object and open the document C:\Scripts\Sample.doc:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Sample.doc")

That was easy, wasn’t it? (Apparently much easier than catching a lazy little pop fly.) With our document open we’re now ready to grab a list of the unique words. To do that, we first need to get a list of all the words. That sounds like a complicated procedure, but, fortunately, it’s not; that’s because all the words in a Microsoft Word document are stored in the document’s Words collection. That’s a collection we can retrieve using just one line of code:

Set colWords = objDoc.Words

Our next step is to weed out all duplicate words in the collection; that will leave us (and RK) with a list of unique words. For example, suppose our Word document contains the following words:

these
words
are
the
words
in
the
document

When we weed out all the duplicates (like multiple instances of the words word and the) we’re left with this:

these
words
are
the
in
document

Which is the very thing RK is hoping to get.

To weed out the duplicate words we use this block of code:

For Each strWord in colWords
    strWord = LCase(strWord)
    If objDictionary.Exists(strWord) Then
    Else
        objDictionary.Add strWord, strWord
   End If
Next

As you can see, we start by setting up a For Each loop to loop through the collection of words in the document. Inside the loop we examine each word individually, using the LCase function to convert the word to all lowercase letters. (Why? Well, that helps us avoid any problems like having the words Cat, cat, and CAT being marked as different words.)

After the word has been converted to lowercase we then use the Exists method to determine whether or not the word is already in the Dictionary:

If objDictionary.Exists(strWord) Then

If the word is already in the Dictionary the script simply loops around and repeats this process with the next word in the collection. If the Word is not in the Dictionary then we use this line of code to add the word (specifying the same value – the word itself – as both the Dictionary item and Dictionary key):

objDictionary.Add strWord, strWord

Note. Sorry; we thought that the Dictionary object was like the Heimlich Maneuver, something everyone already knows. If you aren’t familiar with the Dictionary object and how to use it, take a peek at the Microsoft Windows 2000 Scripting Guide.

What do you mean you don’t know Heimlich Maneuver, either? Fine; we’ll see if we can locate an email address for Hey, Heimlich Maneuver Guy!

After we’re done with the loop the unique words in the document will be safely stashed in the Dictionary. If we wanted to, we could simply echo back those values; that requires no more code than this:

For Each strItem in objDictionary.Items
    Wscript.Echo strItem
Next

We thought we’d go one better than that, however, and add these words – in alphabetical order – to a brand-new Word document. To do that we need to create a new document (notice the new object reference, objDoc2) and then create an instance of the Word Selection object, which simply positions the cursor at the beginning of the document:

Set objDoc2 = objWord.Documents.Add()
Set objSelection = objWord.Selection

Once we’ve done that we can then loop through the items in the Dictionary, using the TypeText method to add the word (plus a carriage return-linefeed) to the document:

For Each strItem in objDictionary.Items
    objSelection.TypeText strItem & vbCrLf
Next

That gives us a Word document that looks something like this:

these
words
are
the
in
document

What’s that? Alphabetical order? No one ever said anything about – oh, that’s right, we did say we’d sort these words in alphabetical order, didn’t we? OK, that’s easy enough:

Set objRange = objDoc2.Range
objRange.Sort

That’s all we have to do. We create a new instance of the Range object; because we provided no additional parameters the new range will, by default, encompass the entire document. And then we call the Sort method; when we call Sort without any parameters we get the items sorted in alphabetical order. Just like this:

are
document
in
the
these
words

Pretty slick, huh?

We should point out that you might get a few anomalies in your list of unique words: that’s because Microsoft Word considers some crazy things – like periods – to be words. If you don’t want punctuation marks to be tagged as words you can add code to, say, weed out anything that doesn’t start with a letter. We won’t discuss this revised script; we’ll just note that it uses the ASC function to filter out anything that doesn’t start with a letter:

Set objDictionary = CreateObject("Scripting.Dictionary")

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Sample.doc")

Set colWords = objDoc.Words

For Each strWord in colWords
    strWord = LCase(strWord)
    strLetter = Left(strWord, 1)
    If ASC(strLetter) < 97 OR ASC(strLetter) > 122 Then
    Else
        If objDictionary.Exists(strWord) Then
            Else
            objDictionary.Add strWord, StrWord
       End If
    End If
Next

Set objDoc2 = objWord.Documents.Add()
Set objSelection = objWord.Selection

For Each strItem in objDictionary.Items
    objSelection.TypeText strItem & vbCrLf
Next

Set objRange = objDoc2.Range
objRange.Sort

As for baseball, after Saturday’s debacle both the Scripting Coach and his Scripting Son vowed that they were through with the sport forever. Of course, that night another team called the Scripting Son and asked if he’d be willing to join their squad for the remainder of the season. And, needless to say, the Scripting Coach said he’d be willing to help out if the team needed his help, too.

But other than that, both father and son are through with baseball. Forever.

Or at least until Fall baseball starts up in August.


How Can I Get a Total Page Count for All the Word Documents in a Folder?

Hey, Scripting Guy! How can I get a total page count for all the Word documents in a folder?

-- RA

Hey, RA. You’ll have to forgive the Scripting Guy who writes this column if he seems a little tired this morning. Yesterday was the first day of school in the Lake Washington School District and, as you might expect, the scene in the Scripting Household was one of utter chaos: the kicking and screaming, the tears and tantrums, the sudden onset of beriberi, which made it impossible to leave the house. All in all, it made for a very long and very tiring morning.

Of course, now that we think about it, all the screaming and crying seemed to emanate from the Scripting Dad, who didn’t want to go to work. The Scripting Son calmly got out of bed and went to school, like he always does.

Despite the tears, and despite the beriberi, the Scripting Dad knew he had no choice but to come in to work. After all, the world (or at least you, RA) was depending on him; someone had to come up with a script that could give you a total page count for all the Word documents in a folder:

Const wdStatisticPages = 2

strComputer = "."

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

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

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

For Each objFile in colFiles
    If objFile.Extension = "doc" Then
        Set objDoc = objWord.Documents.Open(objFile.Name)
        intPages = intPages + objDoc.ComputeStatistics(wdStatisticPages)
        objDoc.Saved = True
        objDoc.Close
    End If
Next

Wscript.Echo "Total pages: " & intPages
objWord.Quit

Note. Does the Scripting Guy who writes this column really have beriberi? Well, probably not. On the other hand, take a look at some of the symptoms of beriberi:

► Apathy
► Drowsiness
► Poor mental concentration

Quick, someone call a doctor right away!

OK; now that the script is written let’s see if we can figure out how it works. To begin with, we define a constant named wdStatisticPages and set the value to 2; we’ll use this constant to retrieve the number of pages in each Word document we find.

Note. Are there other statistics we can retrieve for a Word document? You bet there are; for more information, see one of our most-beloved Office Space columns.

After defining the constant we connect to the WMI service on the local computer and then use this line of code to retrieve a collection of all the files found in the folder C:\Scripts:

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

Next we – what’s that? Can you perform this same task against a remote computer? As a matter of fact you can, provided that Microsoft Word is installed on that remote computer. In order to pull off that trick you need to do two things. First, you need to assign the name of that remote machine to the variable strComputer; for example:

strComputer = "atl-fs-01"

That enables us to connect to the WMI service on the remote computer.

Second, you need to create an instance of the Word.Application object on the remote machine. In the sample script we’re showing you today we’re working with Word documents on the local computer; therefore we use this line of code to create an instance of Word:

Set objWord = CreateObject("Word.Application")

However, if you want to work with Word documents on a remote computer you need to include the computer name as the optional second parameter to the CreateObject method:

Set objWord = CreateObject("Word.Application", "atl-ws-01")

Got that? Good.

OK, so where were we? Oh, yeah: we create an instance of the Word.Application object (in this case, an instance of Word on the local computer) and then set the Visible property to True. That, by the way, is optional: we don’t need Word to be visible onscreen in order to add up the number of pages in the various Word documents. However, this does give a visual cue that something is going on. Leave that line of code in or take it out; that’s entirely up to you.

Now it’s time to get serious. To begin with we set up a For Each loop to loop through the collection of files found in the folder C:\Scripts. The first thing we do inside that loop is check to see whether we’re working with a Word document (that is, a file with a .doc file extension):

If objFile.Extension = "doc" Then

Note. Yes, thanks for pointing that out: do not include the period when specifying a file extension in WMI. It’s doc, not .doc.

Let’s say that we do have a Word document. What then? Well, for starters, we use this line of code to open the document:

Set objDoc = objWord.Documents.Open(objFile.Name)

We can do this because the Name property gives us the complete path to the file. With the document open we can then use the ComputeStatistics method to determine the number of pages in the file:

intPages = intPages + objDoc.ComputeStatistics(wdStatisticPages)

As you can see, we’re using a variable named intPages to keep track of the total number of pages. What we’re doing here is determining the number of pages in our first document, something we do by calling ComputeStatistics and passing it the constant wdStatisticPages. We take the number of pages in document 1, add that to the current value of intPages, and then assign the sum of those two numbers to intPages.

Why? Well, the first time through the loop intPages will be equal to 0; that’s because we haven’t assigned it a value yet. Let’s assume that document 1 has 5 pages. That means our equation will look like this:

intPages = 0 + 5

That makes intPages equal to 5. Now, suppose we loop around and find a second Word document, this one containing 13 pages. That means our next step is to add those 13 pages to the current value of intPages (5). In other words:

intPages = 5 + 13

The variable intPages will now be equal to 18 which – magically enough – also equals the total number of pages we’ve found so far. Clever, huh?

Well, OK: kind of clever.

After we get the total number of pages for the first Word document we set the Saved property to True, which ensures that Word won’t ask us if we want to save changes to the document; we then close document 1. After that we loop around and repeat this process with the next file in the collection.

When we’re all done we use these two lines of code to echo back the total number of pages found and to terminate our instance of Microsoft Word:

Wscript.Echo "Total pages: " & intPages
objWord.Quit

That should do the trick.

You know, that’s a good point: beriberi does sound pretty serious, doesn’t it? So then why did the Scripting Dad come in to work? To be honest, he had at least 200 reasons: $70 for the athletic participation fee; $40 for an ASB card; $40 for a yearbook; $50 for a parking pass.

Etc.

Needless to say, we’ll see you tomorrow.


How Can I Hide a Specific Toolbar in Microsoft Word?

Hey, Scripting Guy! How can I hide a specific toolbar in Microsoft Word?

-- AG

Hey, AG. Now this question brings back memories. When our editor joined the team one of her first actions was to make us redo a whole bunch of screenshots we had made for the Scripting Week 2 webcasts. Why? Because she didn’t think we should include screenshots that showed a custom Microsoft Word toolbar we use when working with content for the Script Center. Thus we had to open Word, hide the toolbar, and redo all the screenshots. If we’d had a script like the one you’re looking for, we wouldn’t have had to manually open Word and hide the toolbar. Granted that wouldn’t have been a huge time savings, but, still….

Note. We kid our editor from time-to-time, but, really, we have no complaints about her whatsoever. Well, OK, it would be nice if she’d only use one parking space when parking her broomstick each morning, but other than that….

But let’s get down to business. You want a script that can hide a specific toolbar in Microsoft Word, right? Well, how about this simple little script, which hides the Formatting toolbar:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objTool = objWord.CommandBars("Formatting")
objTool.Visible = False

As you can see, it’s a simple task – hide a toolbar – and an equally simple script. We begin by creating an instance of the Word.Application object and then setting the Visible property to True (just so we can see what’s going on). We then use this line of code to create an object reference to the Formatting toolbar, which is part of the Word CommandBars collection:

Set objTool = objWord.CommandBars("Formatting")

To hide the toolbar we simply set the toolbar’s Visible property to False. If we wanted to show the toolbar all we’d have to do is set the Visible property to True.

No doubt one question that immediately pops into mind is this: how did we know this particular toolbar has the name Formatting? Well, if you click the View menu in Word and then click Toolbars you’ll see a list of the toolbars available. The names shown in the menu are the same names you use in your scripts.

Note. Couldn’t we just enumerate all the items in the CommandBars collection to retrieve a list of toolbar names? Yes, we could. Unfortunately, though, the CommandBars collection also includes menus, shortcut menus, and submenus, and there doesn’t appear to be a way to programmatically separate toolbars from these other items. Therefore your best bet is just to open Word and take a look at the toolbar names.

The script we showed you actually opens up Word, hides the toolbar, and then leaves Word open. If you’d rather just configure the toolbar setting without seeing anything on screen you can use this script, which starts up Word in a hidden window, hides the toolbar, and then exits the program. You won’t see anything happen on screen, but the next time you start Word the Formatting toolbar will be hidden:

Set objWord = CreateObject("Word.Application")
Set objTool = objWord.CommandBars("Formatting")
objTool.Visible = False
objWord.Quit

One final point we should make is that – as we’ve seen – it’s possible to hide a toolbar in Word. However, the toolbar will still be available: it just won’t be visible at the moment. For better or worse (probably for better) you can’t delete any of Word’s built-in toolbars; the best you can do is hide them from view.

Anyway, thanks for your question AG. And now we have a question for you. We’d like to get our editor a small token of our appreciation, but we have no idea where to find eye of newt in the Seattle area. Any ideas?

Editor’s Note. The preceding was a work of fiction (with the exception of the scripting material). Any resemblance to editors past or present is purely coincidental. Although there is no resemblance to editors present. Past, well….


How Can I Insert a Date Field in Word?

Hey, Scripting Guy! How can I insert a field in a Word document that shows the date two weeks from today?

-- HO

Hey, HO. As you’re reading this the Scripting Guy who writes this column is no doubt sitting in the bowels of the King County Superior Court, waiting to hear whether he actually gets assigned to a jury or not. We realize that many of you have never experienced the excitement of serving on a jury. With that in mind, here’s a recap of our hero’s first day as a Superior Court juror.

The Scripting Guy who writes this column woke up at 5:30 AM, looked out the window, and saw that it was snowing like crazy outside. Hoping for a postponement, he called the jury duty hotline to see if there were any weather-related delays. When there weren’t any, he sighed, took a shower, got dressed, and left the house a little after 6:00 AM; that was the only way he could be sure and reach downtown Seattle by 8:00 AM.

He arrived at the courthouse around 7:45 and, as instructed, went straight to the Juror Assembly room. There he waited with his fellow jurors until someone from Superior Court came out and made an announcement. “Apparently none of you got the message,” he said, “but about 7:45 or so we announced that court would be delayed for 2 hours. So I guess you’ll just have to sit here and wait until 10:00.”

And so the Scripting Guy who writes this column sat there and waited until 10:00. After officially checking in (a process that took only a couple minutes), he then sat there and waited until 12:00, when everyone was dismissed for lunch. After eating a sandwich he went back to the Juror Assembly room and sat there and waited until 2:30, at which time he was assigned to the juror pool for a particular trial. He then sat there and waited for another hour before he and the other members of the pool were instructed to go to the court room on the 7th floor. There they were told, “Well, normally we would do juror interviews at this point, but because we’re running late we’ll have to do those tomorrow. So you’ll all have to come back tomorrow.”

And then he went home.

In other words, as near as the Scripting Guy who writes this column can tell, jury duty involves sitting around bored, going to lunch, then sitting around bored until it’s time to go home. Fortunately, though, he had no problem with that; after all, that’s a pretty good description of his typical day at work.

However, he did have a little trouble figuring out how to insert a field in a Word document, or at least how to insert a field that shows the date two weeks from today. In fact, as near as he could tell there’s no way to do that, not even from within Word itself. But did the Scripting Guy who writes this column give up? You bet he did.

But later on, after finishing both the crossword puzzle and the daily Sudoko, and after reading Dear Abby for the third time, he came up with the following workaround:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set objRange = objDoc.Bookmarks("TwoWeeks").Range
objRange.Text = Date + 14

Because he couldn’t figure out a way to create a field that showed the date two weeks from today he did the next best thing: he inserted a bookmark (named TwoWeeks) into the document, then simply used a little programming wizardry to set the value of that bookmark to the date two weeks from today. A different approach, but the same net effect.

So how does the script work? Well, to begin with, we create an instance of the Word.Application object, then set the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. We then use the Open method to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Important. This script assumes that the document Test.doc already has a bookmark named TwoWeeks. If that bookmark doesn’t exist then the script will fail.

As soon as the document is open we use this line of code to create a Range object that encompasses the bookmark named TwoWeeks:

Set objRange = objDoc.Bookmarks("TwoWeeks").Range

At that point all we have to do is set the Text of the bookmark to the current date plus two weeks (that is, plus 14 days):

objRange.Text = Date + 14

And there you have it.

This actually works pretty well, although there is one drawback: the bookmark will be updated only if you open the document by using the preceding script. That’s because there’s no code within the document itself that will automatically update the bookmark. That may or may not be a problem. If it is a problem you can always create an AutoExec macro inside the document itself. As you probably know, an AutoExec macro runs each time a document is opened; that means that each time the document is opened the bookmark text will be updated.

Without explaining the ins and outs of using VBA (Visual Basic for Applications) inside Microsoft Word, here’s the code for our AutoExec macro:

Sub AutoExec()
    Set objRange = ActiveDocument.Bookmarks("TwoWeeks").Range
    objRange.Text = Date + 14
End Sub

Just the way Dear Abby would write it.

As for the Scripting Guy who writes this column, he’ll find out very soon whether he’s officially fulfilled his civic duty, or whether he gets to sit on a trial that’s expected to last – at a minimum –three weeks. That sure would be … fun …. But we’ll keep you posted; needless to say, we have plenty of time on our hands these days.


How Can I Insert a Manual Line Break into a Microsoft Word Document?

Hey, Scripting Guy! How can I insert a manual line break into a Microsoft Word document?

-- YR

Hey, YR. Greetings (saluti) from Rome, the Eternal City! You know, according to legend, Rome was founded thousands of years ago by the twins Romulus and Remus, along with their distant cousin (and oldest living Scripting Guy) Peter Costantini. Peter, of course, denies this, inisting that he wasn’t even alive when Romulus and Remus founded the city. That might be true. If so, however, then how does he explain the shirt he always wears, the one that says “I co-founded Rome with Romulus and Remus and all I got was this stupid T-shirt”?

Interesting note. According to the Babelfish online translation service, the word Costantini, when translated from Italian to English, means “Costantini.” Strange but true!

We’ll try to figure out who really founded Rome as soon as we get the chance. Right now, however, it’s time for breakfast. So far, the Scripting Guy who writes this column has found Italian food to be quite good. After just a couple of days he’s already had, among other things:

· Bigne, a pastry filled with chocolate or vanilla cream.

· Napoleons, a pastry filled with both chocolate and vanilla cream.

· Sfogliatelle, a pastry filled with a mandarin-flavored ricotta filling.

· Pasticiotti, another pastry filled with chocolate.

· Fruit tarts, éclairs, zeppole, pastiera, cassataSiciliana, zuppette, and profitteroli. All pastries, and all good.

Note. Is there anything to eat in Italy besides pastries? Interesting question; we’ll have to get back to you on that one, too.

In the meantime, and while we wait for our cappuccino, here’s a script that inserts a manual line break into a Microsoft Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.TypeText "This paragraph is followed by a paragraph return."
objSelection.TypeParagraph()

objSelection.TypeText "This paragraph is followed by a line break." & Chr(11)

objSelection.TypeText "This paragraph is also followed by a line break." & Chr(11)

objSelection.TypeText "This paragraph is followed by a paragraph return."
objSelection.TypeParagraph()

Let’s explain how this script works and, along the way, show you a few pictures that should help convince you that it really does work. As you can see, we start out the way most Microsoft Word scripts start out: we create an instance of the Word.Application object and then set the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. Next we call the Add method in order to add a new, blank document to our instance of Word, then use the following line of code to create an instance of Word’s Selection object, something that positions the cursor at the beginning of the document and enables us to start adding text:

Set objSelection = objWord.Selection

The first bit of text we add should look relatively familiar; all we’re doing is using the TypeText method to type a sentence, then using the TypeParagraph() method to add a hard paragraph return (equivalent to pressing the ENTER key on the keyboard) at the end of that sentence:

objSelection.TypeText "This paragraph is followed by a paragraph return."
objSelection.TypeParagraph()

Now, take a look at the next piece of text we type into our document:

objSelection.TypeText "This paragraph is followed by a line break." & Chr(11)

Once again we’re using the TypeText method to add a sentence to the document. However, we aren’t just adding a sentence; we’re also adding Chr(11) to the end of that sentence. What’s Chr(11)? As it turns out, that’s the ASCII code for a line break. Want to add a line break to the end of a sentence in a Microsoft Word document? Then use Chr(11) rather than the TypeParagraph() method.

And then, just for heck of it, we add a couple more paragraphs, one with a line break, one with a paragraph return.

When the script finishes running our Word document will look like this:

Granted, that looks like any other Word document. But notice what happens when we click the button that shows us the formatting marks:

As you can see, lines 1 and 4 have a hard paragraph return after them; by contrast, lines 2 and 3 conclude with a soft paragraph return (a line break). If we center-align just line 2 look what happens:

Why did lines 3 and 4 also get centered? That’s easy: because they are connected using line break characters rather than paragraph returns. Proof positive that the script works!

Breaking news flash. From what we’ve just been told, Romulus and Remus did not co-found the city of Rome. According to the official story (or at least one of many official stories), the two brothers started to found the city together; however, Remus felt that Romulus was making the city walls too low. To prove his point, he proceeded to jump over the walls. Romulus, in turn, did what any self-respecting brother would do: he killed Remus, finished building the city, and then named it after himself.

Needless to say, ever since then none of his fellow Scripting Guys have been brave enough to tell Peter that he’s been building his walls too low. We’ll just have to make do with the walls the way he built them.


How Can I Insert a Symbol into a Word Document?

Hey, Scripting Guy! How can I insert a Wingding symbol into a Word document?

-- RC

Hey, RC. In case you’re wondering, there were two reasons why we decided to answer your question. First, we just liked the phrase insert a Wingding symbol. (No, we don’t know why we liked it, we just did.) Second – and by far most important – this is an easy one, one we can answer with just a few lines of code.

As we’ll see, the code for inserting a symbol into a Word document is trivial; what’s tricky is figuring out which symbol you want to insert (as well as the character code for that symbol). Perhaps the easiest way to do this (although we’ll show you another method at the end of today’s column) is to open up the Symbol dialog box, select the desired font, and then click on the symbol you want to insert:

As you can see, we’ve selected the smiley face. When we do so, the character code for the smiley face is listed in the Character code box. Now that we know the font we want to use (Wingdings) and the character code for our symbol (74), we’re ready to write a script that inserts the smiley face into a Word document:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.InsertSymbol 74, "Wingdings"

We told you the code was trivial. We begin by creating an instance of the Word.Application object, and then set the Visible property to True. We call the Add() method to create a new document, then create an instance of the Word Selection object.

At that point all we need is one line of code in order to insert the smiley face symbol:

objSelection.InsertSymbol 74, "Wingdings"

That’s it: we call the InsertSymbol method followed by the desired character code and the desired font. Run the script, and you should see a smiley face in your document. (It is a miracle, isn’t it?)

If you like using symbols it might be worth your while to create a catalog of symbols and their character codes. Here’s a script that creates a Word document containing all the symbols and their character codes found in the Wingdings font (character codes 32 through 255). You can easily modify the script to give yourself a catalog of symbols for Wingdings2, Wingdings3, Webdings, Bookdings, and any other ding-dang font you want.

Here’s the code:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

For i = 32 to 255
objSelection.TypeText i & " -- "
objSelection.InsertSymbol i, "Wingdings"
objSelection.TypeParagraph()
Next

And here’s what the resulting document looks like:

It’s not quite the IKEA catalog, but it’s close.


How Can I Insert Files into a Word Document?

Hey, Scripting Guy! Awhile back you showed people how they could combine several text files into a single file. I’d like to do the same thing in Word. How can I insert files into a Word document?

-- HK

Hey, HK. Believe it or not it’s actually easier to insert files into a Microsoft Word document than it is to insert a text file into another text file. That’s because the Word Selection object has a method – InsertFile – that does one thing and one thing only: it opens up a specified file and inserts it into the current document. And InsertFile isn’t limited to opening just text files: if it’s a type of file that Word can handle, then InsertFile can open it and insert it in your document.

Let’s take a look at a script that imports a pair of text files (C:\Scripts\Hardware.txt and C:\Scripts\Software.txt):

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

objSelection.TypeText "Hardware Inventory"
objSelection.TypeParagraph()
objSelection.InsertFile("C:\Scripts\Hardware.txt")

objSelection.TypeParagraph()

objSelection.TypeText "Software Inventory"
objSelection.TypeParagraph()
objSelection.InsertFile("C:\Scripts\Software.txt")

Like we said, way easier than trying to combine multiple files in a single text file. We begin by creating an instance of the Word.Application object and setting the Visible property to True; we then add a document and create an instance of the Selection object. That’s what these lines of code are for:

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

That was easy, but now it gets even easier. We want a heading for our hardware inventory so we type he heading Hardware Inventory followed by a paragraph return; that’s what we do here:

objSelection.TypeText "Hardware Inventory"
objSelection.TypeParagraph()

And now we’re ready to insert a file. That takes just one line of code:

objSelection.InsertFile("C:\Scripts\Hardware.txt")

That’s it: you call the InsertFile method followed by the path of the file being inserted into the document. We then add another paragraph return and the heading Software Inventory, and then insert our second file, C:\Scripts\Software.txt. It’s so easy we could sit here all day inserting files into Word documents!

Before we do that, however, let’s show you a handy little variation on the preceding script. This one creates a new Word document and then inserts all the files found in the folder C:\Scripts\Archive. We won’t bother explaining the code line-by-line, but you should be able to figure out most of it yourself:

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

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

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

For Each objFile In FileList
    objSelection.InsertFile(objFile.Name)
    objSelection.TypeParagraph()
Next

That is cool, isn’t it? For more exciting scripts that use Microsoft Office applications take a peek at the Office Space column located elsewhere in the Script Center. (The preceding advertisement was paid for by the Committee to Get More People to Read the Office Space Column.)


How Can I Insert Multiple Files Into a Word Document, Putting a Page Break Between Each File?

Hey, Scripting Guy! On May 3rd you showed us how to merge multiple files in Microsoft Word. I’d like to do that, but put a page break between each file.

-- CE

Hey, CE. You know, when we read your email our first thought was this: no way did we show people how to merge multiple files back on May 3rd 2005. That wasn’t because we didn’t think that the subject matter – merging multiple files into a single Microsoft Word document –sounded like the kind of topic we’d tackle in this column. Instead, it’s just that on most days we don’t do any work, and we couldn’t see why May 3rd would be an exception. But it turns out you were right: we actually did do some work on May 3rd. What were we thinking?

As you no doubt recall (for surely you all memorize each and every Hey, Scripting Guy! column), in that initial column we used a script to import all the files found in the folder C:\Scripts\Archive into a Microsoft Word document. That was fine, except all we did was type a paragraph return between the end of one file and the beginning of the next. Needless to say, if it’s important to know where one file leaves off and the next one begins, one measly little paragraph return probably won’t be of much use. You’re right, CE: inserting a page break between the files is definitely a step in the right direction. And here’s how you do that:

Const wdPageBreak = 7

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

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

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

For Each objFile in FileList
    objSelection.InsertFile(objFile.Name)
    objSelection.InsertBreak(wdPageBreak)
Next

For the most part we’ve simply replicated our script for inserting multiple files; therefore, we won’t go over the code in line-by-line detail. (If you need that information, take a look at the original column.) Instead, we’ll focus on the two changes we made.

To begin with, the script now starts off by defining a constant named wdPageBreak and setting the value to 7. We’ll use this constant to tell Word to insert a page break when we call the InsertBreak method. (As you probably know, Word allows you to insert other kinds of breaks, including column breaks, text-wrapping breaks, and several kinds of section breaks.)

We then have a bunch of code that binds to the WMI service on the local computer, creates a new Microsoft Word document, and then uses an Associators Of query to retrieve a collection of all the files found in the C:\Scripts\Archive folder. Once we have that collection we set up a For Each loop to walk through the collection, using the InsertFile method to insert each and every file.

Here’s the second place where the new script differs from the original. In our initial script, we called the InsertFile method, then used TypeParagraph() to simulate pressing the ENTER key on the keyboard. This time around, our For Each loop looks like this:

For Each objFile in FileList
    objSelection.InsertFile(objFile.Name)
    objSelection.InsertBreak(wdPageBreak)
Next

As you can see, after inserting the first file we call the InsertBreak method, specifying that we want to insert a page break. It’s only after inserting this page break that we loop around and insert the next file in the collection. The net effect: we end up with page breaks between each of the files we imported into our document.

Yes, much better. And seeing as how we’ve already done some work today, we might as well do a little bit more. Here’s a revised script that not only inserts page breaks between each file, but also types in the file path after each page break, formatting that path in 14-point Arial. (The rest of the text is formatted in 10-point Arial.) Here’s what this little bonus script looks like:

Const wdPageBreak = 7

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

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()
Set objSelection = objWord.Selection

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

objSelection.Font.Name = "Arial"

For Each objFile in FileList
objSelection.Font.Size = "14"
objSelection.TypeText objFile.Name
objSelection.TypeParagraph()
objSelection.Font.Size = "10"
objSelection.InsertFile(objFile.Name)
objSelection.InsertBreak(wdPageBreak)
Next

Note. For more information about formatting text in a Word document, take a look at the Microsoft Word articles that have appeared in the weekly Office Space column.

You know, that was kind of fun. Maybe, just for a change of pace, we’ll do some work every May 3rd from now on.

Nah ….


How Can I Insert Text Into an Existing Microsoft Word Bookmark?

Hey, Scripting Guy! How can I insert text into an existing Microsoft Word bookmark?

-- WM

Hey, WM. Believe it or not, this turned out to be the most difficult script the Scripting Guy who writes this column has ever had to create. Not because the script itself is very hard; as you’re about to see, it’s not. Instead, for some bizarre reason the Scripting Guy who writes this column found himself absolutely incapable of typing the word bookmarks, a word used repeatedly in the script. Instead, try as he might, he always mistyped this as booksmarks. Surprisingly, the script failed each time it encountered yet another instance of booksmarks.

And, yes, that does sound like a bug in Microsoft Word, doesn’t it?

Fortunately, the Scripting Guy who writes this column is not only a lousy typist, but he won’t concede defeat, either. And thus, after several days and several thousand typos, he somehow managed to cobble together the following script:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set objNetwork
= CreateObject("Wscript.Network")
strUser = objNetwork.UserName
strDomain = objNetwork.UserDomain
strComputer = objNetwork.ComputerName

Set objRange
= objDoc.Bookmarks("UserBookmark").Range
objRange.Text = strUser
objDoc.Bookmarks.Add "UserBookmark",objRange

Set objRange
= objDoc.Bookmarks("DomainBookmark").Range
objRange.Text = strDomain
objDoc.Bookmarks.Add "DomainBookmark",objRange

Set objRange
= objDoc.Bookmarks("ComputerBookmark").Range
objRange.Text = strComputer
objDoc.Bookmarks.Add "ComputerBookmark",objRange

Let’s see if we can figure out how this works, preferably without having to use the word booksmarks – uh, bookmarks. As you can see, we start out simply enough, creating an instance of the Word.Application object and then setting the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. We then use this line of code to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

We should probably stop here and talk about Test.doc for a moment. Because WM’s question was about modifying existing … things … Test.doc needs to have a few bookmarks predefined. For our sample script, we’re assuming the document has the following three bookmarks:

· UserBookmark

· DomainBookmark

· ComputerBookmark

In case you’re wondering, our version of Test.doc looks like this, with phrases like <user name> indicating the location of the three bookmarks:

As you probably guessed, we’re going to replace the text of the three bookmarks with the logged-on user’s name and domain, as well as the name of the computer he or she is logged on to. To do that, we first create an instance of the Wscript.Network object and then assign the values of the UserName, UserDomain, and ComputerName properties to the variables strUser, strDomain, and strComputer, respectively. That’s what we do here:

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName
strDomain = objNetwork.UserDomain
strComputer = objNetwork.ComputerName

With that done we’re ready to modify some booksmarks. (Yeah, we know: we misspelled it again. But it’s close enough.) To change the text of the bookmark UserBookmark we use this block of code:

Set objRange = objDoc.Bookmarks("UserBookmark").Range
objRange.Text = strUser
objDoc.Bookmarks.Add "UserBookmark",objRange

Like we said, it’s actually pretty easy to locate and modify the text of a bookmark. In the first line of this code block we create a Range object corresponding to the Range property of the bookmark named UserBookmark. To do that, we simply connect to the Bookmarks collection and, more specifically, the bookmark named UserBookmark:

Set objRange = objDoc.Bookmarks("UserBookmark").Range

Why do we even need to create a Range object corresponding to the bookmark’s Range? Well, for whatever reason, a bookmark object doesn’t have a Text property; instead, each bookmark has a Range property that, in turn, has a Text property. It’s a roundabout way of doing things, but to change the text of a bookmark we need to change the text of the bookmark’s Range.

And, yes, that sounds convoluted, but, fortunately, this is a case of easier done than said. In fact, this one line of code is all we need to set the text value of the bookmark UserBookmark to the value of the variable strUser:

objRange.Text = strUser

The third line in our code block is optional; depending on your needs you can leave it out altogether. By default, any time you change the text of a bookmark you delete the bookmark itself. That might or might not be a problem. If it is a problem (that is, if you’d prefer that the bookmark remain in the document, albeit with the newly-modified text) then leave this line of code in place:

objDoc.Bookmarks.Add "UserBookmark",objRange

If you don’t care that the bookmark disappears then you can remove the preceding line.

Oh, right: we should probably mention that the line of code in question simply recreates the bookmark UserBookmark, something it does by calling the collection’s Add method and passing two parameters: the name of the new bookmark and the object variable objRange, which indicates the range (location in the document) for the bookmark.

The remainder of the script simply follows the exact same process in order to modify the text of DomainBookmark and ComputerBookmark. When all is said and done Test.doc should look something like this:

Pretty slick, huh?

Incidentally, here’s another little script you might find handy. This one opens the document Test.doc and then reports back information about all the bookmarks found in that document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set colBookmarks = objDoc.Bookmarks

For Each objBookmark in colBookmarks
    Wscript.Echo objBookmark.Name
    Set objRange = objDoc.Bookmarks(objBookMark.Name).Range
    Wscript.Echo objRange.Text
    Wscript.Echo objBookmark.Start
    Wscript.Echo objBookmark.End
    Wscript.Echo
Next

Most of this should be self-explanatory, which is why we won’t bother to explain any of it. If you do have questions you can take a look at the Microsoft Word VBA Language Reference.

We truly hope this answers your question, WM; after all, considering how long it took us to write (or at least to type) the thing, well …. On the other hand, the inability to type the word bookmarks does put the Scripting Guy who writes this column into very rarefied company. For example, did you know that Albert Einstein also had trouble typing the word bookmarks? It’s true: take a look at his proofs for both the general and special theories of relativity: you won’t find a single instance of the word bookmarks. Coincidence? We think not.


How Can I Open Word with the Cursor Positioned at the Start of a Specified Line?

Hey, Scripting Guy! How can I open Word with the cursor positioned at the start of a specified line?

-- PS

Hey, PS. You know, we Scripting Guys have decided that we don’t like Microsoft Word anymore. How come? Well, as Scripting Guys we like to find solutions to seemingly-unsolvable problems: any time we can find a workaround for something that looked impossible people just shake their heads and say, “Wow, those Scripting Guys are really amazing, aren’t they? I would have bet that there was no way to do X and yet they found a way to do it.”

So why don’t we like Microsoft Word anymore? Well, that’s easy: you never have to look for clever workarounds in Microsoft Word. Instead, pretty much everything you want to script in Microsoft Word can be done in a very simple, very straightforward fashion. And yes, that’s great for you, but it’s a real bummer for us; after all, no one’s going to say, “Wow, PS wanted to go to a specific line in a Word document and the Scripting Guys did that by using the GoTo method. They must be the smartest guys in the world!” Needless to say, it doesn’t take a genius to use the GoTo method to go to a specific line in a Word document.

Of course, on the bright side we didn’t have to put a whole lot of effort into answering this question either. (Scripting Guys are always torn between wanting to do something clever and being pretty dang lazy.) With that in mind, here’s a script that opens up a Word document (C:\Scripts\Test.doc) and then immediately moves the cursor to line 31:

Const wdGoToLine = 3
Const wdGoToAbsolute = 1

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc",,TRUE)

Set objSelection = objWord.Selection
objSelection.GoTo wdGoToLine, wdGoToAbsolute, 31

We begin this script by defining a pair of constants: wdGoToLine (with a value of 3) and wdGoToAbsolute (with a value of 1). wdGoToLine tells the script what kind of object to go to; in addition to going to a line we could go to a bookmark, a paragraph, a page, a field, or all sorts of other things. (For a complete list, click here and look for the wdGoToItem enumeration.) wdGoToAbsolute tells the script to go to a specific line (we’ll tell it which line momentarily). Alternatively, we could go to the next line, the previous line, the first line, the last line, etc.

Next we create an instance of the Word.Application object and set the Visible property to True; this gives us a running instance of Microsoft Word that we can see onscreen. We use the Open method to open the document C:\Scripts\Test.doc, then use this line of code to create an instance of the Word Selection object:

Set objSelection = objWord.Selection

By default, the Selection object (and thus our cursor) will appear at the very beginning of the document. How do we move the cursor to, say, line 31? Here’s how:

objSelection.GoTo wdGoToLine, wdGoToAbsolute, 31

As you can see, all we do is call the GoTo method followed by our two constants and the number of the line we want to go to. That’s it; that’s all it takes.

Although that’s still a lot, you know; after all, we did have to look up the values for wdGoToLine and wdGoToAbsolute. And we did have to type the script into Notepad. That’s still pretty amazing, don’t you think?

Um, we said, “That’s still pretty amazing, don’t you think?”

See? That’s why we don’t like Microsoft Word anymore!


How Can I Put the File Name in the Footer of a Microsoft Word Document?

Hey, Scripting Guy! I’ve seen Word documents where the file name appears in the footer. Except that this file name is dynamic: if the name of the document changes then the footer changes, too. How can I add that kind of a footer using a script?

-- ZK

Hey, ZK. You know, it’s always tough to come back to work after being gone for a week; there’s just so many things to catch up on. For example, we haven’t had the chance yet to mention the fact that, in a study of the 25 largest metropolitan areas in the U.S., Seattle drivers ranked 23rd out of 25 when it came to the rudest drivers. (In other words, we have very polite drivers here. The rudest drivers of all? Those from Miami.)

The Scripting Guy who writes this column finds this interesting, in part because none of his Scripting Siblings ever come over to the Seattle area to visit him. Why not? “No way am I driving with all those crazy Seattle drivers!” Except, as it turns out, Seattle drivers aren’t crazy drivers after all.

Hmmm; wonder why they really don’t ever come over to visit him?

Note. Actually there’s a simple, logical explanation why they don’t come over: driving from the Tri-Cities to Seattle is a long drive, too long to make for a weekend visit. Apparently, though, it’s way shorter and way easier for the Scripting Guy who writes this column to drive from Seattle to the Tri-Cities. And he’s expected to do so, even if that means driving over, attending some sort of family function, and then turning around and driving back.

So if Seattle-area drivers are nice does that mean that Seattle-area Scripting Guys are nice, too? And does that also mean that they’d be happy to answer a question about adding a dynamic file name to the footer of a Word document? Heck no. But they’ll answer the question anyway:

Const wdFieldFileName = 29

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()

Set objRange = objDoc.Sections(1).Footers(1).Range
Set objField = objDoc.Fields.Add(objRange, wdFieldFileName)

objDoc.SaveAs "C:\Scripts\Test.doc"

OK. This script starts out by defining a constant named wdFieldFileName and setting the value to 29. As it turns out, Word includes a number of built-in “fields” that can display information such as file name, user name, author name, last-saved date, last-printed date, etc. The field that displays the file name has a value of 29; hence we assign our constant the value 29.

Note. Gosh, have the Scripting Guys memorized all these Word fields and their corresponding values? No, of course not, we – uh, yes, yes we have; that’s what makes us the Scripting Guys. For the rest of you, though, those constants – and their values – can be found by looking in the Microsoft Word SDK and, in particular, by looking at the wdFieldType enumeration.

After defining the constant we next create an instance of the Word.Application object and set the Visible property to True; that gives us a running instance of Word that we can see onscreen. We then use this line of code to add a new, blank document to our instance of Word:

Set objDoc = objWord.Documents.Add()

Now that we have a document we need to insert the field for the file name in the footer. To do that, we first create a Range object that encompasses the first footer in the first section of the document (which, for a brand-new document, will also be the only footer in the document):

Set objRange = objDoc.Sections(1).Footers(1).Range

And then we’re ready to insert the field:

Set objField = objDoc.Fields.Add(objRange, wdFieldFileName)

That’s right: just one line of code. We reference the Fields collection and then call the Add method. This method requires two parameters: the spot in the document where we want to insert the field (objRange, the object reference to the footer) and the value of the field to be inserted. This, of course, is where we use our constant wdFieldFileName.

Note. By default, this script left-aligns the footer. If you’d prefer the footer to be center-aligned then just add this bit of code after inserting the field: objRange.ParagraphFormat.Alignment = 1.

Of course, having a footer that displays the file name is somewhat meaningless if you don’t actually have a file name. Therefore, we added this line of code to save the file as C:\Scripts\Test.doc:

objDoc.SaveAs "C:\Scripts\Test.doc"

And that, as they say, is all, folks.

You know, it’s funny: usually when we mention a news item in this column we do so only so we can poke fun at that item. In this case, though, we happen to agree with the news: Seattle drivers are pretty polite. But, then again, it’s hard to be a rude and aggressive driver when you’re gridlocked over 8 lanes of traffic, none of which ever seem to move. You can still be rude and aggressive, mind you, but it’s awfully hard to call you a driver when you aren’t actually going anywhere.


How Can I Put the User Name into the Footer of a Microsoft Word Document?

Hey, Scripting Guy! How can I put the user name into the footer of a Microsoft Word document?

-- RR

Hey, RR. That’s an interesting question, and probably the first how-do-I-put-the-user-name-in-the-footer-of-a-Word-document question we’ve ever received. (No, really!) But what the heck; let’s see if we can find an answer for you. Besides, this gives us a chance to shamelessly plug the upcoming Scripting Week 2 webcast series. Among the 10 webcasts scheduled for that week: If You Want Something Done Right, Let Microsoft Office Do It For You, a webcast that will provide an introduction to scripting Microsoft Office applications. If you’re interested in scripting Word, Excel, and/or PowerPoint, this might be 90 minutes well spent.

Of course, right now all you want to do is get the user name into the footer of a Word document. No problem; in fact, we’ll show you two different ways to do this.

As the Microsoft Scripting Guys, we spend most of our time helping system administrators write VBScript scripts. Because of that, let’s first show you a way to do carry out this task from a .vbs file. The following script determines the name of the logged-on user, starts an instance of Word, creates a new document, and then sets the document footer text to the name of the logged-on user:

Set objNetwork =
CreateObject("Wscript.Network")
strUser = objNetwork.UserName

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

objDoc.Sections(1).Footers(1).Range.Text = strUser
objDoc.Sections(1).Footers(1).Range.ParagraphFormat.Alignment = 1

What, you expected big, long, and complicated? Actually, scripting Microsoft Word is pretty easy, as this particular sample demonstrates. In this script, the first two lines of code use the WSH Network object to grab the name of the logged-on user and then store that name in the variable strUser. The next three lines of code create an instance of Microsoft Word, make that instance visible on screen, and then give us a blank document to work with.

At that point, we need only one line of code to set the footer text:

objDoc.Sections(1).Footers(1).Range.Text = strUser

To clarify a bit, footers are tied to document sections; because this document has only a single section, we specify Sections(1). Footers(1), which means we want to add the text to the primary footer in the section, while; Range.Text indicates that we want to set the Text property of the footer. And strUser, of course, is the name of the logged-on user. That’s it; that’s all it takes.

Just for the heck of it, though, we did add a final line of code that centers the footer on the page. Hey, why not?

Of course, it’s quite possible that you don’t want to perform this trick from a .vbs file; instead, you’d like to have this code available as a Word macro. In that case, use a macro script similar to this:

Sub FooterAdder()

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName

ThisDocument.Sections(1).Footers(1).Range.Text = strUser
ThisDocument.Sections(1).Footers(1).Range.ParagraphFormat.Alignment = 1
End Sub

Here we have a subroutine named FooterAdder that gets the name of the logged-on user and makes that the document footer. Notice that we don’t need to create an instance of Word; if you’re running a Word macro, we’re gonna assume that Word is already running. In addition, this code doesn’t create a new document; instead, it simply adds the footer to the current document. That’s what ThisDocument indicates.

Incidentally, we know there is a lot of interest in scripting Office applications, and – hopefully – that interest will only increase after Scripting Week 2. So keep an eye on the Script Center during the month of February. Sometime during that month, we plan to unveil a new section (Office Space) devoted to scripting Microsoft Office. In the meantime, let us know what other Office-related questions you might have. We’ll try to answer a few of those between now and the debut of Office Space.


How Can I Rename a Word Document Using the First Three Characters in That Document?

Hey, Scripting Guy! How can I open a Word document, read the first three characters, and then rename the file using those three characters?

-- DG

Hey, DG. To tell you the truth, we always get a chuckle any time we get a question that asks for our help in renaming files or folders. Why? Well, the Scripting Guy who writes this column has essentially one folder on his computer, a desktop folder named Stuff. Inside that folder he has a few subfolders, including folders named Junk, New Folder, and New Folder (2). And inside those folders he has files with names like X.vbs, Test.txt, Sample.doc, and Hold.xls. Are you sure you want our help when it comes to renaming files?

No, hey, just kidding: after all, just because we don’t do a very good job of naming our files doesn’t mean that we couldn’t do a good job of naming our files (assuming we tried a little harder). (Editor’s Note: You may have noticed the switch from “he” to “we.” Rest assured, not all the Scripting Guys are as disorganized as the Scripting Guy who writes this column.) For example, here’s a script that should help you with your problem: the script opens a Word document, reads the first three characters, and then renames the file using those characters:

Set objWord = CreateObject("Word.Application")

Set objDoc = objWord.Documents.Open("c:\scripts\test.doc")

Set objRange = objDoc.Range(0,3)
strNewName = "C:\Scripts\" & objRange.Text & ".doc"

objWord.Quit
Wscript.Sleep 3000

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.MoveFile "C:\Scripts\Test.doc", strNewName

Before we explain how the script works we should issue a couple of cautions. As you probably know, there are certain characters that cannot be used in a file or folder name; these prohibited characters include:

· \

· /

· :

· *

· ?

· “

· <

· >

· |

In this script we don’t bother to check whether any of those characters show up among the first three characters in the document. Ideally you’d want a script that took the first three valid characters in the document as opposed to the first three characters. We won’t get into how you might do that today; if there’s enough interest we’ll explore that in a future column. But it’s something to keep in mind.

Second, this script doesn’t verify that a file name is unique before attempting to save the file. That could be a problem if you tried running this script against multiple Word documents in the same folder; after all, you’re going to run into problems if the first three characters in two or more of those documents happens to be, say, The. In a more polished script, you’d check for the existence of a file (for example, The.doc) before trying to save it. If that file already exists, then you’d need a contingency plan for giving the new file an alternate name. Again, just something to keep in mind.

For now, however, we won’t worry about any of those things. Instead, we’ll simply point out that the script we did come up with begins by creating an instance of the Word.Application object, then uses the Open method to open the file C:\Scripts\Test.doc.

Note. That’s amazing: how in the world did you know that our sample file would be named Test.doc? We mean besides the fact that all our files seem to be named Test.doc.

With the document open we then use this line of code to create a Range object encompassing the first three characters in the document (the Range starts directly after character 0 and ends directly after character 3):

Set objRange = objDoc.Range(0,3)

And once we know the first three characters in the document we can use this line of code to construct a new file name:

strNewName = "C:\Scripts\" & objRange.Text & ".doc"

As you can see, our new file name is composed of three items:

· C:\Scripts\. This is, obviously, the path to the C:\Scripts folder. Note, however, that we need to stick a \ at the end.

· objRange.Text. The Text property contains all the text that appears in our Range. In this case, that means the first three characters in the document (for example, The).

· .doc. The file extension for our new file (seeing as how we’re dealing with Word documents).

Put them all together and they’ll spell something like this:

C:\Scripts\The.doc

After we construct the new file name we exit Word and then pause the script for three seconds (3000 milliseconds). We insert the pause simply to give Word a chance to close and, in the process, relinquish its grip on the file. Without a pause of some kind the script might try to rename the file while Word is still running and thus still has the file open. As you might expect, your script will fail if it tries to rename a file that another application is using.

Speaking of renaming a file, here’s how we do that:

Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.MoveFile "C:\Scripts\Test.doc", strNewName

Yes, it’s a little cryptic; for one thing, you won’t see a Rename method anywhere. All we’re doing, however, is creating an instance of the Scripting.FileSystemObject and then calling the MoveFile method. MoveFile requires two parameters: the path to the existing file and the path we want the file to be “moved” to. In this case we’re starting with a file that has the path C:\Scripts\Test.doc and “moving” it to a file that has the path C:\Scripts\The.doc. It might seem like a silly way to do things, but the net result is that the file gets renamed. Which is all we really care about, right?

We know that this will be the next question: “Is there any way I can do this to all the Word documents in the C:\Scripts folder?” So here’s a sample script that does just that. We won’t walk you through this code today, but you should find most of it self-explanatory. If you still have questions about how it works, take a look at the FileSystemObject section of the Microsoft Windows 2000 Scripting Guide.

Here’s the multi-file script:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\Scripts")

Set objWord = CreateObject("Word.Application")

For Each strFile in objFolder.Files
    arrNames = Split(strFile.Name, ".")
    If arrNames(1) = "doc" Then
        Set objDoc = objWord.Documents.Open(strFile.Path)
        Set objRange = objDoc.Range(0,3)
        strNewName = "C:\Scripts\" & objRange.Text & ".doc"
        objDoc.Close
        Wscript.Sleep 3000
        objFSO.MoveFile strFile.Path, strNewName
    End If
Next

objWord.Quit

Finally, we should also put your mind to ease by noting that this Scripting Guy did have some help when it came to naming his son. Which explains why he has a son named Dylan and not a son named Junk, Test, Stuff, or Sample. (Although, to be honest, all of those are actually better than some of the names proposed by the Scripting Spouse. Dylan has no idea how lucky he is to be named Dylan.)


How Can I Replace a Specified Font in a Microsoft Word Document?

Hey, Scripting Guy! We have a folder full of Word documents in which the author had an unfortunate tendency to use a strange font from time-to-time. How can I use a script to go in and replace just that strange font with Arial?

-- Z D-F

Hey, Z D-F. You know, when “desktop publishing” first came into vogue, the Scripting Guy who writes this column was in graduate school at the University of Washington. As such, he remembers reading several scholarly articles showing how desktop publishing was perhaps the worst thing that ever happened to business, at least from a productivity standpoint. Why? Well, back in the old days, before desktop publishing, anyone who needed to produce a document simply rolled a sheet of paper into a typewriter and typed it up. There was no concern about layout, no fiddling with graphics, and, above all, no debates about which font to use.

The font issue was especially interesting, because it led to more problems than just people wasting time trying different fonts and sizes. That was bad enough in and of itself, but the issue was exacerbated by the fact that most people simply aren’t very good at graphic design. Not only did they waste time messing around with fonts, but more often than not they ended up choosing the wrong fonts, thus producing a document that was much harder to read. In other words, productivity was lost not only in writing the documents but also in reading the documents.

Isn’t progress wonderful?

So can scripts help us to regain this lost productivity? Heck no, not unless you can write a script that will go around, bop people on the head, and tell them, “Hey, stop using these fonts that no one can read!” But at least a script can help us go in and replace crazy fonts with something a little more readable and a little more professional:

Const wdReplaceAll = 2

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection
= objWord.Selection

objSelection.Find.Font.Name = "Gigi"
objSelection.Find.Forward = TRUE

objSelection.Find.Replacement.Font.Name = "Arial"

objSelection.Find.Execute "", ,False, , , , , , , ,wdReplaceAll

To begin with, Z D-F, we’re assuming that you have a document that looks something like this:

As you can see, the main body of the document is written in Times New Roman, which is fine. However, the section headings are written in a font named Gigi; a pretty good argument can be made that fonts with names like Gigi should never be used in business proposals or other formal documents. Because of that what you’d like to do is search this document and replace the Gigi font with Arial. The tricky part, of course, is that you only want to replace Gigi; anything written in Times New Roman should be left alone. Believe it or not, our script does just that.

So how does our script do that? Well, to begin with, we define a constant named wdReplaceAll and set the value to 2; we’ll use this constant to tell the script to replace any and all the instances of the Gigi font. We then use this block of code to create a visible instance of the Word.Application object, open the document C:\Scripts\Test.doc, and then create an instance of the Word Selection object:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

You’re right: that is a lot of stuff packed into four lines of code, isn’t it? No wonder we’re so tired this morning!

At this point we’re ready to start having some fun. In order to do a find-and-replace operation in Word we need to assign values to two different objects: the Find and Replacement objects. With that in mind, we thus use these two lines of code to assign values to the Find object:

objSelection.Find.Font.Name = "Gigi"
objSelection.Find.Forward = TRUE

Nothing too terribly complicated here. We want to search for all instances of the Gigi font, so in line one we assign the value Gigi to the Find.Font.Name property. And because we’re starting with the cursor positioned at the beginning of the document we then assign the value True to the Find.Forward property; this tells the script to move forward through the document when searching (that is, to start at page 1 and continue on through the end of the document).

That’s the only search criteria we need to specify. As you can see, we don’t even specify the text we want to search for; that’s because we don’t care about the text itself. All we’re looking for here is the formatting.

As everyone knows, it takes two to tango; turns out that it also takes two to do a find and replace operation. That means that, having configured the Find object, our next step is to configure the Replacement object. This turns out to very easy; all we need to do is specify that the replacement font is Arial:

objSelection.Find.Replacement.Font.Name = "Arial"

In other words, we’ve now told the script to start at the beginning of the document, look for all instances of the Gigi font, and replace all those instances with Arial. But rather than sit here and talk about what our script could do why don’t we just go ahead and do it:

objSelection.Find.Execute "", ,False, , , , , , , ,wdReplaceAll

All we’re doing here is calling the Execute method, which tells the script to actually go do the find and replace operation. As you can see, we need to pass a crazy-looking set of parameters to this method:

"", ,False, , , , , , , ,wdReplaceAll

What does all that mean? Well, for detailed information about the Execute method and its parameters you should check the Microsoft Word VBA Language Reference on MSDN. For now we’ll note that the very first parameter indicates the text we want to search for. Because we’re searching only for formatting, the Word convention is to use an empty string (“”) to represent the search text. And you know the Scripting Guys: we always follow the standard conventions.

The second parameter has to do with case sensitivity. For this script case doesn’t matter; however, it’s crucial that the Execute parameters be passed by position. Admittedly, we aren’t going to use the second parameter. However, we can’t leave this parameter out altogether; if we did then the third parameter, which we do need, wouldn’t be the third item in the parameter list. Therefore, we leave the second parameter blank, inserting a blank space followed by a comma to indicate that this represents parameter 2.

And yes, that is kind of confusing, isn’t it? Well, suppose we had a parameter list that looked like this:

1, 2, 3, 4

Suppose, further, that some hypothetical method requires four parameters; however, we only need to use parameters 1 and 4. Could we pass this hypothetical method a parameter string that looks like this:

1, 4

No, we can’t; after all, we list only two parameters here, and our method requires four parameters. Because of that we need to pass a parameter string that looks like this, with the blank spaces indicating that parameters 2 and 3 should be left alone:

1, , , 4

It’s the same thing with the Execute method.

Now, where we were? Oh, right. After inserting a blank space for parameter 2 we then set parameter 3 to False. The third parameter tells Word to search for whole words; if this is set to True (the default value) the font for certain items (like punctuation marks) might not get changed. We then have a series of blank parameters (again, representing parameters we don’t need for this script). Our parameter list then culminates with the constant wdReplaceAll, which tells Word to replace every instance of the Gigi font. Without this parameter Word will find and replace only the first instance of Gigi.

And what are we going to get when we run this script? We’re going to get this:

Hasta la vista, Gigi!

Actually you’re right: we shouldn’t gloat. After all, Gigi might be gone, but it’s not forgotten; no doubt this crazy font will turn up again sometime. But that’s all right. We’ll just keep running our script and – in the true spirit of the modern age – spend our time undoing everything that our fellow employees spent hours and hours doing in the first place.

Isn’t progress wonderful?


How Can I Replace Text in a Microsoft Word Document?

Hey, Scripting Guy! How can I search a Microsoft Word document for all occurrences of the string <computername> and replace those occurrences with the actual name of a computer (e.g., atl-ws-01)?

-- WC

Hey, WC. And by the way, thanks for your question. Microsoft employees will be getting their yearly performance reviews back sometime in the next few weeks; in a completely unrelated matter, the Scripting Guy who writes this column needs a way to open a Word document and replaces all instances of the word underachieved with the word overachieved. Your question gives him a chance to practice those search-and-replace skills.

So how do you search a Word document for all occurrences of the string <computername> and replace it with the actual name of a computer? Here’s how:

Const wdReplaceAll = 2

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection
= objWord.Selection

objSelection.Find.Text = "<computername>"
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE

objSelection.Find.Replacement.Text = "atl-ws-01"

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

As you can see, it’s not a too terribly complicated script (with the possible exception of that weird-looking final line of code). We start out by defining a constant named wdReplaceAll and setting the value to 2; we’ll use this constant to ensure that we replace each and every instance of <computername> found in the document. Following that we create an instance of the Word.Application object and then – just so we can watch the excitement unfold onscreen – we set the Visible property to True. Finally, we open the document C:\Scripts\Test.doc and use this line of code to create an instance of the Selection object (which has the effect of positioning our cursor at the very start of the document):

Set objSelection = objWord.Selection

Got all that? Good. Because now it’s time to roll up our sleeves and get serious.

Note to managers engaged in yearly performance reviews. That, of course, is just a figure of speech. The Scripting Guy who writes this column is always serious, always has his sleeves rolled up, and is always working.

Pretty much.

As it turns out, the Selection object has a child object named Find; this child object is used to find text in a document. (Unlike the Scripting Guy’s own child object, whose purpose in life – other than spending dad’s money – remains unknown.)

Before we can start the search, however, we need to configure a few properties of the Find object:

objSelection.Find.Text = "<computername>"
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE

As you can probably guess, the Text property represents the text we want to search for. The Forward property specifies the direction of the search; we want to go forward, which simply means that we want to start searching from our current location (the beginning of the document) and continue until we reach the end of the document. MatchWholeWord means that we want to match only entire words. In this case that’s probably not all that important. However, suppose we were searching for the word cat. In that case, if MatchWholeWord was False we’d not only match cat, but also catalog, category, catacomb, vacation, and any other word containing the string cat. Like we said, in this case it probably doesn’t matter: we don’t know of very many words that have <computername> embedded in them. But just to be on the safe side ….

As it turns out, the Find object also has a child object: the Replacement object. For our purposes, the Replacement object has only one function: we use it to specify the replacement text. Because we want to replace all instances of <computername> with atl-ws-01 we set the value of the Replacement object’s Text property to, well, atl-ws-01:

objSelection.Find.Replacement.Text = "atl-ws-01"

After all that buildup it takes just one line of code to carry out the search and replace:

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

We won’t explain all those commas following the Execute method; if you’d like to know more about those take a look at this Office Space column. Suffice to say that the Execute method takes a number of parameters, each of which must be passed in order. The commas simply represent parameters that we don’t need in this script; however, we must pass blank parameters to ensure that wdReplaceAll appears in the proper spot. And that’s all we have to do.

Or at least all you have to do. We still have to modify the script so it replaces underachieved with overachieved. But you don’t need to worry about that.

Note. The Scripting Editor has suggested we tell people that the Scripting Guy who writes this column has no intention of cheating on his performance review. But that’s easy for her to say: she’s a highly-qualified and highly-competent employee. Unless he cheats, the Scripting Guy who writes this column could only get a passing score in one way: by working hard and doing a good job. What do you suppose the odds are of that happening?


How Can I Retrieve Field Values in a Microsoft Word Document?

Hey, Scripting Guy! I have a bunch of Word documents that include fields like LastSavedBy. How can I write a script that retrieves the current values for these fields?

-- OS

Hey, OS. You know, the other night the Scripting Guy who writes this column was watching TV when he saw an ad for a new Monopoly game: Monopoly, the Electronic Banking Edition. What was interesting (or disturbing, depending on your point of view) is that this version of Monopoly does away with cash; instead, players use debit cards and an ATM-like machine to keep track of their winnings.

In case you’re wondering, the Scripting Guy who writes this column found this disturbing. After all, there are few things in life more satisfying than watching one of your brothers grimly counting out every last Monopoly dollar he has, desperately hoping to reach the $1000 in rent he owes you. (And all the while you saying, “Tell you what, you give me your two yellow properties and you won’t owe me a thing.”) Having him simply swipe his debit card doesn’t sound like much fun at all. Sitting there with a huge pile of cash in front of you is absolutely the greatest feeling in the world.

Just ask – uh, never mind. There are certain people even the Scripting Guys shouldn’t make jokes about.

Sign of the times. Technically, the game doesn’t use just any old debit card: it’s a Visa debit card. Also, the playing pieces have been updated for the Banking Edition; these pieces now include: a Segway personal transporter (which is nice; after all, most of us will never see a Segway in real life); a space shuttle; a flat-screen TV; a baseball cap; a dog in handbag (yes, a handbag); and – the piece everyone will likely fight over – a tin of Altoids.

Anyway, the Scripting Guy who writes this column has no intention of playing the Electronic Banking Edition; he much prefers the old-fashioned version. And he will never, ever be the tin of Altoids. Instead, he prefers to be the race car any time he plays Monopoly. But, then again, who doesn’t?

Note. Sigh. As you might expect, the Scripting Editor doesn’t like being the race car; she prefers to be the shoe. (You heard right: the shoe.) We assume that’s only because the game doesn’t have any playing pieces even more boring than the shoe.

But it doesn’t really matter; after all, who has time for games anyway? Not us, not when we need to write a script that can retrieve field values from a Microsoft Word document:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Set colFields = objDoc.Fields
colFields.Update

For Each objField in colFields
    Select Case objField.Type
        Case 20
        Set objRange = objField.Result
        strUser = objRange.Text
        Wscript.Echo "Last Saved By: " & strUser
    End Select
Next

Let’s meander through the code and see if we can figure out how this all works. To begin with, we create an instance of the Word.Application object and then set the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. We then use this line of code to open the file C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

Well, that was easy enough, wasn’t it? Like taking candy from a baby.

Or playing Monopoly with your brothers.

After we open the document we then use the following line of code to retrieve a collection of all the fields in that document:

Set colFields = objDoc.Fields

Once we have the collection in hand we call the Update method to update each of these fields. By calling the Update method we get the latest, most up-to-the-minute value for each field in the document; we’re assuming that this is what most of you will want to do. On the other hand, some of you might want to retrieve the value of the fields at the time the document was last saved. That’s fine; just comment out (or remove) the following line of code and your script will not update all the fields when it opens the document. The commented code would end up looking like this:

' colFields.Update

Our next task involves setting up a For Each loop to loop through the collection of fields. As it turns out, Microsoft Word identifies fields by their Type, the Type property being a unique integer value. For example, the LastSavedBy field has a value of 20; the Author field has a value of 17. In our script, we set up a Select Case statement to examine the value of the Type property and then echo back the appropriate field name. As you can see, our Select Case statement only looks for one particular field type: the LastSavedBy field, which has a value of 20. However, you can easily extend the Select Case statement to account for other field types. For example, add this line of code to check for – and echo back the value of – the Author field:

Select Case objField.Type
    Case 17
    Set objRange = objField.Result
    strUser = objRange.Text
    Wscript.Echo "Author: " & strUser

Or, just use this code to echo back all the values, without specifying the field type:

For Each objField in colFields
    Set objRange = objField.Result
    Wscript.Echo objRange.Text
Next

We’ll leave that up to you. If you don’t need to echo back the field Type, or if you, say, want to be the shoe, well, hey, to each his own.

Note. Good point: how are you supposed to know that the Author field has a Type equal to 17? Well, probably the best way is to go to the Microsoft Word VBA Language Reference on MSDN and take a peek at the wdFieldType enumeration.

Let’s assume that we do find the Last Saved By field (a field that has a Type equal to 20). In that case, we execute this block of code:

Set objRange = objField.Result
strUser = objRange.Text
Wscript.Echo "Last Saved By: " & strUser

This is a little goofy, but in order to echo the value of the field we first need to create a Range object encompassing the field’s Result (that is, its value). We use this line of code to create the Range object:

Set objRange = objField.Result

Once we have a Range object we can then assign the value of that object’s Text property to a variable named strUser. And then we can simply echo back the message “Last Saved By: “ along with the name of the person who last saved the document:

Wscript.Echo " Last Saved By: " & strUser

And that, as they say, is that.

As you might expect, when he saw the commercial for Monopoly: The Electronic Banking Edition the Scripting Guy who writes this column immediately thought, “What are they trying to do, ruin the game completely?” Interestingly enough, after doing a little research he discovered that this isn’t the first time Parker Brothers has tried to ruin the game completely. In 1934 Charles Darrow attempted to sell his new creation to the company; Parker Brothers turned him down, claiming that the game took too long to play and was way too complicated. Darrow thus set out on his own, and the game began selling like hotcakes. Needless to say, Parker Brothers reconsidered and, a year later, bought the rights.

Monopoly quickly became the best-selling game Parker Brothers had ever had; in fact, its sales were credited with saving the company from bankruptcy. So what did Parker Brothers do? In 1936 they decided to stop making the game, assuming that it was just a fad that would soon die out. A sharp spike in sales caused them to change their mind, and from that day on– well, you know the rest of the story.

Note. Believe it or not, the Scripting Guy who writes this column didn’t know the history of Monopoly right off the top of his head; as we noted, he had to go out and research this. And yes, that is one of the perks about being a Scripting Guy: you can waste time reading about the history of Monopoly and get paid for doing so! After all, it’s research, right?


How Can I Right-Align a Single Column in a Word Table?

Hey, Scripting Guy! How can I right-align a single column in a Word table?

-- RC

Hey, RC. You know, one of the phrases no one ever wants to hear is “This isn’t as bad as it looks.” After all, that’s what your business partner says when you discover he’s been using the company payroll to buy lottery tickets. “This isn’t as bad as it looks,” is the phrase a doctor uses when you catch a glimpse of the hypodermic needle he’s preparing to stick you with. (In that case, he’s right: it’s usually worse than it looks.) And yes, “This isn’t as bad as it looks” is the phrase that the Scripting Guys use before showing you a script that right-aligns a column in a Microsoft Word table.

The difference, of course, is that we Scripting Guys sincerely mean it. As you’re about to see, it takes only a few lines of code to right-align a table column in Word. The only problem is that it’s difficult to right-align a table column unless you actually have a table in your document. For better or worse, creating a table in Word – while not especially hard – does require a relatively large amount of code, especially if you want to create a table that resembles the sort of table a system administrator might create. So relax. The script will look big and it will look complicated, but the part you’re interested in is very easy.

In other words, this isn’t as bad as it looks:

Const
wdAlignParagraphRight = 2
Const NUMBER_OF_ROWS = 1
Const NUMBER_OF_COLUMNS = 3

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

Set objRange = objDoc.Range()
objDoc.Tables.Add objRange, NUMBER_OF_ROWS, NUMBER_OF_COLUMNS
Set objTable = objDoc.Tables(1)

x=2

objTable.Cell(1, 1).Range.Text = "Process Name"
objTable.Cell(1, 2).Range.text = "Process ID"
objTable.Cell(1, 3).Range.text = "Handle Count"

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer &
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_Process")
For Each objItem in colItems
If x > 1 Then
objTable.Rows.Add()
End If
objTable.Cell(x, 1).Range.Text = objItem.Name
objTable.Cell(x, 2).Range.text = objItem.ProcessID
objTable.Cell(x, 3).Range.text = objItem.HandleCount
x = x + 1
Next

objDoc.Tables(1).Columns(3).Select
Set objSelection = objWord.Selection

objSelection.ParagraphFormat.Alignment = wdAlignParagraphRight

What this script does is grab information about all the processes currently running on a computer; it then displays that process information in a Word table. That’s what almost all the preceding code is for: it creates a Word document, gets the process information, then creates a table and populates the rows and columns with process information. Once we get that far (in other words, once we have a table in our document) we need only three lines of code to right-align one of the columns.

Note. So are we going to explain all the code for creating a table in Microsoft Word? No, not today: that goes beyond what we can cover in a Hey, Scripting Guy! column. However, we do have an Office Space column that explains the process in considerable detail.

Hey, the Scripting Guys would never leave you hanging like that.

So what happens after we do have a table in our document? Well, the first step is to select one of the table columns. In our sample table, we display the process handle count in column 3; we did that because the handle count is a number and numbers are always good candidates for right-alignment. Here’s how we select column 3 in the table:

objDoc.Tables(1).Columns(3).Select

That should be pretty straightforward. The variable objDoc is an object reference to a Word document; we created that object reference way back at the beginning of the script:

Set objDoc = objWord.Documents.Add()

Tables(1) simply means we want to work with the first table in the Tables collection, a collection consisting of all the tables in our document. Likewise, Columns(3) means that we specifically want to work with the third column in table 1. And Select means, well, it means that we want to select column 3 in table 1. Make sense? We thought so.

After selecting the text we need to create an instance of the Selection object in order to manipulate that text. That’s what we do here:

Set objSelection = objWord.Selection

That means that all we have to do now is right-align all the paragraphs in the selected text:

objSelection.ParagraphFormat.Alignment = wdAlignParagraphRight

In case you’re wondering, wdAlignParagraphRight is a constant we defined at the beginning of the script (assigning it the value 2); we use this constant to tell Word that we want the selected text to be right-aligned. Needless to say, we could specify other alignment options, including:

· wdAlignParagraphCenter (value 1) to center the text.

· wdAlignParagraphLeft (value 0) to left-align the text.

· wdAlignParagraphJustify (value 3) to justify the text.

And what happens when we run this script? This happens:

See, we told you: it really wasn’t as bad as it looked, was it?


How Can I Run a Macro After Opening Up Word?

Hey, Scripting Guy! How can I run a specified Word macro after opening Word using a script?

-- JD

Hey, JD. You know, when they first tested the atomic bomb a number of physicists had legitimate fears that the bomb might set off an unstoppable chain reaction that would end up destroying the entire world. We have to admit that, when we read your question we had the same sort of concern: a script that would start a macro that might then start a batch file that might then start a Windows PowerShell session that might then destroy the entire world. Not a pleasant thought, especially for those of us who have season tickets to the University of Washington men’s basketball.

On the other hand, nothing lasts forever, and we’ve always assumed that when the world does end the Scripting Guys will get blamed for it anyway. So what the heck; here’s a script that opens a Word document and then runs a specified Word macro:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

objWord.Run "Macro1"

Actually, we agree with you: you’d think a script this dangerous and this apocalyptic would be a little more impressive, wouldn’t you? For better or worse, though, it’s actually pretty easy to call a macro from within a script. As you can see, we start out by creating an instance of the Word.Application object and then setting the Visible property to True; that gives us a running instance of Word that we can see on screen. We then use this line of code to open the document C:\Scripts\Test.doc:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")

So how do we run a macro once the document is open? Like this:

objWord.Run "Macro1"

Again, there isn’t much to this: we simply call the Run method (which is part of the Word.Application object), passing as the sole parameter the name of the macro (Macro1). And while we won’t go so far as to say that there’s nothing easier than this, well, off the top of our heads we couldn’t really come up with anything.

Incidentally, the first time we tried this script we started by manually opening Test.doc, creating a macro named Macro1, then saving and closing the document. After the macro had been created we then used the script to reopen the document and run Macro1. Following our success there we deleted Macro1 from Test.doc, opened the Word template Normal.dot, then recreated Macro1 in the template file. After closing Normal.dot we ran the script again; once again, it worked. What does that mean? That means that this approach can be used to run a macro stored in an individual document or a macro stored in a template attached to that document.

And because we Scripting Guys like to live dangerously we finished off the day by using the following script to create a brand-new document, type in some text, and then run the macro (which, in case you’re wondering, simply selects all the text in the document and makes it boldface):

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Add()

Set objSelection = objWord.Selection
objSelection.TypeText("AAAAAA")

objWord.Run "Macro1"

Did that work? Do you even have to ask?

That should do the trick, JD. And don’t worry: there is absolutely no chance whatsoever that running this script will bring about the end of the world. No chance.

Just in case, though, remember this: Peter Costantini wrote today’s column. That’s Costantini, C-O-S ….


How Can I Save Word Documents as Text Files By Using a Script?

Hey, Scripting Guy! I have a series of Word files. I would like to open each of these files and save them as plain-text files. Is there a way to do that using a script?

-- CG

Hey, CG. Yes, you can do this quite easily with a script; in fact, with very few exceptions anything you can do within a Microsoft Office program you can do by using a script. Within Word, you can do a File – Save As and save a Word document as a text file. And you can do the very same thing using a script. In fact, you can do it using this script:

Const wdFormatText = 2

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open("c:\scripts\mylog.doc")
objDoc.SaveAs "c:\scripts\mylog.txt", wdFormatText

objWord.Quit

We start off by creating a constant named wdFormatText and assigning it the value 2; this constant will be used to tell Word we want our new file saved as a text file. We then create an instance of Microsoft Word, and use the Open method to open the file C:\Scripts\MyLog.doc. After the document is open, we need just one line of code to save the Word document as a text file:

objDoc.SaveAs "c:\scripts\mylog.txt", wdFormatText

As you can see, we call the SaveAs method, and we pass it two parameters: 1) C:\Scripts\MyLog.txt, which is the path for our new text file; and, 2) wdFormatText, which tells Word to save the file as plain text. That’s it. We then use the Quit method to close our instance of Word.

Two quick notes. First, you won’t see any of this happening on screen; that’s because, by default, Word runs in an invisible window any time you call it from a script. If you’d prefer to see it pop up on the screen, save the file, and then disappear, use this code instead:

Const wdFormatText = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = TRUE
Set objDoc = objWord.Documents.Open("c:\scripts\mylog.doc")
objDoc.SaveAs "c:\scripts\mylog.txt", wdFormatText

objWord.Quit

The only difference here is that we’ve set the Visible property to TRUE.

Second, you can save your Word documents in formats other than plain text. For example, to save a Word document as an HTML file, use the constant wdFormatHTML (value = 8); to save a Word document as XML, use the constant wdFormatXML (value = 11).

And at the risk of sounding like an informercial, if you’d like to know more about scripting and Microsoft Office you might want to check out the Scripting Week 2 webcast If You Want Something Done Right, Let Microsoft Office Do It For You.


How Can I Search a Word Document for All the Words in Double Brackets?

Hey, Scripting Guy! How can I search a Word document for all the words in double brackets? For example, I need to find words like this: [[failed]]. I then need to save the words I find to a text file.

-- PP

Hey, PP. You know, one thing that seems to be true of the Scripting Guys is a willingness to do something even when they have no idea what it is they’re doing. Put new molding in the family room? Come on, you just saw some wood and nail it to the wall, right? New tile in the kitchen? Piece of cake. Search a Word document for words in square brackets? Hey, how hard could something like that be?

In our defense, in most cases we come up with a finished product that looks reasonably good. New molding? Looks nice; just don’t look in the garbage can and count the number of pieces that were cut in the wrong place. Kitchen floor? That looks great … as long as you don’t take a look under the refrigerator, mind you. A script that searches a document for words in square brackets? As long as you’re willing to overlook all the false starts we ran into, well, then it wasn’t so bad after all:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

objSelection.Find.Forward = True
objSelection.Find.MatchWildcards = True
objSelection.Find.Text = "\[\[*\]\]"

Do While True
    objSelection.Find.Execute
    If objSelection.Find.Found Then
        strWord = objSelection.Text
        strWord = Replace(strWord, "[[", "")
        strWord = Replace(strWord, "]]", "")
        Wscript.Echo strWord
    Else
        Exit Do
    End If
Loop

Admittedly, parts of this script are a little odd-looking, most notably the line where we specify our search text. With that in mind, let’s walk you through the code and see if we can explain how it works.

Note. That’s a good point: we do walk through the code in each and every Hey, Scripting Guy! column, don’t we? We just thought that saying we were going to do that would somehow make today’s column sound special.

The script starts out in fairly-straightforward fashion: we simply create an instance of the Word.Application object and then set the Visible property to True. That gives us a running instance of Word that we can see on screen. We use the Open method to open the file C:\Scripts\Test.doc, then create an instance of the Word Selection object.

Note. You’re lost already? Don’t worry about it: if you’ve never written a script that interacts with Microsoft Word we wouldn’t expect that first paragraph to make much sense. For more information on scripting Microsoft Word take a peek at our Office Space Archive. And, while you’re there, you might want to check out this article, which explains how to find and replace text in a Word document.

As it turns out, the Selection object (which, when created without any additional parameters, simply positions the cursor at the very beginning of the document) has a child object named Find. As you might expect, the Find object is used for finding text in a Word document. Before we can use the Find object, however, we need to configure three important properties, something we do here:

objSelection.Find.Forward = True
objSelection.Find.MatchWildcards = True
objSelection.Find.Text = "\[\[*\]\]"

The first two properties – Forward and MatchWildcards – are relatively easy to figure out. The Forward property, when True, tells the script to search the document from the current cursor position to the end of the document. We want to do that because the cursor is currently positioned at the start of the document. If the cursor was at the end of the document we’d probably set the Forward property to False, which would cause us to search the document backwards, from the current cursor position to the beginning of the document. MatchWildcards? That’s even easier: MacthWildcards simply tells the script that we’re going to use a wildcard character in our search.

Why are we going to use a wildcard character in our search? Well, we’re looking for anything that begins with a pair of square brackets ([[) and then ends with a pair of square brackets (]]). To do that we can use the asterisk to represent any character or set of characters. Looking for something that starts with two brackets, has some character or set of characters in the middle, and then ends with two brackets? Then you want to search for something like this:

[[*]]

Of course, what you want to do and what Word will let you do aren’t always the same thing. For example, when it comes to doing a wildcard search, the bracket symbols are reserved characters. If you try using [[*]] in a wildcard search you’ll get back an “invalid range” error.

Note. This is true even if you aren’t using a script. In Word click Edit and then click Find. When the Find and Replace dialog box appears, type [[*]] in the Find what box, then click More and select Use wildcards. Now click Find Next and see what happens.

Because the brackets are reserved characters, we need to “escape” them by preceding each character with a \. That’s why the value of the Text property (the text we want to search for) looks so odd:

objSelection.Find.Text = "\[\[*\]\]"

Believe it or not, that really is the text we’re searching for; we simply had to precede each of the four bracket symbols with a \. Strange but true.

Now we’re ready to start searching, a task we perform inside a Do While loop:

Do While True
    objSelection.Find.Execute
    If objSelection.Find.Found Then
        strWord = objSelection.Text
        strWord = Replace(strWord, "[[", "")
        strWord = Replace(strWord, "]]", "")
        Wscript.Echo strWord
    Else
        Exit Do
    End If
Loop

You might have noticed the syntax for the Do While loop: Do While True. In essence that means, “Continue looping as long as True equals True.” That sounds like a problem waiting to happen: with the possible exception of politics, doesn’t True always equal True? And, if so, doesn’t that mean this script will run forever without stopping? Yes, it does.

Well, unless we add a line of code that we’ll discuss in a moment.

Inside the loop we start out by calling the Execute method; that causes the script to search our document for the first instance of the target text. The script begins searching and stops the first time it finds an instance of the text. On top of that, if the search is successful then the value of the Found property will be True. That’s what we’re checking here:

If objSelection.Find.Found Then

But what if we’ve reached the end of the document and no instance of the target text can be found? In that case, the Found property will be False, and we use the Exit Do command to exit the Do While loop. That’s how we manage to get out of our loop: True is still equal to True, but the Exit Do command will automatically take you out of the loop. Period.

On the other hand, suppose we do find the target text? In that case, Word will automatically move the selection to the text that was just found. Because of that, we can get the value of the new selection just by checking the value of the Selection object’s Text property:

strWord = objSelection.Text

What does that mean? Let’s assume that Word has found this item:

[[failed]]

In that case, the word [[failed]] will (for our purposes) be highlighted, and the value of the Text property will be whatever happens to be highlighted. In other words, the Text property will equal this:

[[failed]]

Of course, ideally we’d like to get rid of the surrounding brackets and pick out just the word failed. Fortunately that’s easy to do. After storing the value of the Text property in the variable strWord we can use the VBScript Replace function to replace the four bracket symbols ([[ and ]]) with, well, nothing. That’s what we do with these two lines of code:

strWord = Replace(strWord, "[[", "")
strWord = Replace(strWord, "]]", "")

To keep the script simple, we then do nothing more than echo back the value of strWord. Admittedly, PP, you wanted to write any words that were found to a text file. For more information on doing that, check out the Microsoft Windows 2000 Scripting Guide. (And, of course, the Hey, Scripting Guy! archive has plenty of sample scripts that show you how to write data to a text file.)

As we noted, the script will find the first instance of the target text and then stop. But what if there are more words enclosed in square brackets? We’ve already thought of that: that’s why we’re using the Do While loop. After finding the first instance the script loops around, calls the Execute method and starts the whole process all over again. That continues until the script reaches the end of the document. At that point the Found property will be False, and we exit the loop.

As for the Scripting Guys, with another challenge successfully tackled it’s time to move on to the next project: putting a basketball court in the backyard. Have we ever actually poured concrete before? Do you even have to ask?


How Can I Search For (and Reformat) Highlighted Text in a Word Document?

Hey, Scripting Guy! How can I find text in a Word document that has been highlighted in a particular color (say, turquoise), and then both remove the highlight and change the font color of that text to something else (say, blue)? I’d be extremely chuffed if you can solve this one!

-- KB

Hey, KB! Blimey! You know, when we first saw your message we thought, “KB’s off her trolley!” But we decided to give it welly, and when we realized that things were tickety-boo we were absolutely gob-smacked; in fact, we said to ourselves, “Guess the lass wasn’t so barmy after all; this should make her bite her arm off!” And then after all that everything ended up luvvly-jubbly. Brill!

Note. No, this is not all codswallop. If you don’t believe us, check out this Web site for more information.

Hmmm … You know, once you start talking like that, it’s hard to stop (blooming hard). So maybe we should just belt up and show you the script (which, as it turns out, was a doddle after all):

Const wdTurquoise = 3
Const wdNoHighlight = 0
Const wdBlue = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objRange = objDoc.Range

objRange.Find.Highlight = True
objRange.Find.Forward = True

Do while objRange.Find.Execute
    If objRange.HighlightColorIndex = wdTurquoise Then
        objRange.HighlightColorIndex = wdNoHighlight
        objRange.Font.ColorIndex = wdBlue
    End If
    intPosition = objRange.End
    objRange.Start = intPosition
Loop

Because we have no desire to drop a clanger, we’ll set aside the British slang for a moment and try to explain how the script works. As you can see, we start out by defining three constants, one that we’ll use to specify the highlight color we’re searching for (wdTurquoise), and two to specify the new highlight and font colors for any text we find (wdNoHighlight and wdBlue, respectively):

Const wdTurquoise = 3
Const wdNoHighlight = 0
Const wdBlue = 2

After that we create an instance of the Word.Application object and set the Visible property to True; that simply gives us a running instance of Microsoft Word that we can see on screen. With Word up and running we use the Open method to open the document C:\Scripts\Test.doc, then use this line of code to create an instance of the Word Range object (which, by default, will be positioned at the beginning of the document:

Set objRange = objDoc.Range

Now we’re ready for action. We won’t discuss finding and replacing text in any detail today; that’s because we do a much more thorough job of that in one of our Office Space columns. However, our next step is to configure two properties of the Find object: we want to find words that have been highlighted (in any color), and we want our search to start at the beginning of the range and then move forward throughout the document. That’s what these two lines of code are for:

objRange.Find.Highlight = True
objRange.Find.Forward = True

Admittedly, the rest is a bit of a bodge; that’s because, while it’s easy enough to search for text that has some kind of highlight, we couldn’t figure out a way to search for text that had a specific color of highlight. Rather than faff around genning up on a way to search for a specific color highlight we decided to use this spot of code instead:

Do While objRange.Find.Execute
    If objRange.HighlightColorIndex = wdTurquoise Then
        objRange.HighlightColorIndex = wdNoHighlight
        objRange.Font.ColorIndex = wdBlue
    End If
    intPosition = objRange.End
    objRange.Start = intPosition
Loop

What we’re doing here is setting up a Do While loop that runs as long as the search command (Find.Execute) runs. When we call the Execute method, the script will start wherever the Range object is positioned and then search for the first item that meets the search criteria. (In this case, of course, our only criterion is that the text be highlighted.) If no items are found then the search automatically ends, which also means that we’ll automatically exit the Do While loop.

That’s how we keep the script from simply searching the document over and over and over again.

If we do find a highlighted item we check to see if the value of the HighlightColorIndex property is turquoise (incidentally, you can find information about all these properties and their allowed values in the Microsoft Word VBA Language Reference):

If objRange.HighlightColorIndex = wdTurquoise Then

If the highlight color is turquoise we execute these two lines of code:

objRange.HighlightColorIndex = wdNoHighlight
objRange.Font.ColorIndex = wdBlue

With the first line, we remove the highlight for the found text; with the second, we change the font color of that text to blue. To tell you the truth, this was a bit jammy, but it ended up working just fine.

And to think that some of you have said that the Scripting Guys are gormless!

Oh: and if text is highlighted but the color is something other than turquoise then we simply leave well enough alone. Quite.

Of course, this only gets us as far as the first highlighted item in the text. To search for any subsequent items that meet our criteria we need to use these two lines of code:

intPosition = objRange.End
objRange.Start = intPosition

Why? Well, until we call these two lines of code our range will consist solely of the text that we just found. That’s not what we want: after all, if we call the Execute method again we’ll end up searching just that little block of text. And because that text is no longer highlighted, the search - and our script - will simply end.

To avoid that, we assign the value of the End property (the last character in the range) to a variable named intPosition; we then set the Start property of the range to intPosition.

So why do we do that? Well, suppose we found a highlight that covers characters 5 through 10; that means our range also covers characters 5 through 10. If we set intPosition to 10 (the last character in the range), and then assign that value to the range’s Start property, that moves the beginning of our range to character position 10. And because we didn’t specify an End value, the range will automatically encompass the remainder of the document. In other words, now when we call Execute we’ll start searching at the point where the first search left off, and we’ll continue searching until either we find a new item or we reach the end of the document.

Which is exactly what we want to happen.

Feeling knackered? We don’t blame you. But even if you don’t fully understand how it works, it’s still a good flutter, especially for a script this smashing.

Come on: would the Scripting Guys tell you porkies?

At any rate, Bob’s your uncle, KB. Cheerio!


How Can I Search for and Highlight Words in a Microsoft Word Document?

Hey, Scripting Guy! How can I search a Word document for any of 305 different words and highlight these words if they are found?

-- SB

Hey, SB. This might be hard for you to believe, but the rest of the technical writing community doesn’t exactly hold the Scripting Guys in awe. However, most of our fellow writers do grudgingly concede that writing a daily scripting column is a noteworthy endeavor. “How do you do that?” they ask. “How do you find the time to write a brand-new column each and every day?”

Needless to say, the Scripting Guys reply with a very thoughtful monologue about the values of hard work, dedication, and devotion both to Microsoft and to Microsoft’s customers. But you guys know better: you’d like to know how we really manage to write a brand-new column each day. Well, just between you and us, here’s the secret: we don’t actually write a new column each day. It just looks like it.

If you ever decide to write a daily scripting column, our advice would be to write one column and then figure out how to keep writing that same column over and over again. For example, not too long ago we answered a question on how to search a Word document for a specific word and, if found, boldface that word. Now, someone who really was hardworking and dedicated would write that column and then forgot about it, moving on to the next challenge. But not the Scripting Guys. We saved that column, then waited for the opportunity to use it all over again. (And you thought the Hey, Scripting Guy! archive was posted for your benefit.)

And, lo and behold, here’s our opportunity. You need to search for any of 305 different words and then highlight those words if found? Well, we won’t actually show a script that uses 305 different words. But here’s a script that searches for two words – boldface and the – and highlights either of those words if found:

Const
wdReplaceAll = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

arrWords = Array("boldface", "the")

For Each strWord in arrWords
objSelection.Find.Text = strWord
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE

objSelection.Find.Replacement.Highlight = TRUE

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
Next

Like we said, this column is largely a rehash of our previous column on boldfacing words in a Word document. Because of that we’ll skip much of the explanation, and focus on the differences between this script and the boldfacing script. If you don’t fully understand the basic workings of this script (for example, how you conduct a find-and-replace operation in Microsoft Word in the first place) please see the boldfacing column for more details.

Just like the previous column, the script in this column starts out by creating a constant named wdReplaceAll and setting the value to 2; we’ll use this constant to tell Word to automatically replace all instances of the target text. We then create a visible instance of the Word.Application object and use the Open method to open the document C:\Scripts\Test.doc. After creating an instance of the Word Selection object we’re ready to roll up our sleeves and get to work.

Our first step is to define the words we want to search for. In this case we’ve decided to search for only two words, so we simply store both of those words in an array named arrWords:

arrWords = Array("boldface", "the")

Note. If you wanted to you could create an array containing 305 words. Alternatively, it might be easier to simply list the words in a text file and then have the script read that text file line-by-line and then search for the words one at a time. That’s something we’ll leave up to you.

Next we set up a For Each loop to loop through this collection of words. Inside the loop we assign the first word in the array to the Text property of the Find object. Those two lines of code look like this:

For Each strWord in arrWords
    objSelection.Find.Text = strWord

After defining the search text we configure a few additional properties of the Find object, including the Forward and MatchWholeWord properties. The Forward property tells the script that we want the search to start at the beginning of the document (the default location for the Selection object) and continue forward through the end of the document. The MatchWholeWord property tells the script to match only exact instances of boldface and the; that way we don’t end up highlighting words like them, anthem, or anything else containing the letters t-h-e.

Of course, we don’t just want to find the target words; we also want to highlight those words as we find them. To do that all we have to do is set the value of the Replacement object’s Highlight property to True:

objSelection.Find.Replacement.Highlight = TRUE

That’s basically all we have to do. From here we call the Execute method, which searches for and highlights all instances of the word boldface. The script then loops around and does the same thing for all instances of the second word in our array (the). This continues until we have searched for and highlighted all the words in our array, regardless of whether we have two words in the array or 302 words in the array. (And yes, it will work even if we have 305 words in the array.)

And what do we end up with when the script grinds to a halt? We end up with this:

Note. As it turns out, Test.doc happens to be the column on boldfacing words in a Word document. The Scripting Guys really are dedicated to recycling!

Of course, you might be thinking, “Well, that’s nice. But I just don’t like that yellow highlighting. If only there was a way to use turquoise highlighting instead.”

Hey, all you had to do was ask. It’s actually quite easy to assign a different color to your highlighter. To do that, start by picking a constant and value from the following table:

Constant | Value
wdAuto | 0
wdBlack | 1
wdBlue | 2
wdBrightGreen | 4
wdByAuthor | 1
wdDarkBlue | 9
wdDarkRed | 13
wdDarkYellow | 14
wdGray25 | 16
wdGray50 | 15
wdGreen | 11
wdNoHighlight | 0
wdPink | 5
wdRed | 6
wdTeal | 10
wdTurquoise | 3
wdViolet | 12
wdWhite | 8
wdYellow | 7

Somewhere near the start of your script define that constant, assigning it the corresponding value:

Const wdTurquoise = 3

Then, after you’ve created an instance of the Word.Application object, create an instance of the Options object, assigning the chosen constant to the DefaultHighlightColorIndex property:

Set objOptions = objWord.Options
objOptions.DefaultHighlightColorIndex = wdTurquoise

The completed script should look something like this:

Const
wdReplaceAll = 2
Const wdTurquoise = 3

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objOptions = objWord.Options
objOptions.DefaultHighlightColorIndex = wdTurquoise

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

arrWords = Array("boldface", "the")

For Each strWord in arrWords
objSelection.Find.Text = strWord
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE

objSelection.Find.Replacement.Highlight = True
objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
Next

This will result in all the target words being highlighted with a turquoise highlight. Is it possible to mix highlighter colors in a single document? Yes, but that will have to be a topic for another day.

Now, if someone would just write in and ask how to search for and italicize words in a Microsoft Word document we’d be in business. And if someone else would ask how to underline those words, well ….


How Can I Search For and Modify Formatted Text in a Word Document?

Hey, Scripting Guy! How can I search for formatted text in a Word document and then apply HTML tags to that text?

-- ES

Hey, ES. You know, it’s funny that you should ask this question. Just last night at the dinner table, the Scripting Guy who writes this column finally had The Talk with the Scripting Son. No sooner had they started eating before the Scripting Son blurted out, “Listen, Dad, I’m 17-years-old now, and there are some things I need to know before I become an adult.”

Needless to say, those are the words every father dreads to hear. But duty calls, right? “OK, son,” said the Scripting Dad. “What is it you need to know?”

“I need to know this, Dad: how can I search for formatted text in a Word document and then apply HTML tags to that text?”

Like we said, no parent (or at least no father) really looks forward to having to explain the facts of Microsoft Word scripting life to their child; to be honest, it’s embarrassing to have to talk about things like that with your kids. Nevertheless, being a parent means more than just going to watch your son play baseball. (Although the Scripting Guy who writes this column wishes someone had told him that before he became a parent.) As awkward as it might have been, the Scripting Guy who writes this column sat down and patiently explained how the Scripting Son could search for formatted text in a Word document and then apply HTML tags to that text. (And he tried his best not to sound too preachy about this, although he did note that Microsoft Word search scripts should only be written by someone who truly loves system administration scripting. Admittedly, casual scripting and recreational scripting sound fun, but, more often than not, they simply lead to heartache. The Scripting Dad’s advice? Get married first and then write a script that can search for formatted text in a Word document and then apply HTML tags to that text.)

For those of you who have yet to have The Talk with your own children, here’s a transcript of what the Scripting Dad told his son, beginning with the script itself:

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

objSelection.Find.Forward = True
objSelection.Find.Format = True
objSelection.Find.Font.Bold = True

Do While True
    objSelection.Find.Execute
    If objSelection.Find.Found Then
        objSelection.Text = "<b>" & objSelection.Text & "</b>"
    Else
        Exit Do
    End If
Loop

“OK, son,” the Scripting Dad continued. “Let’s see if we can figure out how the miracle of searching for text in a Microsoft Word document occurs. As you can see, we start out by creating an instance of the Word.Application object and setting the Visible property to True; that gives us a running instance of Microsoft Word that we can see onscreen. After using the Open method to open the file C:\Scripts\Test.doc, we then use this line of code to create an instance of Word’s Selection object (which, by default, also positions the cursor at the beginning of the document):

Set objSelection = objWord.Selection

“What’s that? Are we done yet? No, not quite, but we’re getting there. Next we need to define three key properties of the Find object (a child object of the Selection object). In particular, we need to:

· Set the Forward property to True. This tells the script that we want the search to move forward through the document; because we’re starting our search at the very beginning of the document (we know that, because that’s where the cursor is positioned) that means that we’ll end up searching the entire file.

· Set the Format property to True. This tells the script that we want to search for formatting rather than a specific string value.

· Set the Font.Bold property to True. This tells the script – hey, that’s right: this tells the script that we want to search for boldface text. Say, if you don’t mind my asking, who told you about the Font.Bold property? And is there any chance you could give me a few pointers about Font.Bold?

“After we configure these property values we set up a Do While loop designed to run forever. Or, at any rate, designed to run as long as True is equal to True. That’s why we can’t say that the loop actually will run forever; U.S. politics being what they are these days, it’s just a matter of time before True is no longer considered equal to True. But that’s a lesson for another day.

“Once inside the loop, we call the Execute method; this tells the script to start searching the document, and to keep searching until one of two things happens: it either finds an instance of the target text (something in boldface), or it reaches the end of the document.

“Still with me? Good. Now, let’s suppose that some boldface text is found; in that case, the Find object’s Found property will be set to True. That’s something we can test for using this line of code:

If objSelection.Find.Found Then

“And assuming that we have found some boldface text, that means we need to put the HTML <b> tag at the beginning of that value and the </b> at the end of the value. That’s what we do with this line of code:

objSelection.Text = "<b>" & objSelection.Text & "</b>"

“As you can see, there’s really nothing mysterious about this: in technical terms, we’re simply replacing the value of the Selection object’s Text property (which will be the boldface text; when executing a search any found item is automatically selected) with the current value of the selected text plus our HTML tags. What does that mean? That means that a phrase that starts out looking like this:

Boldface text

“Will end up looking like this:

<b>Boldface text</b>

“We then loop around, call the Execute method, and search for the next instance of the target text. What happens if there isn’t a next instance of the target text (that is, no more boldface text)? In that case the Found property will not be equal to True and, in turn, we call the Exit Do statement to exit the not-so-endless loop.

“And that, Scripting Son, explains how birds and bees manage to search a Word document for formatted text and then put HTML tags around any such text the search uncovers.”

That should do it, ES. Incidentally, the Scripting Guy who writes this column apologizes for not directly answering your question, but instead giving you a transcript of something he told the Scripting Son. The truth is, the Scripting Guy who writes this column was a little embarrassed about answering this question, particularly in front of a large audience. Yes, we know: scripts that search for formatted text in a Word document and then apply HTML tags to that text are a normal (some would argue a beautiful) part of life, and there’s really nothing to be embarrassed about. Be that as it may, there are some subjects that the Scripting Guy who writes this column simply has problems discussing in this column.

Well, we mean besides system administration scripting.


How Can I Search For Red Text in a Microsoft Word Document?

Hey, Scripting Guy! I have a huge document that features occasional lines printed in red. How can I extract just the lines that are printed in red?

-- GH

Hey, GH. To begin with, the Scripting Guy who writes this column would like to say just one thing: never mind. In yesterday’s column the Scripting Guy who writes this column bemoaned the fact that nothing interesting had happened to him in a long, long time. So guess what? Shortly after finishing the column something interesting did happen to him. And, as it turns out, interesting things aren’t always all that much fun.

So what did happen? Well, if he owned a Rolls Royce, he would say that his car experienced a “failure to proceed.” Surprisingly enough, however, the Scripting Guy who writes this column doesn’t own a Rolls Royce. Therefore, he would say this: his car just plain died.

If there’s any solace to be taken in having a dead car, at least there’s this: the death was quick and painless. One moment the Scripting Guy was driving along without a care in the world, the next moment he was driving along without a car in the world. Pulling out of the grocery store parking lot the engine suddenly stopped running and would not restart, period. And before you ask, yes, the radio, the power windows, and all the other electrical stuff continued to work just fine; it was just the engine that no longer worked. Unfortunately, though, an engine that no longer works is about all it takes to bring a car to a dead stop. Talk about a single point of failure, huh?

The preliminary diagnosis is that the car has a broken ECM. The Scripting Guy who writes this column doesn’t know much about ECMs; he didn’t even know what ECM stood for. Since then, however, he has learned at least two things: 1) the ECM is the central computing system that controls pretty much everything in the car, and 2) replacing an ECM will cost at least $2,000.

Hurray!

Anyway, if you’ve been busy trying to think up ways to make this Scripting Guy’s life more interesting, well, thanks but no thanks. Granted, a life where nothing interesting ever happens can be a bit boring at times. Nevertheless, it’s a heck of a lot cheaper.

In fact, from now on the Scripting Guy who writes this column is going to limit his interests to scripting (something his manager has been trying to get him to do for years). For example, wouldn’t it be interesting to have a script that could extract only the lines in a Word document that were printed in red? Let’s see if we can determine just how interesting that would be:

Const wdColorRed = 255

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

objSelection.Find.Forward = True
objSelection.Find.Format = True
objSelection.Find.Font.Color = wdColorRed

Do While True
    objSelection.Find.Execute
    If objSelection.Find.Found Then
        Wscript.Echo objSelection.Text
    Else
        Exit Do
    End If
Loop

Gosh; that’s way more interesting than a broken ECM, isn’t? As you can see, we start out by defining a constant named wdColorRed and setting the value to 255; we’ll use this constant later on to tell Word the color of the text we’re searching for. What if you wanted to search for text of a different color? Tell you what: send us, oh, say, $2000, and we’ll help you out with that.

Or, better yet, seeing as how the Scripting Editor is giving us that look again, don’t send us $2000; instead just check out the wdColor enumeration in the Microsoft Word VBA Language Reference.

Note. But if you do feel like sending us $2000, well ….

After defining the constant wdColorRed we next create an instance of the Word.Application object and then set the Visible property to true; that gives us a running instance of Word that we can view onscreen. We then use these two lines of code to open the document C:\Scripts\Test.doc and create an instance of the Word Selection object, the object we need in order to search the document:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

Cool; this just keeps getting more and more interesting, doesn’t it?

Our next step is to configure the properties of the Find object, which happens to be a child object of the Selection object. That’s what these three lines of code are for:

objSelection.Find.Forward = True
objSelection.Find.Format = True
objSelection.Find.Font.Color = wdColorRed

What does all this mean? Well, to begin with, setting the Forward property to True tells the script to search forward through the document; in other words, start searching at the beginning of the document and continue on through the end of the document. Setting the Format property to True tells the script that we want to search for formatting in addition to (or, in this case, instead of) text. Finally, we specify which formatting we’re interested in: fonts that have a color equal to the constant wdColorRed (Font.Color).

At this point we’re ready to start searching. To that end, the first thing we do is set up a Do While loop that continues to run as long as True is equal to True. (That sounds like an endless loop, but don’t worry: we’ll show you a way out in a minute.) Inside that loop we then use the following command to start the search process and ferret out the first instance of red text:

objSelection.Find.Execute

So how will we know if the Find object actually finds any red text? That’s easy; in that case, the value of the Found property will be True:

If objSelection.Find.Found Then

If the Found property is equal to True then the found text (that is, the red text) will automatically be selected; in turn, all we have to do to report what we found is to echo back the Text property of our Selection object:

Wscript.Echo objSelection.Text

And then we simply loop around, call the Execute method, and then search for the next instance of our target text. This process continues until the Found property is no longer True, which can mean only one thing: there’s no more red text left to find. When that happens we simply call the Exit Do statement and exit our not-so-endless loop:

Exit Do

As you just saw, this script merely echoes back the found text. In his email, though, GH asked how he could go about extracting this text and adding it to a brand-new Word document. Here’s a revised script that can do just that:

Const wdColorRed = 255

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection

Set objDoc2 = objWord.Documents.Add()
Set objSelection2 = objWord.Selection

objSelection.Find.Forward = True
objSelection.Find.Format = True
objSelection.Find.Font.Color = wdColorRed

Do While True
objSelection.Find.Execute
If objSelection.Find.Found Then
strText = strText & objSelection.Text
& vbCrLf
Else
Exit Do
End If
Loop

objSelection2.TypeText strText

So what’s the difference between this script and the first script we showed you? Well, for one thing, this script features two Word documents: C:\Scripts\Test.doc (with an object reference of objDoc) and a new document created using the Add method (with an object reference of objDoc2):

Set objDoc2 = objWord.Documents.Add()

In addition, this time around we don’t echo back the text as we find it; instead, we store all the found text in a variable named strText (tacking a carriage return-linefeed onto the end of each segment):

strText = strText & objSelection.Text & vbCrLf

And then, last but surely not least, after we’ve finished searching Test.doc we use the TypeText method to add all the found text to our second document:

objSelection2.TypeText strText

And there you have it.

As for the ECM, well, the Scripting Guy who writes this column is already learning way more about ECMs than he ever wanted to know (starting with the fact that ECM is apparently short for Electronic Control Module). For example, Volkswagen Passats can’t be hot-wired; you have to start them with the key fob because the fob sends a signal to the ECM that says, “It’s OK; go ahead and start the car.” Unfortunately, though, the ECM will sometimes malfunction and refuse to start the car even though you have the key fob. (And yes, this unnecessary layer of security does remind us of something. But we’re not going to say what.) At other times the ECM can malfunction and apply a “virtual speed governor” that limits the car to a top speed of 5 miles an hour. Needless to say, that’s bad.

Well, unless you live in Seattle. With the traffic we have around here, you rarely reach a top speed of 5 miles an hour anyway.

At any rate, we’ll keep you posted about this … interesting … new development. And remember, if you do want to send us $2,000 go right ahead. Just make sure you address the envelope to The Scripting Guy Who Writes That Column and not to The Scripting Editor. Thanks!


How Can I Set the Document Orientation in Microsoft Word to Landscape?

Hey, Scripting Guy! How can I set the document orientation in Microsoft Word to landscape?

-- EE

Hey, EE. We think the script that we’re about to show you will answer your question; if it doesn’t, however, we’ll hope you cut us a little slack. After all, according to an exercise bike at the local gym, the Scripting Guy who writes this column died last night.

Now, don’t worry: the Scripting Guy actually feels pretty good. (Maybe death is overrated.) What happened is this: Last night the Scripting Guy who writes this column and his Scripting Son went to workout at the gym. After playing some basketball they sat down to ride the exercise bikes. As they started, the Scripting Guy who writes this column noticed that, even though he wasn’t holding on to the heart rate monitor, the exercise bike insisted that his heart rate was 140. Oh, well, he thought; who cares?

A few minutes into the workout, however, he noticed that every 10 seconds or so his heart rate would suddenly drop by a few beats (even though he still wasn’t gripping the heart rate monitor). At first he liked that: after all, having such a low heart rate while working out was proof of the great shape he’s in. As his alleged heart rate continued to drop, however, he began to worry; as his heart rate dipped into the 40s he really began to worry. And then, suddenly, his heart rate started falling, beat-by-beat, until the heart rate monitor measured 0. No heart beat? That didn’t sound very good.

Note. Admittedly, heart rate isn’t necessarily the criteria used to determine whether someone is dead or alive; often times death is indicated by absence of any sort of brain activity. However, that methodology can’t be applied to the Scripting Guy who writes this column; if it could, he would have been declared dead years ago.

So was the Scripting Son alarmed at the fact that, according to the exercise bike, the Scripting Dad no longer had a heart beat? Not exactly. Although they still had a good 10 minutes or so left in their workout the Scripting Dad thought the fact that he was dead should give him the right to quit early. The Scripting Son would have no part of that, however; instead, he insisted that his poor deceased father continue riding for the last 10 minutes, even forcing him – as is their custom – to sprint the final minute.

Which, when you think about it, was really pretty impressive. After all, anyone can ride a bike while they’re alive; however, we’re willing to bet that even Lance Armstrong himself would have trouble riding a bike while dead.

Note. Did the Scripting Guy’s life flash before his eyes when he died? That’s hard to say. Granted, he didn’t see anything interesting as the heart rate monitor counted down to 0. But, then again, he hasn’t really lived a very interesting life. So who knows?

Now, does all that mean that there won’t be a Hey, Scripting Guy! column today? Of course not; you don’t think a little thing like having no heartbeat could stop the Scripting Guys, do you? Here’s a script that sets the page orientation in Microsoft Word to landscape:

Const wdOrientLandscape = 1

Set objWord =
CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

objDoc.PageSetup.Orientation = wdOrientLandscape

As you can see, there’s not much to this script. We start out by defining a constant named wdOrientLandscape and setting the value to 1; we probably don’t need to tell you that we’ll use this constant to switch the page orientation to landscape. What if we decide later on to switch the orientation back to portrait mode? No problem; in that case we just need to use a constant named wdOrientPortrait and set the value of that constant to 0:

Const wdOrientPortrait = 0

After defining our constant we create an instance of the Word.Application object and set the Visible property to True; that gives us an instance of Microsoft Word that we can see on screen. We call the Add method to add a new document to our instance of Word, then use this line of code to change the page orientation to landscape:

objDoc.PageSetup.Orientation = wdOrientLandscape

Thatis easy, isn’t it?

All we’re doing in that line of code is assigning the constant wdOrientLandscape to the Orientation property of the PageSetup object (which happens to be a child object of the Document object). At that point our document will switch to landscape mode.

Of course, if you’re working in landscape mode you might want to do two additional things: you might want to ensure that the document is in Print Layout view (that way you can easily verify that the orientation really has been set to landscape) and you might want to change the Zoom (to ensure that the entire document fits on screen). Here’s a bonus script that changes the View to Print Layout and the Zoom to 80 percent:

Const wdOrientLandscape = 1
Const wdPrintView = 3

Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Add()

objDoc.PageSetup.Orientation = wdOrientLandscape

objDoc.ActiveWindow.View.Type = wdPrintView
objDoc.ActiveWindow.View.Zoom.Percentage = 75

We won’t bother explaining all this today; all we’re really doing is modifying the Type and Zoom properties of the ActiveWindow.View object. If you’d like to use a different view type (that is, something other than Print Layout), well, here’s a complete list of constants and their values:

Constant | Value
wdMasterView | 5
wdNormalView | 1
wdOutlineView | 2
wdPrintPreview | 4
wdPrintView | 3
wdReadingView | 7
wdWebView | 6

Just substitute the appropriate constant (and value) for wdPrintView and have at it.

One last thing. The answer to the age-old question is this: yes, the fact that the Scripting Guy made it in to work today proves that there is life after death. The only drawback? Considering that he made it in to work suggests that our Scripting Guy didn’t quite make it to heaven, if you know what we mean.

And the fact that all the other Scripting Guys are here, too pretty much proves that, doesn’t it?


How Can I Set Word’s Default File Location to be the User’s Home Directory?

Hey, Scripting Guy! How can I set Word’s default file location to be the user’s home directory?

-- LD

Hey, LD. Ah, memories. There was a period of time when it seemed like every question we answered in this column was a two-part question: we’d always find ourselves doing things like showing people how to do task 1 (retrieve the user’s home directory), and then showing them how to combine that with task 2 (set the default file location to the home directory). We don’t seem to answer many two-part questions these days, probably because it’s hard enough to get us to do one thing let alone get us to do two things. But we’ll see what we can do, just for old time’s sake.

To begin with, we’re assuming that you want to do this in a logon script. If that’s the case, then it’s very easy to take care of task 1, retrieving the user’s home directory:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

Wscript.Echo objUser.homeDirectory

As you can see, we begin by creating an instance of the ADSystemInfo object; that’s an ADSI object that can return a number of properties about the logged-on user. (Unfortunately, though, the ADSystemInfo object can be created only on the local machine; that’s why this script works best as a logon script, which always runs locally.) One of the properties returned from ADSystemInfo is UserName, which just happens to be the distinguished name of the logged-on user. That’s going to be something similar to this:

CN=kenmyer, ou=Accounting, dc=fabrikam, dc=com

Is it worth our while to grab the distinguished name and stash it in a variable named strUser? You bet it is; by using the distinguished name along with the LDAP provider we can bind directly to the user’s user account in Active Directory:

Set objUser = GetObject("LDAP://" & strUser)

All we have to do now is echo the value of the homeDirectory attribute.

We’re beginning to recall why we used to do these two-part questions all the time: they’re easy.

So much for task 1. Now let’s put this together with task 2 – setting the default file location for Microsoft Word – and see if we can wrap this up:

On Error Resume Next

Const wdDocumentsPath = 0

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)

If IsNull(objUser.homeDirectory) OR objUser.homeDirectory = "" Then
Else
Set objWord = CreateObject("Word.Application")
Set objOptions = objWord.Options
objOptions.DefaultFilePath(wdDocumentsPath) =
objUser.homeDirectory
objWord.Quit
End If

As you can see, the first few lines of code are pretty much the same ones we used to bind to the user account in Active Directory. The only addition is this line, which defines a constant named wdDocumentsPath:

Const wdDocumentsPath = 0

We’ll use this constant later on to indicate which file location (the one for the user’s documents) that we want to modify.

Next we retrieve the value of the homeDirectory attribute, and check to see if the value is either Null or empty:

If IsNull(objUser.homeDirectory) OR objUser.homeDirectory = "" Then

Why? Well, if the user doesn’t have a home directory then we probably shouldn’t try setting the default file location to that non-existent folder. If the user does not have a home directory then we do nothing all; notice that an Else statement immediately follows the If-Then statement.

But what if the user does have a home directory? In that case, we run these lines of code:

Set objWord =
CreateObject("Word.Application")
Set objOptions = objWord.Options
objOptions.DefaultFilePath(wdDocumentsPath) = objUser.homeDirectory
objWord.Quit

As you can see, we create an instance of the Word.Application object, then create an object reference to the Options object. Notice that we don’t set the Visible property of Microsoft Word to True. That’s intentional: we don’t want Word to appear onscreen. Instead, we want all our configuring to take place in the background.

We then assign the user’s home directory to the DefaultFilePath property. Because DefaultFilePath can refer to multiple file paths (startup path, templates path, pictures path, etc.) we pass along the constant wdDocumentsPath; that tells Word to configure the default documents path. As you might have guessed, we set the value of the default documents path to the user’s home directory:

objOptions.DefaultFilePath(wdDocumentsPath) = objUser.homeDirectory

We then call the Quit method to terminate our instance of Microsoft Word and we’re done.

That was fun, wasn’t it? Remind us to do these two-part questions more often.


How Can I Set Word’s Revision View Mode to Final?

Hey, Scripting Guy! In Microsoft Word 2003, my documents open as Final Showing Markup. How can I change this so the default view is Final?

-- CD

Hey, CD. You know, this is the first Hey, Scripting Guy! column we’ve written since returning from the Thanksgiving holiday. After four days of eating, football, eating, basketball, and then a little more eating (well, OK, a lot more eating), we thought it would be smart to pick a really easy question to answer, something to help us get back into the swing of things. And that would have been smart had we actually picked an easy question. Instead, we chose your question.

As we quickly discovered, this was a bit trickier than we had expected (and had hoped). For one thing, we couldn’t find a way to make Final the global default; that is, to configure Word so that all documents would then open up with the revision view mode set to Final. Instead, it appears that the revision view must be configured on a per-document basis. Therefore, what we’ve given you is a script that opens a specific document and then sets the revision view mode for that document. That’s not quite what you were looking for, but it was the best we could do.

For another, it turns out that the RevisionsView property in Word has only two settings: Final Showing Markup and Original Showing Markup. That’s fine, except that Word itself has four such settings: Final Showing Markup, Original Showing Markup, Original, and – the one you wanted – Final. Did that throw us for a loop? Do you even have to ask?

Fortunately, the Scripting Guys have been thrown for so many loops that things like that don’t bother us anymore. It took a little experimenting and a little poking around the Microsoft Word object model, but eventually we came up with a solution. For example, here’s a script that opens the document C:\Scripts\Test.doc and sets the revision view mode to Final:

Const
wdRevisionsViewFinal = 0

Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open("c:\scripts\test.doc")

Set objView = objWord.ActiveDocument.ActiveWindow.View

objView.RevisionsView = wdRevisionsViewFinal
objView.ShowRevisionsAndComments = False

objWord.Visible = True

As you can see, the script isn’t very long and it isn’t very complex; it’s just a little tricky. We start out by defining a constant named wdRevisionsViewFinal and setting the value to 0; needless to say, we’ll use this constant to indicate whether the RevisionsView property should be set to Final or Final Showing Markup. (Don’t worry, we’ll explain how to get one or the other.) What if we wanted to set the RevisionsView to Original or Original Showing Markup? In that case, we’d need to define a constant named wdRevisionsViewOriginal and set the value to 1:

Const wdRevisionsViewOriginal = 1

Next we create an instance of the Word.Application object and then use the Open method to open the file C:\Scripts\Test.doc. We then use this line of code to create an object reference to the View object for this document:

Set objView = objWord.ActiveDocument.ActiveWindow.View

Now we need to do two things. First, we set the value of the RevisionsView property to 0 (using the constant wdRevisionsViewFinal). Setting RevisionsView to 0 gives us a revisions view mode of Final Showing Markup; setting the RevisionsView to 1 gives us a view mode of Original Showing Markup.

“But wait,” you say, “we don’t want Final Showing Markup; we want just plain old Final.” Understood. In order to get to that mode you first have to configure the RevisionsView property; at that point, you then need to set the ShowRevisionsAndComments property to False:

objView.ShowRevisionsAndComments = False

That should do the trick. After configuring both RevisionsView and ShowRevisionsAndComments we then set the Visible property to True: that makes Test.doc visible, and shows us that the revisions view mode has been set to Final.

Confused? Don’t feel bad; we were confused at first, too. But maybe this little table, which shows the required settings for each view mode, will help:

View Mode | RevisionsView | ShowRevisionsAndComments
Final | 0 | False
FinalShowingMarkup | 0 | True
Original | 1 | False
OriginalShowingMarkup | 1 | True

Play around with it a bit, and you’ll catch on in no time.

Note. Next Thanksgiving we’re definitely going to pick a question that’s a little easier to answer. In fact, for those of you who like to get a jump on the holidays here’s the column for Thanksgiving 2006:

Hey, Scripting Guy! How can I make a message box appear using WSH?
-- GS

Hey, GS. Here’s how:

Wscript.Echo "This is my message box."

If only we’d picked that question this year ….


How Can I Play a Sound From Within a Script?

Hey, Scripting Guy! I have a script that pops up a message box to alert users to a problem. Is there any way I can play a sound at the same time the message box appears?

-- TL

Hey, TL. You guys do this on purpose, don’t you; you love to ask questions where there isn’t a clear-cut answer. This is yet another case of that: you can search high and low through the VBScript and the WSH documentation, but you won’t find any sort of method that allows you to directly play a sound from within a script, not so much as a peep. Or beep, as the case may be.

Because of that, you’ll have to call another utility in order to play a sound. As long as your sound is in the .WAV format, your best bet is to use the Windows Sound Recorder. There are at least two reasons for that. For one thing, the Sound Recorder is lightweight and reasonably quick to load; Media Player is far more powerful, but is probably overkill just to play a beep. For another, by passing the appropriate command-line parameters, you can run Sound Recorder invisibly; as a result, you (or the user) will hear the sound, but won’t see Sound Recorder onscreen.

Here’s a script that will play Notify.wav, a standard operating system sound found in the Windows\Media folder:

strSoundFile = "C:\Windows\Media\Notify.wav"
Set objShell = CreateObject("Wscript.Shell")
strCommand = "sndrec32 /play /close " & chr(34) & strSoundFile & chr(34)
objShell.Run strCommand, 0, True

The first two lines of the script are pretty simple. In the first line, we just assign the file path to the variable strSoundFile; in the second line, we create an instance of the Wscript Shell object, the object we’ll use to actually run the Sound Recorder.

Things get a tiny bit trickier in line 3. To run Sound Recorder from the command-prompt, we need to type in this command:

sndrec32 /play /close "C:\Windows\Media\Notify.wav"

The first two command-line parameters tell Sound Recorder to play the file and then terminate itself; the third parameter, of course, is the name of the file we want to play. In this example, we don’t need to enclose the file path in double quotes; that’s only required if there are spaces in the path name. We went ahead and enclosed it in double quotes to give you a template you can use when playing a sound file that does have a space in the path name. Thus:

sndrec32 /play /close "C:\Windows\Media\Windows XP Error.wav"

In order to enclose the file path in double quotes, we use the Chr(34) command, which, well, inserts a double quote in our string. Hence this line of code, which strings together sndrec32 /play /close, a double quote mark (“) the file C:\Windows\Media\Notify.wav, and another double quote mark (”):

strCommand = "sndrec32 /play /close " & chr(34) & strSoundFile & chr(34)

Finally, in line 4 we use the Run method, passing along the command string we just constructed. The 0 parameter causes Sound Recorder to run in a hidden window; the True tells the script to wait until the sound finishes playing before resuming.

As for playing a sound at the same time a message box appears, well, you’ll find that close-to-impossible; that’s because it takes a second or two for the Sound Recorder to load and start playing. Your best bet is to do two things. First, use the False parameter when calling the Sound Recorder; that will tell the script to start the recorder and then continue on, without waiting until the sound finishes playing.

Second, use the Wscript.Sleep command to pause the script for a second or so; that will give the Sound Recorder time to load and – with luck – the sound will start playing right about the time the message box appears. Will it start playing at exactly the same time the message box appears? Probably not. But at least you can come close.

Here’s a sample script that plays a sound and – at about the same time – pops up a message box as well:

strSoundFile = "C:\windows\Media\Notify.wav"
Set objShell = CreateObject("Wscript.Shell")
strCommand = "sndrec32 /play /close " & chr(34) & strSoundFile & chr(34)
objShell.Run strCommand, 0, False
Wscript.Sleep 1000
Msgbox "A problem has occurred."


How Can I Tell Whether a Computer has a Specific CD in It?

Hey, Scripting Guy! How can I tell whether or not a computer has a CD named MyApps in it?

-- CB

Hey, CB. You know, this is one of those questions that the Scripting Guys almost feel bad about answering. Why? Well, at first glance this sounds like it might be a really complicated problem to solve; as it turns out, however, it’s pretty easy, so easy that we almost feel that it’s unfair for us to write a column on a topic that elementary. After all, we’re the Scripting Guys: we should be taking on nothing but the toughest and most-demanding of challenges. The Scripting Guys take the easy way out? Never!

Like we said, though, we almost feel bad about answering it:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_CDROMDrive Where VolumeName = 'MyApps'")

Wscript.Echo colItems.Count

The reason this question is so easy to answer is because the WMI class Win32_CDROMDrive includes a property named VolumeName. If a CD happens to be in a CD drive then the VolumeName property will be equal to the name of that CD (or at least the name of the CD as it appears in Windows Explorer). If we want to know if a CD named MyApps happens to be loitering in a computer all we have to do is count up the number of CDs on that computer that have a VolumeName equal to MyApps. If the number is 0, then the CD is not inserted on any of the drives on that computer. If the number is greater than 0 then a CD by that name is inserted in the computer.

That pretty much sums up everything this script does. We start out by binding to the WMI service on the local computer. It goes without saying that you could perform this same task on a remote machine; in fact, that’s where the script really comes in handy. (If you want to know if a CD is inserted in your local machine it’s probably easier to just open the drive and take a peek.) Even better, suppose this CD might have been left in any of one of a hundred different computers. In that case you might want to paste this code into one of our multi-computer script templates and search each of those 100 or so machines. For more information on doing that see, well, our multi-computer script templates page.

OK. After connecting to the WMI service we then use this line of code to retrieve a collection of all the CD drives that have a VolumeName equal to MyApps (again, the VolumeName is the name of the CD as it appears in Windows Explorer):

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_CDROMDrive Where VolumeName = 'MyApps'")

At that point all we have to do is echo back the Count, which tells us the number of MyApps CDs found on the computer.

Boy, we did end a little bit early today, didn’t we? OK, how about this? Seeing as how we mentioned the multi-computer templates, here’s a revised script that checks for the MyApps CD on all the computers listed in a text file named Servers.txt:

On Error Resume Next

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("c:\scripts\servers.txt", ForReading)

Do Until objTextFile.AtEndOfStream
    strComputer = objTextFile.Readline

    ' =====================================================================
    ' Insert your code here
    ' =====================================================================

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

    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_CDROMDrive Where VolumeName = 'MyApps'")

    Wscript.Echo strComputer, colItems.Count

    ' =====================================================================
    ' End
    ' =====================================================================

Loop

objTextFile.Close

As you can see, all we had to do was insert our basic code (slightly revised, in that we echo back the computer name as well as the number of CDs found) into the section of the template labeled Insert your code here. The script will then go out and search for the CD on every computer listed in the text file.

Could anything be easier? Probably not. Which means we almost feel like we should take on a harder question next time out. Almost.


How Can I Use a Script to Automatically Queue Up Files That I Want to Burn to a CD?

Hey, Scripting Guy! In Windows XP, how can I use a script to automatically queue up files that I want to burn to a CD?

-- BD

Hey, BD. Well, unfortunately, we have some bad news for you: there’s currently no way to burn a CD using a script. You see, the problem is that – oh, wait a second: you just want to queue up the files that will be burned to the CD, don’t you? Well, that’s very different. And, for we Scripting Guys, a huge relief; after all, people ask us every other day about using scripts to burn CDs, and the answer is always the same: you can’t. That’s why we started this column off by telling you can’t burn a CD using a script; we’ve had to tell so many people that you can’t burn CDs using a script that this has become a reflexive reaction. But simply collecting all the files and getting them ready to be burnt to a CD? Well, that we can help you with.

Note. And for those of you who want to use scripts to burn CDs, well, we have at least some good news: this capability is coming in Windows Vista. In fact, in Windows Vista you’ll be able to use scripts to burn DVDs as well as CDs. Granted, this doesn’t help you too much today, but good things come to those who wait, right? You just need to wait a little bit longer and you’ll be able to burn all the CDs and DVDs you want.

As for queuing up files that will be burned to a CD, well, this little script should do the trick:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
strValueName = "CD Burning"

objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strBurnFolder

strFolder = "C:\Test"

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

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strFolder & "'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In colFileList
    strCopy = strBurnFolder & "\" & objFile.FileName & "." & objFile.Extension
    objFile.Copy(strCopy)
Next

There’s really no secret to queuing up files for burning: all you have to do is copy those files to the CD Burning folder (which will typically have a path like C:\Documents and Settings\kenmyer\Local Settings\Application Data\Microsoft\CD Burning). After you’ve determined the location of the CD Burning folder (something you can do by reading the registry) the rest is simply a matter of copying files to this folder.

With that in mind, let’s start with the first part of our task, determining the location of the CD Burning folder. In order to create a script that can queue up files on remote computers as well as on the local computer, we decided to use WMI to determine the folder location. To do that, we start out by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this tells the script which registry hive we want to work with. We then bind to the WMI service on the local computer (although, again, we could just as easily run this script against a remote machine), and connect to the System Registry provider. That’s what this code does:

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Next we need to assign values to a pair of variables. The variable strKeyPath is assigned the value Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders; that just happens to be the path (within HKEY_CURRENT_USER) to the registry value we need to read. Meanwhile, the variable strValueName is assigned the value CD Burning; that just happens to be – oh, that’s right, that’s the name of the registry value we want to read.

Man, here we are trying to show off how much we know about scripting, only it turns out you guys know every bit as much as we do. Dang!

As you doubtless know then, we can now use the GetStringValue method to read the registry value and determine the path to the CD Burning folder:

objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strBurnFolder

GetStringValue takes four parameters: the constant we defined at the beginning of our script, our two variables (strKeyPath and strValueName), and an “out” parameter named strBurnFolder. Being an out parameter we don’t assign a value to strBurnFolder; instead, the GetStringValue method will automatically assign the location of the CD Burning folder (as read from the registry) to that variable.

But you already knew that, didn’t you?

After we know the location of the CD Burning folder the next step is to copy files to that folder. For the sake of simplicity we’re going to assume that we want to copy all the files found in the folder C:\Test. To do that we first assign the path C:\Test to a variable named strFolder, then make a second connection to the WMI service, this time binding to the root\cimv2 namespace. After making that connection we can then use this query to return a collection of all the files found in the folder C:\Test:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strFolder & "'} Where " _
        & "ResultClass = CIM_DataFile")

At this point it gets just a tiny bit tricky. Why? Because copying files using WMI requires you to specify the complete path name of the copied file. For example, suppose we want to copy the file C:\Test\File1.txt. The only way we can copy this file is to provide WMI with the full path to the new file: C:\Documents and Settings\kenmyer\Local Settings\Application Data\Microsoft\CD Burning\File1.txt. Like we said, that problem is a tiny bit tricky, but it’s one we can solve easily enough. And here’s how.

To begin with, we set up a For Each loop to loop through all the files in the folder C:\Test. Inside that loop we start off by using this line of code to construct the path for the copied file:

strCopy = strBurnFolder & "\" & objFile.FileName & "." & objFile.Extension

You can see what we’re doing here. We start off by taking the location of the CD Burning folder (represented by the variable strBurnFolder) and then add a trailing \ to the folder name. We then tack on the value of the FileName property (for example, File1) followed by a dot (.) and the value of the Extension property (for example, txt). After all that the variable strCopy will contain a value similar to this:

C:\Documents and Settings\kenmyer\Local Settings\Application Data\Microsoft\CD Burning\File1.txt

Which, by remarkable coincidence, just happens to be the path to the copied file.

Once we have that path we can then pass the value to the Copy method and copy the file to the CD Burning folder:

objFile.Copy(strCopy)

From there we loop around and repeat this process for the next file in the collection. After we’ve looped through the entire collection all the files in C:\Test will have been copied to the CD Burning folder, and you’ll be ready to burn those files to a CD.

Or wait until Windows Vista is released and then use a script to burn those files to a CD. We’ll leave that up to you. And, yes, you can bet that we’ll fill you in on how to use scripts to burn CDs and DVDs in Windows Vista; trust us, after years of delivering people the same bad news (“No, sorry, you can’t use a script to burn a CD”) we can’t wait to give people a better answer. At long last we’ll have an answer for every question you guys can throw at us.

What’s that? You want to know how manage a DHCP server using a script? OK, maybe not every question ….


How Can I Determine the Height and Width of a Picture File Without Using Windows Media Player?

Hey, Scripting Guy! Awhile back you showed us a method to determine the height and width of a picture file. However, that solution required us to import all our picture files into Windows Media Player. Isn’t there another way to get this information, one that doesn’t require Windows Media Player?

-- RP

Hey, RP. You see, that’s what happens any time we Scripting Guys try to be clever. What with the burgeoning popularity of digital photography, scripters are very interested in retrieving the height and width of their image files using a script. Unfortunately, there’s no way – or at least no way built into the operating system – to gather that information. That’s a problem. So we came up with a nifty little workaround: if you import those pictures into Windows Media Player, then you can use Windows Media Player’s object model to get the height and width. Problem solved!

Or so we thought. As it turns out, though, not everyone was as excited as we were about importing all their picture files into Windows Media Player. (We should point out that this doesn’t move or modify the files; it just lets Windows Media Player know that those files exist.) On top of that, Windows Media Player lets you import (and thus get information about) only .JPG files. Do you have .GIF, .PNG, .TIF, or .BMP files you’d like to work with? Well, ….

Therefore, we’ve searched for another way to determine the height and width of a picture file. Does this one come with a catch, too? Unfortunately, yes: you’ll need to download and install the Windows Image Acquisition Automation Library 2.0. Once that’s installed, though, you’ll be able to determine the height and width of a picture file using four simple lines of code:

Set objImage = CreateObject("WIA.ImageFile")
objImage.LoadFile "C:\Scripts\Test.gif"

Wscript.Echo "Width: " & objImage.Width
Wscript.Echo "Height: " & objImage.Height

Pretty easy, and it makes you wonder why this library isn’t just added to the operating system, doesn’t it? We can’t answer that one; all we can do is explain how the script works. As you can see, there isn’t much to it. We begin by creating an instance of the WIA.ImageFile object, then using the LoadFile method to bind to an image file. That’s very easy: all we have to do is pass LoadFile the path to the picture.

And then we simply echo back the values of the Width and Height properties. Like we said this works with .GIF, .PNG, .TIF, and .BMP files as well as .JPG files; in fact, if you look closely at our example code, you’ll see that we actually connected to a .GIF file. Will wonders never cease?

Incidentally, there are a few additional picture file properties you can retrieve using Windows Image Acquisition; for more information, take a peek at the WIA documentation on MSDN. And we promise: no more trying to be clever. We’ve learned our lesson!


How Can I Change the Copy Music to This Location Folder in Windows Media Player?

Hey, Scripting Guy! How can I change the Copy music to this location folder in Windows Media Player?

-- BD

Hey, BD. You know, a lot of people think that the reason we Scripting Guys don’t do very much work is because we’re just plain lazy. Those people are right. However, what we tell our bosses is this: it’s hard for us to do any work because every time we write a script we get hundreds of questions related to that script. That proved to be the case when we released our article – The Scripting Guys Really Do Rock – that talked about scripting Windows Media Player. No sooner did we release the article than we were inundated with millions of questions about scripting Windows Media Player. Trust us, it’s not easy for a bunch of lazy guys to answer millions of questions.

Editor’s Note: “Millions” might be exaggerating just a little.

But while we look into doing more articles on Windows Media Player we’ll try to address a few of these million or so questions in this column. As you noted in your email, BD, you’re having a problem with users automatically saving music files to your file servers; you’d like to be able to set the default location for saving music files and you’d like to be able to do it using a script.

Before we go any further we should point out that in Windows Media Player 10 this option is no longer called Copy music to this location; instead, it’s called Rip music to this location. You can find the default folder setting in the Options dialog box on the Rip Music tab:

As you can see, on this computer new music files will be saved, by default, to the folder D:\Music.

So can you change the default location using a script? As a matter of fact, you can. The value for the rip music folder is actually stored in the registry; you’ll find it here:

HKEY_CURRENT_USER\Software\Microsoft\MediaPlayer\Preferences\CDRecordPath

And, yes, for some reason the registry value is named CDRecordPath. But the name doesn’t matter; all that matters is that we can use a script to connect to this registry value and then set the default folder path to any valid folder we have access to. (Note that if you’ve never changed the path, you might not see CDRecordPath in your registry until after you’ve changed the value. But trust us, this will still work.)

In fact, here’s a script that changes the default folder setting to C:\Audio:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & _
    strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\MediaPlayer\Preferences"
strValueName = "CDRecordPath"
strValue = "C:\Audio"
objRegistry.SetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strValue

We begin by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this later on to indicate which portion of the registry we want to work with. We then connect to the WMI service, binding to the standard registry provider class (StdRegProv) in the root\default namespace. We then assign values to three variables:

· strKeyPath, which is assigned the registry path within HKEY_CURRENT_USER (Software\Microsoft\MediaPlayer\Preferences).

· strValueName, which is assigned the name of the registry value we want to change (CDRecordPath).

· strValue, which is assigned the new folder location (C:\Audio).

Next we call the SetStringValue method, passing the HKEY_CURRENT_USER constant and the preceding three parameters. That will change the value in the registry and, in turn, change the default folder location for copying (or, if you prefer, ripping) music.


How Can I Change the Title Property of a

How Can I Change the Title Property of a .WMA File?

Hey, Scripting Guy! How can I change the Title property of a .WMA file?

-- LL

Hey, LL. Here’s a piece of trivia for you. Originally, the Beatles’ classic Yesterday was titled Scrambled Eggs. (Start singing the song, but instead of singing “Yesterday” sing “Scrambled eggs.”) Before the group recorded the song, however, Paul wrote some lyrics and, in the process, changed the name as well. The rest, as they say, is history.

No doubt your next question is this: did Paul McCartney use a script to change the title of Scrambled Eggs? To tell you the truth, we don’t know; we always mean to ask Paul that, but every time we get together it seems like something comes up and we forget to ask. However, there’s no doubt that he could have used a script like this one to change the title:

Set objPlayer = CreateObject("WMPlayer.OCX"
)

Set objMediaCollection = objPlayer.MediaCollection
Set objTempList = objMediaCollection.getByName("Scrambled Eggs")

Set objSong = objTempList.Item(0)
objSong.setItemInfo "Name", "Yesterday"

Before we go any further we should point out that this script changes the song title only as far as Windows Media Player is concerned. When you view your song list in Windows Media Player the song formerly known as Scrambled Eggs will now appear as Yesterday. However, this script doesn’t change the .WMA file itself: the file name remains as-is, and if you right-click the file and look at the Summary Properties the title will still show up as Scrambled Eggs. Unfortunately, we don’t know of any way to change the Title property of the file itself; the best we can do is change the way the song title appears in Windows Media Player.

Note. What’s that? You didn’t even know that you could script Windows Media Player? You guys haven’t been to Dr. Scripto’s Fun Zone have you?

If you’re OK with just changing the song as it appears in Windows Media Player, well then, read on. As you can see, our script begins by creating an instance of the WMPlayer.OCX object; this is simply the object that provides programmatic access to Windows Media Player. We then use these two lines of code to bind to the Windows Media Player media collection and to return a list of all the songs that have the title Scrambled Eggs:

Set objMediaCollection = objPlayer.MediaCollection
Set objTempList = objMediaCollection.getByName("Scrambled Eggs")

And yes, we do need to return a collection: that’s because Windows Media Player actually allows you to have multiple songs with the same title. We’re going to assume that you’ve given all your songs unique titles; that enables us to take a little bit of a shortcut when binding to the song and changing your title. If you do have multiple songs with the title Scrambled Eggs then you can use this modified script to change the title for each of those songs:

Set objPlayer =
CreateObject("WMPlayer.OCX" )

Set objMediaCollection = objPlayer.MediaCollection
Set objTempList = objMediaCollection.getByName("Scrambled Eggs")

For i = 0 to objTempList.Count - 1
Set objSong = objTempList.Item(i)
objSong.setItemInfo "Name", "Yesterday"
Next

Now, back to the script that assumes you have only one song named Scrambled Eggs. If that’s the case then we need only this line of code to bind directly to the single song with that title (completely bypassing the For Next loop we had to use in the preceding script):

Set objSong = objTempList.Item(0)

Once we’ve made the connection, changing the title is as simple as using the setItemInfo method to change the song Name to Yesterday:

objSong.setItemInfo "Name", "Yesterday"

The rest, as they say, is history.

Incidentally, the Scripting Guys also like the name Scrambled Eggs better; if nothing else, it goes nicely with the song I Am the Walrus (“I am the egg man, they are the egg men”). But the Beatles never asked the Scripting Guys for their opinion on this one. We just hope things worked out OK for them even without our help.


How Can I List All the Songs in the Windows Media Library?

Hey, Scripting Guy! How can I get a list of all the songs (and their artists) in the Windows Media Library?

-- KF

Hey, KF. Three words: practice, practice, practice.

You know, the old joke about how do you get to Carnegie Hall and this is kind of a music-related question and Carnegie Hall is where they play music and we were just trying to be cute and… well ….

OK, never mind. Instead of practice, practice, practice, how about these three words: Use this script.

By which we mean this script right here:

Set
objPlayer = CreateObject("WMPlayer.OCX" )

Set objMediaCollection = objPlayer.MediaCollection
Set colSongList = objMediaCollection.getByAttribute("MediaType",
"audio")

For i = 0 to colSongList.Count - 1
Set objSong = colSongList.Item(i)
Wscript.Echo objSong.Name & " -- " &
objSong.getItemInfo("WM/AlbumArtist")
Next

The script begins by creating an instance of the WMPlayer.OCX object, which turns out to be the way you instantiate Windows Media Player in VBScript. We then use this line of code to make a connection to the Media Library:

Set objMediaCollection = objPlayer.MediaCollection

As you probably know, the Media Library can contain all sorts of things: JPEG pictures, video files, audio files, etc. Because we only care about music files (like .MP3 and .WMA files) we use this line of code to return a subset of the Media Library, a subset containing only audio files:

Set colSongList = objMediaCollection.getByAttribute("MediaType", "audio")

As you can see, all we do is call the getByAttribute method, passing two parameters:

· “MediaType”, which represents the attribute we’re interested in.

· “audio”, which represents the value of the attribute we’re interested in.

In other words, “Bring me back all the objects where the MediaType is equal to audio.”

The getByAttribute method returns an array of media items, each one representing an individual audio file. To retrieve information about these media items we need to set up a For Next loop that loops from 0 to the Count of the media items minus 1. (As with most arrays in VBScript, the first item in our array is item 0; therefore the last item will be the number of items minus 1. For example, if we have 100 items in the array then the last item will be number 99.)

Each time through the loop we bind to an individual audio file using this code:

Set objSong = colSongList.Item(i)

For each audio file we then echo back the name and the artist. You might note the crazy way we have to get the artist name: we have to call the getItemInfo method and specify the WM/AlbumArtist attribute. Why? Well, just like your mom and dad used to tell you: because. That’s just the way the Media Player object model works.

When we run this script we get back information similar to this:

Losing My Religion -- R.E.M.
Garden Party -- Rick Nelson
Teacher Teacher -- Rockpile
Let's Spend the Night Together -- The Rolling Stones
Anybody Seen My Baby? -- The Rolling Stones
It's Only Rock 'N Roll (But I Like It) -- The Rolling Stones

Cool, huh?

We realize that this script is a bit different from most of the scripts we cover in this column, and we realize that we might have scrimped on the explanation from time-to-time. If you’re truly interested in using scripts to manage your Windows Media playlists and audio files then check out this article in Dr. Scripto’s Fun Zone.

And don’t forget: practice, practice, practice.


How Can I Associate a Network Connection and an IP Address?

Hey, Scripting Guy! How can I determine the local area connection that’s associated with a particular IP address?

-- SH

Hey, SH. Believe it or not, this is a fairly complicated procedure, and for two reasons. To begin with, we have to use two separate WMI classes – Win32_NetworkAdapter and Win32_NetworkAdapterConfiguration – in order to retrieve the information we need. That’s because network connection information is stored in one class (Win32_NetworkAdapter) and IP addresses are stored in another (Win32_NetworkAdapterConfiguration). On top of that, Windows considers all sorts of things – wired connections, wireless connections, VPN connections, FireWire ports, you name it – to be network connections. This means we have quite a bit of data to wade through before we can link a specific network connection to a specific ID.

Oh, yeah: and IP addresses are stored as arrays. This isn’t a major problem, but it does represent one more hurdle we have to overcome.

OK, now that you all feel sufficiently sorry for us (“Oh, those poor Scripting Guys; they have to work so hard to try and answer our questions!”) let’s show you a script that reports back the network connection associated with the IP address 192.59.244.247:

strComputer = "."
strTargetAddress = "192.59.244.247"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

For Each objItem in colItems
    arrIPAddresses = objItem.IPAddress
    For Each strAddress in arrIPAddresses
        If strAddress = strTargetAddress Then
            strMACAddress = objItem.MacAddress
        End If
    Next
Next

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapter Where MACAddress = '" & strMACAddress & "'")

For Each objItem in colItems
    If Not IsNull(objItem.NetConnectionID) Then
        Wscript.Echo objItem.NetConnectionID
    End If
Next

After assigning values to the variables strComputer and strTargetAddress (which represent the computer we’re working with and the target IP address, respectively) we bind to the WMI service and then use the ExecQuery method to select all instances of the Win32_NetworkAdapterConfiguration class where the IPEnabled property is equal to True. This returns only TCP/IP-enabled network adapters, and weeds out things like FireWire connections.

We then set up a For Each loop to cycle through the returned collection. Inside that For Each loop we assign the value of the IPAddress property of our first network adapter to an array variable named arrIPAddresses. (Don’t be fooled by the named IPAddress; this property can contain more than one IP address, which is why we need to use an array to get at the values.) We then set up a second For Each loop to loop through the array of IP addresses. For each address we check to see if the value matches our target IP address (192.59.244.247). If it does, we assign the value of the MACAddress property to a variable named strMACAddress. We continue this process until we’ve checked all the IP addresses for all the network adapters.

When we exit the loop we will know the MAC address of the network adapter associated with our target IP address. Believe it or not, that’s the key to unlocking this mystery. Why? Because the MACAddress property is found in both of our WMI classes. As soon as we know the MAC address associated with the IP address 192.59.244.247 all we have to do find this same MAC address somewhere in the Win32_NetworkAdapter class. When we do that, we can then determine the name of the network connection associated with that IP address.

And that’s exactly what we do with this second WMI query, one that retrieves all instances of the Win32_NetworkAdapter class where the MACAddress is equal to the MAC address associated with our IP address:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapter Where MACAddress = '" & strMACAddress & "'")

Relax; we’re almost done here. We now loop through the returned collection, eliminating any instances where the NetConnectionID property is null. Eventually we’ll get back a message like this one:

OK, sure, it doesn’t look like much. But this just happens to be the name of the network connection associated with the IP address 192.59.244.247. And that ought to be worth something, right?

Incidentally, you can find some additional information about associating network connections and network adapters in the whitepaper Automating TCP/IP Networking on Clients. Click the link and then search for Associating the Network Connections Name with MAC and IP Addresses Using Two Classes.


How Can I Change the First Two Octets of an IP Address?

Hey, Scripting Guy! How can I change the first 16 bits on an IP address? I want to keep the last 16 bits exactly the same.

-- LD

Hey, LD. Before we answer your question we should note that we are well aware that “suggestive” photos of an American Idol contestant have allegedly been uncovered on the Internet. Needless to say, we know many of you are concerned that a similar scandal might erupt around the Script Center and the Scripting Guys. We thought we should take a moment to assure you that you have nothing to worry about: there are absolutely no “suggestive” photos of the Scripting Guys to be found anywhere on the Internet. None.

Not that we didn’t try to post some, mind you. Interestingly enough, however, it turns out that there are some things that even the Internet won’t accept!

Now that we’ve got that out of the way let’s talk about changing the first 16 bits of an IP address. According to your email, LD, you have a bunch of servers that have IP addresses similar to this:

10.10.1.2

You’d like to change these “10.10” IP addresses to “192.168” IP addresses. In other words, you’d like to take an IP address like 10.10.1.2 and turn it into an IP address like this:

192.168.1.2

As you noted, the first two octets change; the last two octets remain the same.

So can you do that using a script? You bet you can:

strComputer = "."

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

Set colNetAdapters = objWMIService.ExecQuery
_
("Select * From Win32_NetworkAdapterConfiguration Where
IPEnabled=True")

arrSubnetMask = Array("255.255.255.0")

For Each objNetAdapter in colNetAdapters
For Each strAddress in objNetAdapter.IPAddress
arrOctets
= Split(strAddress, ".")
If arrOctets(0) =
"10" and arrOctets(1) = "10" Then
strNewAddress = "192.168." & arrOctets(2)
& "." & arrOctets(3)
arrIPAddress = Array(strNewAddress)
errEnable = objNetAdapter.EnableStatic(arrIPAddress, arrSubnetMask)
End If
Next
Next

Let’s see if we can figure out how this all works. As you can see, we start out by connecting to the WMI service. In this sample script we connect to the WMI service on the local computer; however, simply by assigning a remote computer name to the variable strComputer you can just as easily run this script against a remote machine. And can you also adapt this script so that it runs against a bunch of machines? Of course you can; that’s what our Multiple/Remote Computer Scripting Templates are for.

After connecting to the WMI service our next task is to retrieve a collection of all the network adapters installed on the computer. That’s what this little chunk of code is for:

Set colNetAdapters = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled=True")

You might have noticed that our query includes the clause Where IPEnabled = True. Why did we include that clause? Well, the Windows operating system features all sorts of “virtual” network adapters, items that get picked up by the Win32_NetworkAdapterConfiguration class. These aren’t real network adapters, at least not in the sense that they are tied to an actual network adapter card and are assigned an IP address. If you’ve ever run a script that uses the Win32_NetworkAdapterConfiguration class you’ve likely gotten back 10 or more items, even if your computer only has one network card installed. That’s because of all these virtual adapters. By restricting returned data to network adapters where the IPEnabled property is True we filter out everything except “real” network cards.

After we execute our query and get back a collection of all the network adapters on the computer we then take a slight detour, using this line of code the create an array named arrSubnetMask:

arrSubnetMask = Array("255.255.255.0")

Why do we do that? Because when we assign a new static IP address to a computer we need to pass two parameters: the new IP address and the new subnet mask. And both of those values must be passed as arrays, even if we only have one such IP address or subnet mask.

Speaking of assigning new IP addresses, here’s how we do that. To begin with, we set up a For Each loop to walk through the collection of network adapters. Inside that loop we then set up a second For Each loop, this one designed to walk through all the IP addresses assigned to each network adapter:

For Each strAddress in objNetAdapter.IPAddress

Inside this second loop the first thing we do is use the Split function to split each IP address into an array:

arrOctets = Split(strAddress, ".")

By splitting on the period we end up with an array (named arrOctets) consisting of four separate items, each item corresponding to an octet in the IP address. For example, with an IP address of 10.10.1.2 we end up with these four items in our array:

· 10

· 10

· 1

· 2

At this point it’s easy to determine whether or not we are dealing with a “10.10” address; all we have to do is check to see if the first item in the array (item 0) and the second item in the array (item 1) are both equal to 10:

If arrOctets(0) = "10" and arrOctets(1) = "10" Then

Note. Depending on your setup, you might not have to do this. We added this step to ensure that we don’t automatically change every IP address assigned to an adapter; instead, we only change 10.10 addresses. If it’s OK to change every IP address, including addresses that might not be on the 10.10 subnet, then you can leave this If-Then statement out.

Now, what happens when we find a 10.10 address? Well, to begin with, we use this line of code to construct a new IP address:

strNewAddress = "192.168." & arrOctets(2) & "." & arrOctets(3)

You can see what we’re doing here. We’re simply taking the string 192.168. and tacking on: 1) the third element in our array (1); 2) a period; and 3) the fourth element in our array (2). That’s going to make strNewAddress equal to 192.168.1.2, which just happens to be the new IP address we want assigned to this network card.

In order to make that assignment, we add this new address to an array named arrIPAddress, then call the EnableStatic method to assign the new address and subnet mask to the adapter:

arrIPAddress = Array(strNewAddress)
errEnable = objNetAdapter.EnableStatic(arrIPAddress, arrSubnetMask)

And that’s all we have to do. We then loop around and repeat the process with the next IP address for the card (assuming there is another such address). When we’re done there we then loop around and do the same thing with the next adapter in the collection.

We should note that all we’ve done here is show you how to assign a new IP address (or addresses, as the case may be) to a computer. In switching IP addresses you might also need to do other tasks, such as changing DNS servers. We won’t cover any of those other tasks in this column, but you can find information on practically everything related to network adapters by looking at Peter Costantini’s epic masterpiece Automating TCP/IP Networking on Clients.

We should also clarify our earlier statement. The truth is that you won’t find any suggestive photos of the Scripting Guys on the Internet because the Scripting Guys don’t actually exist; instead, they are simply a marketing gimmick cooked up by Microsoft. As it turns out, all the material found in the Script Center, including the Hey, Scripting Guy! column, is created by a computer running a trial version of Office 2025.

But that shouldn’t come as a surprise to most of you; we’re sure that you’ve suspected all along that no human being would ever write a daily scripting column like this one.


How Can I Change the IP Address Assigned to a Computer?

Hey, Scripting Guy! How can I change the IP address assigned to a computer?

-- RW

Hey, RW. To tell you the truth, this is a very difficult question for the Scripting Guys to answer. Not because this is a hard script to write; as you’ll see in a minute, it’s actually pretty easy. No, this is hard for the Scripting Guys because it involves change, and when you’re a Scripting Guy, well, why would you ever want to change anything? As you might expect, we all drive imported sports cars, we all live in opulent mansions, and we all light our cigars with $100 bills. Do we have the perfect lives? You bet we do: even our IP addresses are perfect. Why would we want to change any of that, IP addresses included?

Note. OK, we admit it: we may have exaggerated a little here. Sometimes – and we’re a bit embarrassed to admit this – we light our cigars using $10 bills. The Scripting Guys deeply regret any inconvenience our previous statements might have caused.

Of course, unlike most Microsoft people the Scripting Guys aren’t totally out-of-touch with the real world: we realize that there are a few people here and there who don’t have several million dollars in their bank account. (Not that we’ve met any of these people, mind you, but we saw some of them on TV. Or at least we think we did; we quickly switched the channel to Lifestyles of the Rich and Famous Scripters.) That means that, for some people, change is good. And it goes without saying that changing their IP address is even better:

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

Set colNetAdapters = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled=True")

strIPAddress = Array("192.168.1.0")
strSubnetMask = Array("255.255.255.0")

For Each objNetAdapter in colNetAdapters
    errEnable = objNetAdapter.EnableStatic(strIPAddress, strSubnetMask)
Next

Before we explain how this script works we should issue one important caveat. We’re assuming (based simply on the fact that you didn’t say otherwise) that all you need to do is change the IP address; in other words, we’re assuming that you don’t need to change your IP gateway, that you don’t need to change your DNS servers, that you don’t need to change your WINS servers, etc. All of those things (and more) can be modified using a script, but it would take a few hundred pages for us to detail all the possibilities. And we’d be willing to do that, too, except that the legendary Peter Costantini has already done it for us; check out Peter’s classic tome Automating TCP/IP Networking on Clients for more information.

And no, although he could have, Peter didn’t hire any consultants: he came up with that title all by himself.

Really.

Now, back to the business at hand. Like we said, this is a pretty simple little script. We start out by connecting to the WMI service on the local computer, although we could use this same script to assign a new IP address to a remote machine. (How? Just assign the name of the remote machine to the variable strComputer.) We then use this line of code to return a collection of all the IP-enabled network adapters on the computer:

Set colNetAdapters = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled=True")

Here’s another caveat. This script assumes you have only one IP-enabled network adapter on the computer. In this day-and-age, however, it’s very possible for you to have more than one network adapter installed in a machine. If that’s the case then you should modify this query to return just the network adapter that’s supposed to get the new IP address. How do you do that? Well, an easy way is to specify the adapter Description in the query:

Set colNetAdapters = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where Description = " & _
        "'Broadcom NetLink (TM) Gigabit Ethernet'")

So couldn’t we have used the Description property in our query to begin with? Sure, it’s just that we haven’t memorized the descriptions for our network adapters. (Good point: what were we thinking?) Because of that it was easier for us to use IPEnabled=True. Like we said, though, that won’t work if you have more than one IP-enabled network adapter installed. You’ll have to figure out what works best for you.

Scripting Guys Tip. When you’re trying to figure this out, we recommend that you write the query and then test it by having the script do something harmless, like echo back the Description for each item in the collection. Don’t actually start changing IP addresses until you know for sure which adapters will be affected by the script and which ones won’t.

Now, where were we? Oh, right: these two lines of code:

strIPAddress = Array("192.168.1.0")
strSubnetMask = Array("255.255.255.0")

All we’re doing here is assigning the new IP address (192.168.1.0) to a variable named strIPAddress and the corresponding subnet mask (255.255.255.0) to a variable named strSubnetMask. The tricky part is that we have to assign these values as arrays; hence the use of the Array function. And, yes, we know that we have only one IP address and one subnet mask. That doesn’t matter; we still have to configure these as arrays or the script will fail.

After setting up our variables we have just one thing left to do: actually assign the new IP address and subnet mask. To do that we set up a For Each loop to loop through the collection of network adapters (see why it’s important that our collection have only one such adapter in it?) and then call the EnableStatic method to assign the new IP address:

For Each objNetAdapter in colNetAdapters
    errEnable = objNetAdapter.EnableStatic(strIPAddress, strSubnetMask)
Next

Yes, very simple: all we do is pass EnableStatic our two variables (strIPAddress and strSubnetMask) and we’re done.

Like we said, there are plenty of other network adapter settings that can be changed using a script. However, we’ll have to refer you to Peter’s white paper for information on that. Right now we need to call room service and order another tray of cigars and a stack of $100 bills.


How Can I Configure a Computer to Use a Dynamically-assigned DNS Server?

Hey, Scripting Guy! How can I configure a computer to use a dynamically-assigned DNS Server?

-- JB

Hey, JB. Ah, this question brings back memories. A couple years ago the Scripting Guys were faced with this same problem, and discovered the answer entirely by accident; in fact, if Greg was able to type a line of code without making a mistake the truth might never have been known. Thanks to Greg’s bumbling, however, we have an answer for you.

If you aren’t sure what we’re talking about take a look at the TCP/IP properties for a network connection. As you can see, this particular network adapter has been configured to automatically obtain both an IP address and a DNS server address:

What we’re interested in is the second part: automatically obtaining a DNS server address. Why aren’t we interested in obtaining an IP address automatically? Because configuring a network adapter to get its IP address from a DHCP server is easy; in fact, here’s a script that does just that:

strComputer = "."

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

Set colNetAdapters = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration where IPEnabled=TRUE")

For Each objNetAdapter In colNetAdapters
    errEnable = objNetAdapter.EnableDHCP()
Next

It’s equally easy to assign specific DNS servers to a network adapter. Here’s a script that assigns two DNS servers – 192.168.1.100 and 192.168.1.200 – to all the IP-enabled network adapters on a computer:

strComputer = "."

Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

Set colNetCards = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

For Each objNetCard in colNetCards
    arrDNSServers = Array("192.168.1.100", "192.168.1.200")
    objNetCard.SetDNSServerSearchOrder(arrDNSServers)
Next

But once assigned how do you unassign DNS servers; that is, how do you configure a network adapter to automatically obtain DNS server addresses? Setting SetDNSServerSearchOrder to Null doesn’t work; setting it to an empty string doesn’t work; setting it to an array of empty strings doesn’t work. Are DNS servers like stray cats, something you can never get rid of?

Fortunately, no. As we noted earlier, when trying to solve this problem Greg mistyped a line of code: he somehow failed to specify values for the array (DNS servers must be assigned as an array, even if you’re assigning only one such server). Lo and behold, that did the trick. Here’s the fruit of his, uh, efforts:

strComputer = "."

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

Set colNetCards = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

For Each objNetCard in colNetCards
    arrDNSServers = Array()
    objNetCard.SetDNSServerSearchOrder(arrDNSServers)
Next

We begin by connecting to the WMI service, then retrieving a collection of all the instances of the Win32_NetworkAdapterConfiguration class where the IPEnabled property is true. That allows us to zero in on the “real” network adapters, and weed out virtual adapters, VPNs, and other non-IP-enabled connections.

After we obtain our list of IP-enabled network adapters, all the action takes place within the For Each loop that loops through that collection. We begin by assigning a completely empty array (no elements whatsoever) to a variable named arrDNSServers; that’s what happens here:

arrDNSServers = Array()

We then pass that empty array as the sole parameter to the SetDNSServerSearchOrder method:

objNetCard.SetDNSServerSearchOrder(arrDNSServers)

That’s all you need to do; from that point on all your IP-enabled network adapters will automatically obtain the address of their DNS server.

Like Greg always says, genius is overrated. Of course, when he says this he has a habit of mispronouncing the word “genius,” but, hey….


How Can I Display the IP Addresses Associated with a Given MAC Address?

Hey, Scripting Guy! How can I display the IP addresses associated with a given MAC address?

-- NK

Hey, NK. Before we answer your question we should take a moment to answer a related question. People often ask us, “If I have a MAC address can I write a script that will tell me which computer on my network is using that address?” Unfortunately, the answer to that question is this: no, you can’t write a script that somehow searches the network looking for a particular MAC address. The best you can do is bind to each individual computer and check to see if the MAC address can be found on that machine. For example, you could write a script that retrieves all your computer names from Active Directory and then methodically connects to each of those computers, checking to see if that MAC address can be found. But that’s something we won’t talk about today.

Note. However, you can find sample code for retrieving computer names from Active Directory, from a text file, and from other sources in the Remote/Multiple Computer Templates section of the Script Repository.

But if all you want to do is check for a MAC address (and return the associated IP addresses) on a given computer, well, that’s easy:

On Error Resume Next

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where MACAddress = '99:99:99:AA:99:A9'")

For Each objItem in colItems
    For Each strIPAddress in objItem.IPAddress
        Wscript.Echo "IP Address: " & strIPAddress
    Next
Next

As you can see, there’s nothing fancy here at all. The script begins by connecting to the WMI service on the local computer (although we could just as easily connect to the WMI service on a remote computer). We then use this line of code to retrieve a collection of all the network adapters that have the MAC address 99:99:99:AA:99:A9:

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where MACAddress = '99:99:99:AA:99:A9'")

At this point all we have to do is walk through the collection and echo back the IP addresses associated with that network adapter. The key here – and a mistake many people make – is that we’re echoing the IP addresses (plural) for each network adapter. It’s entirely possible for a single network adapter to have multiple IP addresses; because of that, the IPAddress property stores values as an array. That means we need to set up a second For Each loop to walk through that collection:

For Each strIPAddress in objItem.IPAddress
    Wscript.Echo "IP Address: " & strIPAddress
Next

Bear in mind that we need to do this even if there’s one (or even if there aren’t any) IP address associated with the adapter.

What if there aren’t any network adapters with that particular MAC address? Well, in this case you just won’t get back any data. However, we could add a tiny bit of code that takes action based on the value of the collection’s Count property: if this value is 0, that means there are no items in the collection, and thus no network adapters with that MAC address. Here’s a modified script that reports back the appropriate message if no such adapter can be found:

On Error Resume Next

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where MACAddress = '99:99:99:AA:99:A9'")

If colItems.Count = 0 Then
    Wscript.Echo "There are no adapters with that MAC address."
    Wscript.Quit
End If

For Each objItem in colItems
    For Each strIPAddress in objItem.IPAddress
        Wscript.Echo "IP Address: " & strIPAddress
    Next
Next

And, just for the heck of it, here’s a bonus script, one that simply reports back the names and MAC addresses for all the network adapters on a computer. Admittedly this is a script you could easily write yourself, but, hey, now you don’t have to:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapter")

For Each objItem in colItems
    Wscript.Echo objItem.Name, objItem.MACAddress
Next


How Can I Prevent a Computer from Using the LMHosts File?

Hey, Scripting Guy! How can I prevent a computer from using the LMHosts file?

-- MJ

Hey, MJ. You know, this was an interesting one, at least for the Scripting Guys. Not because the script was hard to write; that was actually pretty easy. What was hard – at least for us – was to find the LMHosts setting in the Windows GUI; that’s something we needed to do so we could verify that the script actually worked. After stumbling around a bit (which isn’t too unusual for the Scripting Guys) we eventually found what we were looking for:

In case you’re as clueless as the Scripting Guys (and, for your sake, let’s hope not) here’s how we got to this dialog box:

1. From Network Connections in Control Panel, pick any of your network connections.

2. In the Properties dialog box for that connection select Internet Protocol (TCP/IP) and then click Properties.

3. In the Internet Protocol (TCP/IP) Properties dialog box click Advanced.

4. In the Advanced TCP/IP Settings dialog box, look on the WINS tab. There’s your setting.

Like we said, finding the LMHosts check box was the hard part; clearing the check box (and thus preventing the computer from using the LMHosts file) was easy:

On Error Resume Next

Const USE_WINS = False
Const USE_LMHOST_FILE = False

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

Set objNetworkSettings = objWMIService.Get("Win32_NetworkAdapterConfiguration")
errResult = objNetworkSettings.EnableWINS(USE_WINS, USE_LMHOST_FILE)

Wscript.Echo errResult

We start this script off by defining a pair of constants – USE_WINS and USE_LMHOST_FILE – and setting both to False. We use the constant USE_WINS to tell the script that we want to disable the use of WINS (Windows Internet Name Service) altogether. If that’s not the case – if you want to continue to use WINS but just don’t want to use the LMHosts file – then set the value of USE_WINS to True.

Meanwhile, the constant USE_LMHOST_FILE tells the script whether we want to use the LMHosts file. We set this constant to False because we don’t want to use LMHosts. If you change your mind and decide you want to use LMHosts after all, then simply set this constant to True.

Of course it’s easy. With scripting it’s always easy.

Well, OK: almost always.

The next step is to connect to the WMI service on the local computer (although we can also perform this operation on a remote machine). That brings us to this line of code:

Set objNetworkSettings = objWMIService.Get("Win32_NetworkAdapterConfiguration")

You’re right: this is a bit unusual. In most WMI scripts we’d call the ExecQuery method at this point; in turn, ExecQuery would bring us back a collection of objects to work with. You might have noticed that we don’t use ExecQuery at all in this script. Why not? Well, the EnableWINS method (the method we use to turn off WINS and the LMHosts file) is a “static” method. A static method does not work on a collection of objects; instead, it works on the class itself. That means that you bind to the Win32_NetworkAdapterConfiguration class (using the Get method) and then call EnableWINS. The net effect: all instances of the class (that is, all the network adapters on your computer) will have LMHosts disabled. If you have multiple network adapters there’s no provision for disabling LMHosts on one adapter yet enabling it on another. It’s all or nothing.

At that point then we simply call the EnableWINS method, passing – in order – the constants USE_WINS and USE_LMHOST_FILE:

errResult = objNetworkSettings.EnableWINS(USE_WINS, USE_LMHOST_FILE)

Notice that we capture the return code (the results of the operation) in a variable named errResult. In the last line of the script we echo back this return code. If errResult is equal to 0, that means the operation succeeded and LMHosts has been disabled. If errResult is anything but 0, well, then something went wrong. In that case, you should check the WMI SDK for a detailed list of EnableWINS error codes.

And that’s about the size of it. You now have a script that disables LMHosts, and the Scripting Guys now know how to find the LMHosts setting in the GUI. A win-win situation!


How Can I Rename a Local Area Connection?

Hey, Scripting Guy! How can I rename a local area connection?

-- AP

Hey, AP. You know, you hurt our feelings here: we Microsoft types spent a lot of time coming up with clever and catchy names like Local Area Connection, Local Area Connection 2, and Local Area Connection 3. And now you say you want to change those names? That hurts.

But you know what they say: the customer is always right. Give us a minute to dry our tears and then we’ll show you a script that changes the name of Local Area Connection 2 to Home Office Connection.

OK, we’re better now:

Const NETWORK_CONNECTIONS = &H31&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(NETWORK_CONNECTIONS)

Set colItems = objFolder.Items
For Each objItem in colItems
If objItem.Name = "Local Area Connection 2" Then
objItem.Name = "Home Office
Connection"
End If
Next

You might not be aware of this, but network connections are actually housed in a Windows special folder. Of course, this particular folder is really special: instead of having a path like C:\Documents and Settings\kenmyer\My Documents it has a path like this:

::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\::{21EC2020-3AEA-1069-A2DD-08002B30309D
}\::{7007ACC7-3202-11D1-AAD2-00805FC1270E}

Yikes! But that’s OK. You won’t have much success passing a folder path like that to the dir command, but you can use the Shell object to bind to the path, list all the items (that is, all the network connections) found in the folder, and rename any of those items. And that’s exactly what our script does.

We begin by defining a constant named NETWORK_CONNECTIONS and assigning it the value &H31&; we’ll use this constant to tell the Shell object which special folder we want to work with. We then create an instance of the Shell.Application object, then use the Namespace method to bind to the Network Connections folder. That’s what we do here:

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(NETWORK_CONNECTIONS)

After we bind to the Network Connections folder we can start doing something a bit more interesting. To begin with, we use the Items method to return a list of all the items found in the folder; because we’re dealing with the Network Connections folder that’s going to be a list of all the network connections on the computer. We then set up a For Each loop to iterate through the collection, checking to see if any of the connections have the Name Local Area Connection 2. If they do, we simply use the Shell object to change the Name to Home Office Connection. That’s what we do here:

For Each objItem in colItems
    If objItem.Name = "Local Area Connection 2" Then
        objItem.Name = "Home Office Connection"
    End If
Next

That’s all you have to do: Local Area Connection 2 will now be named Home Office Connection and all will be right with the world.

And before you ask, the answer is no: we will never change the name of the Scripting Guys. Ever.

Well, unless – taking a cue from major league sports – someone would be interesting in buying the naming rights to the Scripting Guys. The Fabrikam Scripting Guys? Contoso Corporation Presents the Scripting Guys? We’re open to offers….


How Can I Turn Off the Windows XP Service Pack 2 Firewall?

Hey, Scripting Guy! How can I turn off the Service Pack 2 firewall?

-- JC

Hey, JC. You know, if there’s a drawback to working at Microsoft (and if our manager is reading this we hasten to add that there are no drawbacks to working at Microsoft) it’s this: no matter what you do some people will be glad you did it and others will be … not so glad … that you did it. The Windows Firewall released as part of Service Pack 2 is a good example of that. By default, the Windows Firewall is turned on the moment you install Service Pack 2. That made a lot of people happy; after all, without having to do anything they now had protection against hackers, crackers, and other ne’er-do-wells.

Of course, it’s also true that other people – particularly those who were already running a firewall program of some kind – weren’t quite as excited to find out that the Firewall was enabled by default. Unlike hot fudge sundaes or million-dollar bills, two firewalls aren’t necessarily better than one. Because of that, if you’ve already set up and configured your original firewall, there’s a good chance that all you want to do with the Windows Firewall is get rid of it.

So can you turn off the Windows Firewall using a script? Yes, you can, although there’s one important caveat we’ll talk about in a moment. For now, though, this script will disable the Windows Firewall:

Set objFirewall =
CreateObject("HNetCfg.FwMgr")
Set objPolicy = objFirewall.LocalPolicy.CurrentProfile

objPolicy.FirewallEnabled = FALSE

As you can see, there’s really not much to this; just three little lines of code. We begin by creating an instance of the HNetCfg.FwMgr object (catchy name, huh?) and then create an object reference to the Firewall’s CurrentProfile. After doing that all we have to do is set the FirewallEnabled property to False, and the Firewall will be turned off. If later on you decide to turn the Firewall back on then simply set the value of FirewallEnabled to True.

If you’re thinking to yourself, “Wow, that’s too easy; there must be a catch,” well, sadly, you’re right: there is a catch. For better or worse (and we’ve heard conflicting arguments on both sides), the Windows Firewall cannot be managed remotely; that means that this script must run locally on the computer where you need to disable the Firewall. If you need to disable the Firewall on only a few computers you could probably just go around from machine-to-machine and run the script; alternatively, you might want to run this as a computer startup script. Definitely not the most convenient way to do things, but you don’t have a lot of choice: there aren’t even any command-line or GUI tools that can be used to manage the Firewall remotely. Outside of a logon or computer startup script, your only other option for remote management is to use Group Policy.

If you’d like more information about the Windows Firewall and how it can be managed using scripts, you might take a look at our Tales from the Script column I Married Bigfoot. Oh: and Service Pack 2 Made My Computers Disappear.

And, yes, we do get paid to sit around all day and think up titles like that. Why do you ask?


Why am I Getting an Error when Trying to Determine an IP Address?

Hey, Scripting Guy! I’m trying to determine the IP address on a computer, but I keep getting a Type Mismatch error. Do you know why?

-- AQ, Jacksonville, FL

Hey, AQ. As a matter of fact, we do know why you’re getting a Type Mismatch error, and it’s a common problem when working with the WMI class Win32_NetworkAdapterConfiguration. The line in your script that’s causing the problem most likely looks something like this:

Wscript.Echo "IP Address: " & objItem.IPAddress

So what’s the problem? Well, the IPAddress property is actually stored as an array; that’s because it’s possible for multiple IP addresses to be assigned to a single network adapter. You’re treating IPAddress as though it was a string or numeric variable and it’s not; an array is a totally different datatype. Hence the Type Mismatch error. This is the same kind of error you get with this script, which tries to multiply dog times cat:

A = "dog"
B = "cat"
C = A * B

So now that we know the problem, what’s the solution? It’s actually pretty easy: you just need to loop through the IPAddress array and echo all the values found there. Admittedly, most of the time there will only be one IP address per network adapter, but that doesn’t matter: whether there’s one IP address, 100 IP addresses, or even 0 IP addresses you still need to use a For Each loop to iterate all the items in the array. A simplified version of your script would thus look like this:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration " & _
        "Where IPEnabled = True")
For Each objItem in colItems
    Wscript.Echo "Caption: " & objItem.Caption
    For Each objAddress in objItem.IPAddress
        Wscript.Echo "IP Address: " & objAddress
    Next
Next

Bonus answer to a question you didn’t even ask: So how did we know that IPAddress is stored as an array? Well, the Type Mismatch error message gave us a pretty good clue, and then we verified that by opening up Wbemtest and locating the Win32_NetworkAdapterConfiguration class. As you can see from the following screenshot, IP address is, indeed, stored as an array:


How Can I List Open Sessions and Open Files on a Computer?

Hey, Scripting Guy! Is there a way to export the information found in the Shared Folders portion of the Computer Management snap-in (in particular, the list of sessions and the list of open files)?

-- PP

Hey, PP. Funny you should ask; this very issue came up the other day, and while we assumed that there was a way to get at this information we weren’t totally sure about that. As it turns out, though, there are actually a couple different ways to get at this data; for now we’ll show you one approach that can retrieve this information off of Windows 2000 computers as well as Windows XP and Windows 2003 computers.

So how do we get at the sessions as shown in the Shared Folders snap-in? One way is to use the IADsSession interface. That sounds very technical, but it’s actually pretty easy. For example, here’s a script that binds to the file service on the computer atl-ws-01 and then echoes back information about each session. Note that in our binding string we connect to LanmanServer; that’s because – in the registry – LanmanServer is the name used by the file service. Here’s the script:

Set objConnection = GetObject("WinNT://atl-ws-01/LanmanServer")
Set colSessions = objConnection.Sessions

For Each objSession in colSessions
    Wscript.Echo "Computer: " & objSession.Computer
    Wscript.Echo "Connected Time: " & objSession.ConnectTime
    Wscript.Echo "Idle Time: " & objSession.IdleTime
    Wscript.Echo "Name: " & objSession.Name
    Wscript.Echo "User: " & objSession.User
    Wscript.Echo
Next

We begin by binding to the file service; after making the connection the script then retrieves a collection of open sessions. That’s what this line of code does:

Set colSessions = objConnection.Sessions

The rest of the script is simply a matter of walking through that collection and echoing back the property values for each open session. (To tell you the truth, we thought it was going to be harder than that, too; like we said, this is almost too easy.)

Incidentally, if you want to verify that the script is returning the correct data, type net sessions at the command prompt; the data brought back by your script and the data brought back by Net.exe should be the same. Likewise, you can verify that your script is returning the correct information about open files by typing net files from the command prompt.

Oh, yeah: what about open files? Turns out we can use the IADsResource interface to get the set of open files:

Set objConnection = GetObject("WinNT://atl-ws-01/LanmanServer")
Set colResources = objConnection.Resources

For Each objResource in colResources
    Wscript.Echo "Path: " & objResource.Path
    Wscript.Echo "User: " & objResource.User
    Wscript.Echo
Next

As you can see, this is very similar to the script that returns the open sessions; the only differences are: 1) We retrieve a collection of resources rather than sessions; and, 2) We echo a different set of property values. But that makes sense: after all, open files are going to have different properties and property values than open sessions. There you go: open files and open sessions.

Of course, one thing we’ve learned in writing this column is that the moment we answer one question people respond with 50 or 60 additional questions related to that topic. Because of that we know we’re bound to get questions like, “How can I disconnect all these sessions?” or “How can I close all these files?” Without going in any detail, here’s a sample script that closes all the open sessions on a computer:

Set objConnection = GetObject("WinNT://atl-ws-01/LanmanServer")
Set colSessions = objConnection.Sessions

For Each objSession in colSessions
    colSessions.Remove(objSession.Name)
Next

And here’s one that closes all the open files:

Set objConnection = GetObject("WinNT://atl-ws-01/LanmanServer")
Set colResources = objConnection.Resources

For Each objResource in colResources
    colResources.Remove(objResource.Name)
Next

We should point out that when we tested these close session and close file scripts they seemed to work just fine on Windows 2000 Server and Windows Server 2003. Results on Windows XP, however, were mixed at best. So, no guarantees; give them a try and see if they work in your environment.


How Can I Determine the Signal Strength of a Wireless Connection?

Hey, Scripting Guy! Any time I’m connected to a wireless network I can open the Wireless Network Connection Status dialog box and see a series of bars – 1 through 5 – that indicates the signal strength. How can I get that same information using a script?

-- RK

Hey, RK. Before we answer today’s question we need to make one slight clarification. For the past week or so, the Scripting Guy who writes this column has noted that, thanks to the NCAA men’s basketball tournament, this is his favorite time of the year. As it turns out, he was mistaken. Instead, thanks to the Girl Scouts of America, this is his least favorite time of the year.

Not that the Scripting Guy who writes this column has anything against the Girl Scouts, mind you; after all, the Girl Scouts of America is a fine, upstanding organization that does a lot of good things for both its members and for the community. It’s just that this is the time of year when the Girl Scouts hold their annual cookie sale. And that can only mean one thing: every penny that the Scripting Guy who writes this column has (along with a number of pennies that he doesn’t have) is going to end up being spent on Girl Scout cookies he doesn’t want, doesn’t need, and doesn’t even take with him. (The Girl Scouts give you the option of donating the cookies to troops serving overseas.)

The Scripting Guy who writes this column is always a sucker for kids selling things, but in years past it was a little easier; that’s because the Girl Scouts only sold their cookies door-to-door. That meant that, sooner or later, a Girl Scout would show up on your doorstep, you’d buy a bunch of cookies, and then you’d be home free for the rest of the year. But now the Girl Scouts don’t bother with door-to-door sales; instead, they stake out the entrances to grocery stores, drug stores, shopping centers and other places that you simply can’t avoid. (Editor’s Note: We’re not sure where the Scripting Guy who writes this column has been for most of his life, because the Scripting Editor distinctly remembers standing outside a bank for several hours selling Girl Scout cookies, and that was…several…years ago.)

And, truth be told, they get the Scripting Guy who writes this column every single time. On one fateful occasion this Scripting Guy entered the local grocery store through door A, buying a bunch of cookies along the way. Without even thinking he then exited the store through door B, where a different pair of Girl Scouts were selling cookies. And yes, he bought cookies from them, too. Talk about getting you coming or going.

But don’t blame the Scripting Guy who writes this column; instead, this is all the Girl Scouts’ fault. After all, somewhere in this world there’s a 17-year-old Girl Scout who spends her time smoking cigarettes, listening to her iPod, and text-messaging her friends. The Scripting Guy who writes this column might be able to walk past someone like that without giving her a second thought. But you don’t see girls like that selling Girl Scout cookies. No, the Girl Scouts who sell the cookies are all tiny little 7- and 8-year-olds, with huge brown eyes and tremulous little voices, and they’re just so cute and so shy when they ask if you’d like to buy some cookies that – well, you can figure out the rest for yourself. It’s not fair!

Worst of all, the Scripting Guy who writes this column needs to stop at the store on his way home tonight. (Yes, the smart thing to do would be to stock up on food and other supplies ahead of time, and thus avoid the stores during the cookie sale. But it’s been so long since the Scripting Guy who writes this column has done the smart thing that he no longer even knows how to go about doing something like that.) That means that this Scripting Guy is going to need plenty of money tonight, and the only way for him to make some money is to come up with a script that can report the strength of a wireless network connection. You know, something along these lines:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From MSNdis_80211_ReceivedSignalStrength")

For Each objItem in colItems
    intStrength = objItem.NDIS80211ReceivedSignalStrength

    If intStrength > -57 Then
        strBars = "5 Bars"
    ElseIf intStrength > -68 Then
        strBars = "4 Bars"
    ElseIf intStrength > -72 Then
        strBars = "3 Bars"
    ElseIf intStrength > -80 Then
        strBars = "2 Bars"
    ElseIf intStrength > -90 Then
        strBars = "1 Bar"
    Else
        strBars = "Strength cannot be determined"
    End If

    Wscript.Echo objItem.InstanceName & " -- " & strBars
Next

Note: Unfortunately, this script does not work on Windows Vista. How do you get this information on Windows Vista? Well, we’re not sure about that. But for those of you who haven’t upgraded yet, read on for an explanation of the pre-Windows Vista solution.

Let’s see if we can figure out how this script works. (After all, if we stall long enough maybe the Girl Scouts will go home and go to bed before we get to the store.) We begin by connecting to the WMI service on the local computer (although we could also run this script against a remote computer). Note, however, that with this script we don’t connect to the root\cimv2 namespace; instead, we make a connection to the root\wmi namespace:

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

After making the connection we then use this line of code to retrieve all the instances of the delightfully-named MSNdis_80211_ReceivedSignalStrength class:

Set colItems = objWMIService.ExecQuery("Select * From MSNdis_80211_ReceivedSignalStrength")

That’s going to give us back a collection of all our wireless connections and their signal strengths.

Once that’s done our next step is to set up a For Each loop to loop through all the items in that collection; inside that loop we use this line of code to assign the value of the NDIS80211ReceivedSignalStrength property to a variable named intStrength:

intStrength = objItem.NDIS80211ReceivedSignalStrength

This is where things start to get a tiny bit weird.

As it turns out, the NDIS80211ReceivedSignalStrength property is going to be a negative value (e.g., -55). To be perfectly honest, we ran into a lot of problems when we tried to find out exactly what that value means and how it is derived. However, we did find a couple of references that provided at least a rough approximation of how these values translate to the signal strength bars found in the Wireless Network Connection Status dialog box. Based on those suggested values (and your values might vary slightly), we decided to next use the following If-Then block to assign a value to a variable named strBars:

If intStrength > -57 Then
    strBars = "5 Bars"
ElseIf intStrength > -68 Then
    strBars = "4 Bars"
ElseIf intStrength > -72 Then
    strBars = "3 Bars"
ElseIf intStrength > -80 Then
    strBars = "2 Bars"
ElseIf intStrength > -90 Then
    strBars = "1 Bar"
Else
    strBars = "Strength cannot be determined"
End If

It should be fairly clear what we’re doing here: we’re simply taking action based on the value of the variable intStrength. For example, suppose intStrength is greater than -57. (And, of course, -55 is greater than -57; don’t let those negative numbers throw you off.) If intStrength is greater than -57 we assign the value 5 bars to the variable strBars and then exit the code block. If intStrength isn’t greater than -57 we then check to see if the value happens to be greater than -68. If it is, then strBars is assigned the value 4 bars and we then exit the code block. Etc., etc.

When we’re done we echo back the value of both the InstanceName property and the variable strBars, then loop around and repeat the process with the next wireless connection.

The one drawback to this script is that you’re likely to end up with multiple reports for a single wireless connection. For example:

Broadcom 802.11a/b/g WLAN -- 5 Bars
Broadcom 802.11a/b/g WLAN - Virtual Machine Network Services Driver -- 5 Bars
Broadcom 802.11a/b/g WLAN - Packet Scheduler Miniport -- 5 Bars

That’s due to the fact that WMI considers both physical network adapters and virtual network adapters to be one and the same. If you wanted to, you could write code that limits the returned data to “real” network adapters (perhaps by first getting a list of IP-enabled adapters from the Win32_NetworkAdapterConfiguration class). We didn’t see any need to go to all that trouble, but if anyone would like to see an example of how you might go about doing that just let us know.

As for the Girl Scouts of America, the Scripting Guy who writes this column has decided that enough is enough: come this time next year he’s just going to have his paycheck direct-deposited to the Girl Scouts. That should save everyone a lot of time and a lot of trouble. And, for once at least, his money will actually be put to good use.


How Can I Tell If a Wireless Network Adapter is Connected to the Network?

Hey, Scripting Guy! How can I tell if a specific wireless network adapter is connected to the network?

-- MS

Hey, MS. You know, this is a sad day for the Scripting Guy who writes this column. After 10 days of eating Italian pastries, visiting the ruins of ancient cities, and flitting about Venice on water taxis, well, it’s time to go home. In fact, even as we speak, the Scripting Family is preparing to take one final water taxi ride back to the Marco Polo Airport. And the Scripting Guy who writes this column is preparing to eat one last zeppole: fried dough topped with cinnamon and sugar.

One last zeppole? Excuse us for a moment; the Scripting Guy who writes this column is about to cry.

Oh, well. You know what they say: all good things must come to an end. Before his Italian vacation comes to an end, however, the Scripting Guy who writes this column has vowed to do two things: eat a few more zeppoles, and show everyone a script that can tell you whether a specific wireless network adapter is connected to the network:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapter Where Name = 'Broadcom 802.11a/b/g WLAN'")

For Each objItem in colItems
    Select Case objItem.NetConnectionStatus
        Case 0 strStatus = "Disconnected"
        Case 1 strStatus = "Connecting"
        Case 2 strStatus = "Connected"
        Case 3 strStatus = "Disconnecting"
        Case 4 strStatus = "Hardware not present"
        Case 5 strStatus = "Hardware disabled"
        Case 6 strStatus = "Hardware malfunction"
        Case 7 strStatus = "Media disconnected"
        Case 8 strStatus = "Authenticating"
        Case 9 strStatus = "Authentication succeeded"
        Case 10 strStatus = "Authentication failed"
        Case 11 strStatus = "Invalid address"
        Case 12 strStatus = "Credentials required"
    End Select
    Wscript.Echo "Net Connection Status: " & strStatus
Next

As you can see, this isn’t a particularly complicated script; as it is, most of the code involves a Select Case statement that converts return values (such as 0) into more meaningful string values (such as Disconnected). Other than that, there isn’t all that much to it.

The script starts out by connecting to the WMI service on the local computer. Could we check the status of a network adapter on a remote computer? Sure, but with one caveat. We can definitely attempt to connect to the remote computer; to do that we simply assign the name of that computer to the variable strComputer:

strComputer = "atl-fs-01"

The only catch, of course, is that if this computer has only a single network adapter (the wireless card) and if the wireless card isn’t currently connected then it goes without saying (although we’ll say it anyway) that we won’t be able to connect to that computer, let alone to the WMI service on that computer. At that point we have no way of knowing: 1) whether the computer is running, but not connected to the network, or 2) whether the computer isn’t running at all. Most likely that distinction won’t matter to you, but it’s something to keep in mind, just in case.

According to MS, their organization has standardized on a single wireless card; that makes it easy to extract information about only that particular network adapter:

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapter Where Name = 'Broadcom 802.11a/b/g WLAN'")

As you probably already figured out for yourself, the preceding line of code retrieves information only about network adapters (instances of the Win32_NetworkAdapter class) that have the Name value Broadcom 802.11a/b/g WLAN. What if you use a different wireless card? That’s no problem; just change the name of the card in the WQL query. For example, if your wireless card is named My Wireless Adapter then modify the WQL query to look like this:

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapter Where Name = 'My Wireless Adapter'")

And what if your organization uses all make and manner of wireless adapters? Don’t worry; at the end of this article we’ll show you a script that returns network connection information for all the wireless adapters on a computer, regardless of their name.

But first, the script we wrote for MS. After we issue our query we get back a recordset containing all the network adapters that have the Name Broadcom 802.11a/b/g WLAN. From there we set up a For Each loop to loop through this collection. Inside that loop, we use a Select Case statement to grab the value of the NetConnectionStatus property; this value will be an integer corresponding to one of the following states:

Value | State
0 | Disconnected
1 | Connecting
2 | Connected
3 | Disconnecting
4 | Hardware not present
5 | Hardware disabled
6 | Hardware malfunction
7 | Media disconnected
8 | Authenticating
9 | Authentication succeeded
10 | Authentication failed
11 | Invalid address
12 | Credentials required

In our Select Case statement we simply take a peek at the value of NetConnectionStatus and then assign the corresponding state to the variable strStatus. For example, if NetConnectionStatus is equal to 0 then we execute this line of code:

Case 0 strStatus = "Disconnected"

After that all we have to do is to echo back the connection status, using the value we assigned to the variable strStatus rather than the value of the NetConnectionStatus property:

Wscript.Echo "Net Connection Status: " & strStatus

In turn, that means we’ll see a report similar to this:

Net Connection status: Authentication succeeded

As for a generic script, one that returns the network connection status of all the wireless adapters on a computer, well, this following bit of code seems to do the trick:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From MSNdis_80211_Configuration")

For Each objItem in colItems

    strName = objItem.InstanceName

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

    Set colAdapters = objWMIService2.ExecQuery _
        ("Select * from Win32_NetworkAdapter Where Name = '" & strName & "'")

    For Each objAdapter in colAdapters
        Select Case objAdapter.NetConnectionStatus
            Case 0 strStatus = "Disconnected"
            Case 1 strStatus = "Connecting"
            Case 2 strStatus = "Connected"
            Case 3 strStatus = "Disconnecting"
            Case 4 strStatus = "Hardware not present"
            Case 5 strStatus = "Hardware disabled"
            Case 6 strStatus = "Hardware malfunction"
            Case 7 strStatus = "Media disconnected"
            Case 8 strStatus = "Authenticating"
            Case 9 strStatus = "Authentication succeeded"
            Case 10 strStatus = "Authentication failed"
            Case 11 strStatus = "Invalid address"
            Case 12 strStatus = "Credentials required"
        End Select

        Wscript.Echo strName
        Wscript.Echo "Net Connection Status: " & strStatus
        Wscript.Echo
    Next
Next

We won’t discuss this in any detail today. Instead, we’ll simply note that we start out by using the following line of code to bind us to the root\wmi namespace:

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

We then select all instances of the MSNdis_80211_Configuration class, a class that returns information about all the wireless adapters (including virtual adapters) installed on the computer:

Set colItems = objWMIService.ExecQuery("Select * From MSNdis_80211_Configuration")

For each wireless adapter we grab the value of the InstanceName property, store it in a variable named strName, then use this block of code to retrieve information about the actual (physical) network adapters that have that name:

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

Set colAdapters = objWMIService2.ExecQuery _
    ("Select * from Win32_NetworkAdapter Where Name = '" & strName & "'")

We’ll let you try the script on your own to see what happens from there.

Uh-oh: it looks like our water taxi is here. We’ll see everyone tomorrow, when the Scripting Guy who writes this column returns to work. Is he excited about once again seeing all his colleagues from Microsoft? Let’s put it this way: he’s as excited about seeing them as they are about seeing him.


How Can I Change the Size of a Page File?

Hey, Scripting Guy! How can I change the size of a page file?

-- HM

Hey, HM. You know this is a somewhat unusual question for us. Why? Because there’s a very easy and straightforward answer, which doesn’t involve weird workarounds, doesn’t need to be accompanied by a bunch of excuses, and works the same on Windows 2000 as it does on Windows XP and Windows Server 2003. A question that we can just answer without a lot of caveats and what-have-you? How are we supposed to do that?!?

Well, one way might be to just show you the code:

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

Set colPageFiles = objWMIService.ExecQuery _
    ("Select * from Win32_PageFileSetting")

For Each objPageFile in colPageFiles
    objPageFile.InitialSize = 300
    objPageFile.MaximumSize = 600
    objPageFile.Put_
Next

As you can see, this is a fairly standard WMI script. It starts out by connecting to the WMI service on the local computer, although simply by changing the value of the variable strComputer we would be able to change the page file size on a remote computer. (Just assign strComputer the name of the remote computer.) We then use this line of code to return a collection of all the page files found on the computer:

Set colPageFiles = objWMIService.ExecQuery _
    ("Select * from Win32_PageFileSetting")

As we noted, this query returns a collection of all the page files found on a computer. Alternatively, we could add a Where clause that returns a specific page file. For example, this query returns only the page file settings for drive C (the file C:\pagefile.sys):

Set colPageFiles = objWMIService.ExecQuery _
    ("Select * from Win32_PageFileSetting Where Name ='C:\\pagefile.sys'")

Note. No, that’s not a misprint: the file name, at least as far as the Where clause is concerned, is C:\\pagefile.sys. The two \ marks are required because the \ is a reserved character in WMI; hence we have to “escape” the character (by doubling it up) any time we use the \ in a query.

All we have to do now is set up a For Each loop in order to walk through the collection. Inside that loop we use the following lines of code to change two properties of the Win32_PageFileSetting class: InitialSize and MaximumSize. Note that, in each case, these values are expressed in megabytes. Thus this code sets the InitialSize to 300 megabytes and the MaximumSize to 600 megabytes:

objPageFile.InitialSize = 300
objPageFile.MaximumSize = 600
objPageFile.Put_

And, whatever you do, don’t forget that last line of code: the Put_ method is required to actually save the changes and modify the size of the file. Leave out that line of code and the script will run just fine; you won’t get an error message of any kind. However, you won’t change the size of the page file, either. Any time you change the value of a read/write property in WMI (and, yes, InitialSize and MaximumSize are read/write properties) you must use the Put_ method to actually put those changes into effect.

And that’s it, that’s all you have to do. But now what are we supposed to do with all these unused caveats and excuses?

Good point: most likely we will have a use for them sooner or later, won’t we? In fact, now that we think about it, tomorrow’s column could use a few. Ah, the joys of scripting on different versions of Windows ….


How Can I Delete All the Duplicates in a Set of Processes But Keep the Oldest Process?

Hey, Scripting Guy! If I have a bunch of duplicate processes, how can I delete all of them except for the oldest one?

-- IR

Hey, IR. We apologize for the delay in answering this question. Originally we thought it would be cute if we could get Peter Costantini, the oldest living Scripting Guy, to answer this; after all, we figured Peter would get a kick out of a scenario in which everything except the oldest thing was targeted for elimination. And we did ask Peter if he knew how to do carry out this task, but instead of answering the question he began telling us stories about the Spanish-American War and the day he got his first Model T. After a half hour or so he insisted that we go to Denny’s in time for the early-bird special; as soon as we got back to the office, he turned on Wheel of Fortune and fell asleep. When he woke up, he resumed telling us stories about the Spanish-American War. (Apparently he and Teddy Roosevelt were bitter rivals.)

It was about that time when we decided we’d better go ahead and answer this question ourselves:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'Notepad.exe'")

If colItems.Count < 2 Then
    Wscript.Quit
End If

dtmTarget = Now

For Each objItem in colItems
    dtmDateHolder = objItem.CreationDate

    dtmDateHolder = CDate(Mid(dtmDateHolder, 5, 2) & "/" & _
        Mid(dtmDateHolder, 7, 2) & "/" & Left(dtmDateHolder, 4) _
            & " " & Mid (dtmDateHolder, 9, 2) & ":" & _
                Mid(dtmDateHolder, 11, 2) & ":" & Mid(dtmDateHolder, 13, 2))

    If dtmDateHolder < dtmTarget Then
         intProcessID = objItem.ProcessID
         dtmTarget = dtmDateHolder
    End If
Next

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'Notepad.exe' " & _
        "AND ProcessID <> " & intProcessID)

For Each objItem in colItems
    objItem.Terminate
Next

You say that it’s a bit complicated looking? Well, yeah, a little. But don’t worry; we’ll explain how it all works, step-by-step.

Note. Peter would like us to inform everyone that back in his day he didn’t need fancy stuff like Windows and Notepad just to terminate a few processes. Give him a punch card and the good old Univac I and in a few days he’d get rid of those pesky old processes. And he’d do it after walking 10 miles through waist-deep snow just to get to the office in the first place.

As you can see, our script starts out in pretty straightforward fashion, binding to the WMI service on the local computer. (Although this script works equally well against remote machines; all you have to do is assign the name of the remote computer to the variable strComputer.) For this example, we’ve decided that we want to get rid of any “extra” copies of Notepad that might be running. With that in mind, we go ahead execute this query to get back a collection of all the processes that have a Name equal to Notepad.exe:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'Notepad.exe'")

At this point we take a minor detour. Suppose it turns out that we have only one instance of Notepad running; in other words, suppose we don’t have any duplicate processes running after all. If that’s the case we’re assuming that we don’t want to terminate our one and only instance of Notepad. That’s why we stuck in this block of code:

If colItems.Count < 2 Then
    Wscript.Quit
End If

What we’re doing here is checking the value of the Count property to see how many instances of Notepad really are up and running. If the Count is less than 2 that means we have, at most, one instance of Notepad running. In that case our work is done: there aren’t any duplicate processes to be terminated. Therefore, we go ahead and call the Wscript.Quit method and terminate the script.

Let’s assume, however, that we do have multiple instances of Notepad running. In that case we need to figure out which instance is the oldest (that is, which instance was created first), then go ahead and delete all the other instances of Notepad. There are a number of different ways to do this, but we chose the following approach.

The first thing we do is assign the current date and time (using the Now function) to a variable named dtmTarget. Why? Well, we’re going to check the creation date and time of each instance of Notepad against this target date and time. Suppose we check the creation time for Notepad instance 1 and find out that it’s older than the value in dtmTarget (which it will be). In that case, Notepad instance 1 becomes our oldest instance of Notepad, and we’ll assign its creation date and time to dtmTarget. We’ll then check the creation date and time of Notepad instance 2 against the new value of dtmTarget. What if Notepad instance 2 is “younger” than dtmTarget? No problem; in that case we just loop around and repeat the process with Notepad instance 3.

If it turns out that Notepad instance 2 is older than dtmTarget then we assign its creation date and time to dtmTarget. That means that, for the moment at least, Notepad instance 2 is our oldest instance.

Make sense? If not, bear with us; we’ll show you exactly how this all takes place.

After retrieving all the instances of Notepad we set up a For Each loop to walk through each item in the collection. The first thing we do inside that loop is assign the value of the process CreationDate property to a variable named dtmDateHolder:

dtmDateHolder = objItem.CreationDate

This is where things get a little messy. The value of the CreationDate property – like most WMI date-time values – is stored using the Universal Time Coordinate (UTC) format; that means that dtmDateHolder is going to be equal to something like this:

20070504085221.620161-420

Needless to say, there’s not a lot we can do with a UTC value, at least not in that particular format. Instead, we need to convert this to a “real” date-time value. That’s what this block of code is for:

dtmDateHolder = CDate(Mid(dtmDateHolder, 5, 2) & "/" & _
Mid(dtmDateHolder, 7, 2) &
"/" & Left(dtmDateHolder, 4) _
& " " & Mid (dtmDateHolder, 9, 2) & ":" & _
Mid(dtmDateHolder, 11, 2) & ":" & Mid(dtmDateHolder, 13, 2))

We won’t discuss the ins and outs of converting a UTC value to a regular date-time value; for detailed information on that take a look at the Microsoft Windows 2000 Scripting Guide. About all we will say is that the preceding block of code turns a value like 20070504085221.620161-420 into a value like this:

5/4/2007 8:52:21 AM

Obviously that’s a value that is much easier for us to work with. More important, it’s a value that’s much easier for VBScript to work with.

OK. Let’s next assume that the value of dtmTarget is equal to this:

5/4/2007 8:58:13 AM

In our next line of code we check to see if our first process (dtmDateHolder) is older than this target date:

If dtmHolder < dtmTarget Then

In this case, of course, that’s going to be true: our process is older than the target date. Therefore we do two things: 1) we assign the value of the ProcessID property to a variable named intProcessID, and 2) we assign the value of dtmDateHolder to our target variable (dtmTarget). That’s what these two lines of code are for:

intProcessID = objItem.ProcessID
dtmTarget = dtmDateHolder

What’s the point of all that? Well, now we know the creation date and time for the oldest process (or at least the oldest process so far). In addition, we also know the process ID for the oldest process. That’s important, because when it comes time to start deleting processes we need an easy way to ensure that we don’t delete the oldest process. Because process IDs are unique this seemed like a pretty good way to separate the oldest process from the rest of the herd.

We then loop around and do this all over again with the next item in the collection. When we’ve finished looping through the entire collection the variable intProcessID will contain the process ID for the oldest instance of Notepad running on the computer.

Still with us? Good. We’re almost done. Promise.

It’s now time to get rid of all those young whippersnapper instances of Notepad. How are we going to do that? To begin with, we’re going to retrieve a brand-new collection of processes, using the following query:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'Notepad.exe' " & _
        "AND ProcessID <> " & intProcessID)

With this query we’re still retrieving all the members of the Win32_Process class that have a Name equal to Notepad.exe. However, we’ve now added a second stipulation: we only want those instances of Notepad where the ProcessID is not equal to intProcessID. That’s what this clause is for:

AND ProcessID <> " & intProcessID

What will that do? That will retrieve all the instances of Notepad except for the instance that has a process ID equal to intProcessID. And because intProcessID just happens to have the same process ID as our oldest instance of Notepad that means that our collection will contain all the instances of Notepad except for the oldest one.

That also means that we can now delete all the instances of Notepad (except, of course, for the oldest instance) by using this little block of code:

For Each objItem in colItems
    objItem.Terminate
Next

And there you have it. If only Peter were still alive to see this.

Note. Peter has just informed us that he is still alive. We believe him. However, we’d still like to get a second opinion.

Incidentally, a lot of people ask us just how old Peter really is. As we noted in one of our webcasts, he’s only 27; he’s actually old only in comparison to the other Scripting Guys, all of whom are barely out of their teens.

Note. What’s that? If the other Scripting Guys are barely out of their teens then how could the Scripting Son be 17 years old? You know, that’s a really good question, and we’d love to answer it. However, it’s time for the early-bird special at Denny’s. See you tomorrow!


How Can I Get a List of All the Users Who Have a Process Open on a Computer?

Hey, Scripting Guy! How can I get a list of all the users who have a process open on a computer?

-- RE

Hey, RE. You know, over the past 5 years the number of people who watch the news on the major networks (ABC, NBC, and CBS) has dropped precipitously; the number of people who read the newspaper each day has fallen even more dramatically. And yet, at the same time, the number of people who visit the Script Center each day has skyrocketed. That can mean only one thing: when people want news and information, they turn to the Script Center first.

And, really, who can blame them? While we’re sure that the major networks and newspapers mean well, they simply don’t cover important stories of the day. For example, suppose you want to know the status of the Dr. Scripto bobblehead dolls. Are you going to find that information in the New York Times? No. Is CBS going to interrupt its regularly-scheduled programming in order to bring you news about the bobbleheads? Yeah, right. Just where can you turn for late-breaking bobblehead bulletins?

As if you had to ask.

Speaking of which, we just happen to have a late-breaking bulletin for you: the bobbleheads are done and ready to be shipped.

In fact, the Scripting Guys have actually received a box of bobbleheads, and they’re pretty cool. For one thing, they’re solid and substantial; they aren’t made of cheap, flimsy plastic (like Scripting Guy Dean Tsaltas is). And, their heads actually bobble (just like Scripting Guy Peter Costantini’s does). Is there anything in the entire world better than a Dr. Scripto bobblehead doll? No, there isn’t.

For those of you who have been waiting on pins and needles for the past 6 weeks or so, the Scripting Guys have filled out all the paperwork and sent the appropriate names and addresses to the bobblehead maker; we expect that they’ll begin air mailing the bobbleheads within the next few days. If you were a lucky winner during the 2007 Winter Scripting Games, well, the waiting is almost over.

And what if you weren’t a lucky winner during the 2007 Winter Scripting Games; does that mean you’ll never be able to get a Dr. Scripto bobblehead of your own? Yes, that’s exactly what it means. Too bad; life’s tough, isn’t it?

Wait, wait, don’t cry; we’re sorry. OK, tell you what: if you promise to enter the 2008 Winter Scripting Games next February, and if you promise to read all the way down to the end of today’s column we’ll give you another chance to (maybe) win a Dr. Scripto bobblehead doll. But whatever you do, don’t try to cheat by simply scrolling down the end of today’s column; that won’t work.

Oh; really? OK. Never mind; the Scripting Editor has informed us that simply scrolling down to the end of today’s column will work. But don’t do that; after all, if you do that then you’ll never know how to get a list of all the users who have a process open on a computer:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process")

Set objDictionary = CreateObject("Scripting.Dictionary")

For Each objProcess in colProcessList
    objProcess.GetOwner strNameOfUser, strUserDomain
    strOwner = strUserDomain & "\" & strNameOfUser

    If Not objDictionary.Exists(strOwner) Then
        objDictionary.Add strOwner, strOwner
    End If
Next

For Each strKey in objDictionary.Keys
    Wscript.Echo strKey
Next

So how does this work? Well, it – wait a second: are you really interested in the script or are you just pretending to be interested just in case that helps you get a bobblehead doll? Oh, OK. In that case, the script starts out by connecting to the WMI service on the local computer (although we can also run this same script against a remote machine). After making the connection we then use this line of code to return a collection of all the processes running on the computer in question:

Set colProcessList = objWMIService.ExecQuery("Select * From Win32_Process")

Once we have our collection safely tucked away in the variable colProcessList we then create an instance of the Scripting.Dictionary object:

Set objDictionary = CreateObject("Scripting.Dictionary")

Why do we create an instance of the Scripting.Dictionary object? We’ll explain that in just a second.

Before we do that, however, we need to set up a For Each loop to loop through all the processes in our collection. Inside that loop we first execute these two lines of code:

objProcess.GetOwner strNameOfUser, strUserDomain
strOwner = strUserDomain & "\" & strNameOfUser

What are we doing here? Well, for some reason the Win32_Process class doesn’t include a property that tells you the owner of a process (the owner of a process being the same thing as the account under which the process is running). Instead, for each process in the collection we need to call the GetOwner method in order to determine the process owner. As you can see, when we call GetOwner we supply two parameters; in this case, strNameOfUser and strUserDomain. Both of these are “out” parameters. An out parameter is simply a variable that we supply to a method; in return, that method populates those two variables with the appropriate values. In this script that means that GetOwner will assign the name of the process owner to strNameOfUser, then assign that user’s domain to strUserDomain. And yes, those names are completely arbitrary; we can name the out parameters anything we want. Would you rather have out parameters named x and y? Okey-doke:

objProcess.GetOwner x, y

The important thing here is not the names but the order: the variable specified first will get the owner name and the variable specified second will get the owner domain.

Once we have that information we then use the second line of code to combine the name and domain in this format: Domain\Username (e.g., FABRIKAM\kenmyer).

That was pretty easy, wasn’t it?

Note. No, agreeing with the Scripting Guys won’t help you get a bobblehead. But it won’t hurt, either.

That brings us to this block of code:

If Not objDictionary.Exists(strOwner) Then
    objDictionary.Add strOwner, strOwner
End If

This is where the Dictionary object comes into play. Each process running on a computer has an owner; if you have 50 processes running on your machine then there will be 50 process owners. However, it’s unlikely that you’ll have 50 unique owners; instead there are bound to be duplicates (for example, a large percentage of your processes will be running under the NT AUTHORITY\SYSTEM account). We don’t really need to – or want to – see all those duplicate names. But how can we echo back only unique owner names?

One way is to use the Dictionary object. As you can see, in our first line of code we use the Exists method to determine whether the owner of the first process (e.g., FABRIKAM\kenmyer) can be found in the Dictionary. Let’s say that this process owner isn’t found in the Dictionary. (Needless to say, the owner of the first process won’t appear in the Dictionary, simply because the Dictionary will be empty the first time through the loop.) In that case we then call the Add method to add the process owner to the Dictionary, using the variable strOwner to specify both the Dictionary Key and Item:

objDictionary.Add strOwner, strOwner

Note. What’s that? You say you don’t understand how the Dictionary object works? Tsk, tsk, tsk: you haven’t been reading Sesame Script, have you?

We then loop around and repeat the process with, well, with the next process in the collection. Let’s say that process No. 2 is also owned by FABRIKAM\kenmyer. This time when we call the Exists method the process owner will be found in the Dictionary. What happens then? Nothing; we simply loop around and start all over again with the third process in the collection.

When we’ve finished retrieving the owner of each process we then echo out the value of the Dictionary keys. By amazing coincidence, those keys will represent the unique set of process owners:

NT AUTHORITY\SYSTEM
NT AUTHORITY\NETWORK SERVICE
NT AUTHORITY\LOCAL SERVICE
FABRIKAM\kenmyer

And there you have it.

Note: If you run this script on Windows Vista, you’ll see different results when you Run As Administrator rather than simply run the script as an Administrator. Make sense? No, it doesn’t, does it? But what it means is that if you’re logged on as an administrator and you open a command prompt and run this script, you won’t get the full list of process owners. In order to get the full list, you have to right-click on the command prompt shortcut and select Run As Administrator. When you run the script from that window, you’ll see the full list of process owners.

Now, what about those bobbleheads? Well, as it turns out, the Scripting Guys will be attending this year’s TechEd conference in Orlando, FL (June 4-8). As part of the festivities the Scripting Guys will spend a good part of their time sitting at the TechNet Magazine booth in the Exhibition Hall, handing out a fun little giveaway and giving away bobbleheads. (How many bobbleheads? Good question; however, we have to wait and see what the budget looks like first before we can answer it.) If you’d like another shot at a bobblehead doll then all you have to do is sign up for TechEd, swing by the TechNet Magazine booth, say hi to the Scripting Guys, and enter the drawing.

Sure, that sounds like a lot to do. But what do you expect when the grand prize is a Dr. Scripto bobblehead doll?

Can’t make it to Orlando this year? Well, with any luck we’ll also be attending TechEd Europe in November. Can’t make it to TechEd Europe, either? Sheesh; what do you want us to do, just hand out bobbleheads to everyone who asks for one? Oh. Well, sorry: we definitely don’t have the budget for that. But we’ll make one final deal with you: after the smoke clears from TechEd we’ll see what we can do about giving everyone one last shot at a bobblehead doll. Because we agree with you: living without a Dr. Scripto bobblehead doll really isn’t living, is it?

Determining Process Ownership Using Windows PowerShell

The Windows PowerShell solution to this question is made much easier by the presence of the Group-Object cmdlet, which groups objects based on the value of a selected property. The default display of a Group-Object command lists the property value, the names of objects in each group, and the number of objects in each group. As a result, the Scripting Guy's nifty scripting dictionary technique is not needed here.

The following Windows PowerShell command answers the user's question:

get-wmiobject win32_process | group-object -property {$_.getowner().domain}, {$_.getowner().user}

Here's the output. It's not exactly like the output of the VB script, but we also haven't put any effort into it, and we get a bit more information.

Count Name                      Group
----- ----                      -----
    1                           {System Idle Process}
   27 NT AUTHORITY, SYSTEM      {System, smss.exe, csrss.exe, winlogon.exe...}
    5 NT AUTHORITY, NETWORK ... {svchost.exe, svchost.exe, AdtAgent.exe...}
    4 NT AUTHORITY, LOCAL SE... {svchost.exe, wdfmgr.exe, alg.exe...}
    3 SERVER01, Administrator   {init, inetd, cron}
   31 DOMAIN01, user01          {explorer.exe, smax4pnp.exe...}

To eliminate the truncated Name values, you can send it to the Format-Table cmdlet and use its -Wrap parameter.

get-wmiobject win32_process | group-object -property {$_.getowner().domain}, {$_.getowner().user} | format-table -wrap

Understanding the Command

This command uses the Get-WmiObject cmdlet with the Win32_process class. This returns a Win32_Process object for each process on the computer.

get-wmiobject win32_process

We use a pipeline operator (|) to send the results to the Group-Object cmdlet. Group-Object has a -Property parameter that specifies the properties to group by. If you give it more than one property, it groups first by the first property value and then by each subsequent property value.

get-wmiobject win32_process | group-object -property

In this case, we follow the model of our hero, the Scripting Guy [bowing respectfully]. We need to give Group-Object a property, but first, we use the GetOwner method to get the object that has the Domain and User properties.

For each property, we use the $_ symbol to represent the current object, that is, each Win32_process object that Get-WmiObject sends through the pipeline to Group-Object. The $_ is followed by a dot (.), the name of the method (GetOwner), and a set of parentheses to indicate that it's a method, not a property.

$_.getowner()

Now, we'll get the Domain and User properties of the object that the GetOwner method returns. Just follow the method with another dot (.) and the property name.

$_.getowner().domain
$_.getowner().user

Because we've written an expression to get each property, we'll enclose each expression in curly braces {}.

{$_.getowner().domain}
{$_.getowner().user}

We're going to use the -Property parameter of Group-Object to specify the two properties to group by. When you specify more than one value for a Windows PowerShell parameter, you separate the values with a comma. And, be sure to specify them in the right grouping order. First domain, then user.

group-object -property {$_.getowner().domain}, {$_.getowner().user}

Now, we'll put it all together.

get-wmiobject win32_process | group-object -property $_.getowner().domain}, {$_.getowner().user}

Process Objects v. Process Objects

For Windows PowerShell users, the interesting part of this task -- the part that the Scripting Guy did for us -- was to realize the difference between the process object that the Get-Process cmdlet returns and the process object that WMI returns.

Let's use the Get-Member cmdlet to look at them. Get-Member gets the .NET type of the object and all of its properties and methods.

First, we send the results of a Get-Process command to Get-Member.

get-process | get-member

It shows that you have a System.Diagnostics.Process object and it lists all of its gazillion properties and methods. I won't list them here, but you can see it on your machine or on MSDN.

Now, we'll send the results of a Get-WmiObject Win32_process command to Get-Member.

get-wmiobject win32_process | get-member

Surprise. We have a System.Management.ManagementObject#root\cimv2\Win32_Process object, which is essentially a WMI process object. Again, a bunch of very useful properties and methods (per MSDN).

These objects have some common properties, like WorkingSet (Get-Process) and WorkingSetSize (WMI), and SessionID (both). They have some common methods, like Terminate (WMI) and Kill (Get-Process), but they have lots of disjoint properties and methods. And, if you use Windows PowerShell, this is a great benefit, because you have them both.

So, the next time you need something from an object, check both the PowerShell and WMI versions of the object.

Oh, and do whatever you can to get a Dr. Scripto bobblehead. I have one on my desk (with a Windows PowerShell logo) and my life will never be the same.


How Can I Get the Process ID for an Executable File, But Only If the Process Has a Specified Owner?

Hey, Scripting Guy! How can I get the process ID for a file like MyExecutableFile.exe, but only if a certain user is the owner of that process?

-- SC

Hey, SC. You know, here at Microsoft ownership is very important; for example, when you sit down to discuss joint projects with people the very first question that comes up is this: who actually owns this project?

Is that because people at Microsoft want to own everything? Heavens no: after all, the owner of the project has to do all the implementation and all the maintenance, has to respond to customer feedback, has to fix bugs, etc. No one wants to own everything; on the contrary, people are always working to ensure that they don’t own anything. If you’re really clever you can figure out a way to get credit for stuff without actually having to own that stuff. That’s what the smart people around here do all the time.

Which, alas, explains why the Scripting Guys own the Script Center and everything in it.

Note. Sure, maybe we’re a little naïve and gullible at times. But once we get our hands on that $90 million we are helping the widow of the Nigerian Defense Minister transfer to a US bank account, well, then we’ll see who has the last laugh.

So can we get the process ID for an executable file, but only if that file is owned by a specific user? Of course we can:

strComputer = "."

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

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'notepad.exe'")

For Each objProcess in colProcessList
    objProcess.GetOwner strNameOfUser,strUserDomain
    strOwner = strUserDomain & "\" & strNameOfUser
    If LCase(strOwner) = "fabrikam\kenmyer" Then
        Wscript.Echo objProcess.Handle
    End If
Next

Yes, it’s that easy. What we’re doing here is getting the process ID for any instance of Notepad that is owned by the account fabrikam\kenmyer. To do that, we begin by connecting to the WMI service on the local computer (although we could just as easily run this script against a remote computer). We then use the ExecQuery method to retrieve a collection of all the processes that have a Name equal to notepad.exe:

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'notepad.exe'")

Good question: wouldn’t it be better if we ran a query that returned all the processes that had a Name equal to notepad.exe and an owner equal to fabrikam\kenmyer? You bet it would; unfortunately, though, we can’t do that. That’s because the name of the process owner can’t be retrieved as part of a query; instead, we have to use the GetOwner method to individually retrieve process owners one-by-one. In fact, there’s really only one reason that we even bother to limit the returned data to processes with the Name notepad.exe: that way we’ll have fewer processes we need to run GetOwner against.

Speaking of which, as soon as we have our collection we set up a For Each loop to cycle through each process in the collection; the very first thing we do inside that loop is call the GetOwner method:

objProcess.GetOwner strNameOfUser,strUserDomain

As you can see, for the process in question we call GetOwner and include a pair of “out” parameters: strNameOfUser and strUserDomain. Out parameters are nothing more than variables, and the names are arbitrary: we can name these variables anything we want. (We chose strNameOfUser and strUserDomain because the names just seem to roll off the tongue, don’t they?) The only real difference between out parameters and regular old variables is that we never assign a value to these parameters; instead, the GetOwner method assigns the name and domain of the process owner to strNameOfUser and strUserDomain, respectively.

Once that’s done we then use this line of code to construct an owner name using the format domain\user (e.g., fabrikam\kenmyer):

strOwner = strUserDomain & "\" & strNameOfUser

At this point we finally know the owner of the process; the only thing we don’t know is whether or not this happens to be the user we’re interested in. To answer that question we use the following If statement to determine whether the lowercase value of strOwner (the variable containing the owner name) is equal to fabrikam\kenmyer:

If LCase(strOwner) = "fabrikam\kenmyer" Then

If the owner is fabrikam\kenmyer we echo back the process ID (which can be found in the Handle property). If the owner happens to be anyone else, well, then we don’t do anything at all. Which is what we Scripting Guys do best.

Does that answer your question, SC? If not, please don’t write to us; instead, we ask you to direct all additional inquiries to the owner of this particular column.

Which would be, well, us. You know, we really have to work on taking credit for stuff without actually owning it, don’t we?


How Can I Start a Process and Then Log the User Off When That Process Ends?

Hey, Scripting Guy! How can I write a script that starts a process, waits until that process ends, and then logs the user off the computer?

-- AG

Hey, AG. You know, the Scripting Guy who writes this column is never one to brag, mainly because he never has much of anything to brag about. In addition tot that, however, he is a humble and modest sort of person; that’s why he hasn’t said a word about something he did a couple weeks ago. But seeing as how he doesn’t have anything else to talk about today (You say he could talk scripting? What, in a daily scripting column?) maybe it’s time to mention what was undoubtedly the greatest athletic feat in recorded history.

What’s that? Lance Armstrong? Well, sure, Lance Armstrong has had a few successes. But, let’s face it, all Lance Armstrong did was ride a bike. Did Lance Armstrong ever throw a basketball the length of the court and make a basket?

Oh, he did? Well, that figures. But too bad. If Lance Armstrong wants to brag about his athletic feats, well, let him get his own daily scripting column!

The occasion was yet another one-on-one basketball game between the Scripting Dad and the Scripting Son. This time out the Scripting Dad managed to win, sinking a last-second shot from behind the three-point line. The Scripting Son – always a good sport when he plays against his father – picked up the ball and heaved it towards the basket at the end of the gym. As is so often the case, that act of frustration suddenly turned into a competition, with the two vying to see who could be the first to make a full-court basket.

To be honest, the Scripting Dad’s first attempt was a bit pathetic; it bounced right around the foul line (to the vast amusement of the Scripting Son, we might add). But that embarrassment only spurred the Scripting Dad on, and around his 15th try or so: swish! Game over, Scripting Son!

Not that the Scripting Dad takes any pleasure in defeating his own son, mind you.

At any rate, we’re sure everyone will agree that making a full-court basket is the greatest athletic feat since the beginning of time; heck, it might very well be the great feat of any kind, athletic or not. As for the second-greatest feat of all time, that’s hard to say. However, and with all due modesty, we’d like to nominate a script that starts a process, waits until that process ends, and then logs the user off the computer:

strComputer = "."

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

objWMIService.Create "Notepad.exe", null, null, intProcessID

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

Set colItems = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceDeletionEvent " _
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")
Do
    Set objProcess = colItems.NextEvent
    If objProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

Set objWMIService = GetObject("winmgmts:{(Shutdown)}\\" & _
        strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

For Each objItem in colItems
    objItem.Win32Shutdown(0)
Next

You know, you’re right: this is kind of a long script, isn’t it? But what the hey; let’s walk through it anyway and see if we can figure out how it all works.

As you can see, we start out by binding to the WMI service on the local computer. There are two important things to note here. First, we bind directly to the Win32_Process class; that’s what the \root\cimv2\:Win32_Process portion of this command does:

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

Second, any time we show you a WMI script we usually say, “Blah, blah, blah. But, of course, you can also run this script against a remote computer.” Is that the case here, can you run this script against a remote computer? Well, maybe. The script will actually run just fine against a remote machine; however, the process you create will run in a hidden window. In other words, that process will not be visible on the remote machine (or anywhere else, for that matter). If your application requires no user intervention to run (and then to terminate) then this script will work fine against a remote machine. However, if your application requires some sort of user intervention, well, in that case this script won’t work against a remote machine. That’s because no one will be able to intervene with the application.

Incidentally, that’s all done for security reasons, and there’s no way to change it.

After we connect to the WMI service (and the Win32_Process class) we then use this line of code to start the application (in this case, Notepad.exe):

objWMIService.Create "Notepad.exe", null, null, intProcessID

As you can see, this isn’t too terribly complicated: we simply call the Create method followed by the name of the process we want to create. That process name is, in turn, followed by a pair of Null parameters (these are optional parameters that we don’t care about for this script), which are followed by an “out parameter” we named intProcessID.

This out parameter turns out to be the key to getting the script to work. An out parameter is nothing more than a variable that you supply to a method; in turn, the method assigns a value to that variable. In the case of the Create method, that value is the process ID for our new instance of Notepad. Needless to say, that’s how we’ll be able to determine when our application is terminated. If that process ID disappears, that can only mean that our application is no longer running.

After we’ve started the application our next step is to sit around and wait for the application to end. To do that we re-bind to the WMI service (this time binding just to the root\cimv2 namesapce) and then create a new WMI query, one that looks like this:

Set colItems = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceDeletionEvent " _
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")

What we’re doing here is creating an event subscription: we’re asking WMI to notify us each time there are new instances of the __InstanceDeletionEvent class; as the name implies, a new instance of this class is created any time an object is deleted.

You know, you’re right: we did kind of mislead you, didn’t we? After all, we aren’t asking to be notified when any object is deleted; instead, and as our Where clause states, we only want to be notified if the object in question (the TargetInstance) happens to be a member of the Win32_Process class. In other words, we only want to be notified when processes are deleted (that is, each time a process is terminated).

Note. __InstanceDeletionEvent class? TargetInstance? You say you have no idea what we’re talking about here? In that case, you should take a peek at the Scripting Week 2 webcast on WMI events. Granted, you probably still won’t have any idea what we’re talking about; this column has that effect on people. But at least you’ll have a better understanding of things like __InstanceDeletionEvent and TargetInstance.

After setting up our event subscription we then create a Do loop designed to run forever (notice that there’s no exit criteria anywhere):

Do
    Set objProcess = colItems.NextEvent
    If objProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

Of course you’re right, as usual: nothing lasts forever, does it? And, as it turns out, this loop won’t last forever, either. Instead, what it’s going to do is wait until a process gets deleted; that will trigger both our event subscription and, in turn, move the script off this line of code (a line of code designed to “block” the script until an event of interest has occurred):

Set objProcess = colItems.NextEvent

Once the event has fired we go ahead and check the ProcessID property of the TargetInstance in order to determine whether that value is equal to intProcessID, the ProcessID of the application we started:

If objProcess.TargetInstance.ProcessID = intProcessID Then

Suppose the two process IDs are different. No problem: that just means that the process that was terminated wasn’t the process that we started. Therefore we loop back around and wait for the next process to be deleted.

On the other hand, suppose the two process IDs are identical. Because process IDs must be unique at any given time, that can only mean one thing: our process has come to an end. And that means that it’s time to log the user off the computer.

To do that we first call the Exit Do statement to exit our heretofore un-exitable loop. As soon as we’re safely out of the loop we then run this block of code:

Set objWMIService = GetObject("winmgmts:{(Shutdown)}\\" & _
        strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

For Each objItem in colItems
    objItem.Win32Shutdown(0)
Next

Here we start out by binding to the WMI service one more time, this time including the Shutdown privilege in our binding string. (And, yes, we could have included the Shutdown privilege earlier and then simply reused that object reference. But we thought this approach makes it a little easier for everyone to follow what we were doing.) After making the connection we then use this query to retrieve all instances of the Win32_OperatingSystem class. (Because this class can only retrieve information about the current operating system, there will always be one – and only one – item in the resulting collection.)

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

We then set up a For Each loop to loop through each item in the collection; for each of those items we call the Win32Shutdown method, passing the value 0 as the sole method parameter:

objItem.Win32Shutdown(0)

In case you’re wondering, the 0 tells Win32Shutdown to log the user off the computer. Are there other values we could supply to Win32Shutdown, values that would enable you to, say, restart or completely shut down a machine? You bet there are; take a look at the Microsoft Windows 2000 Scripting Guide for more information.

And that should do it, AG.

For those of you who keep track of this sort of thing, the Scripting Son later got his revenge: the next time he and the Scripting Dad played one-on-one he absolutely annihilated his father. But, for the moment at least, the Scripting Dad retains the last laugh. After that particular game they had a three-point shooting contest. The Scripting Son went first, and made 7 out of 15 three-pointers. How many shots did the Scripting Dad need to win the contest? You got it: 8.

Actually, we agree with you: the Scripting Dad should quit his job and become a full-time basketball player. Interestingly enough, that’s something his manager has been suggesting he do for years now ….


How Can I Start a Process and Then Wait For the Process to End Before Terminating the Script?

Hey, Scripting Guy! How can I start a process and then wait until that process quits before terminating the script?

-- FG

Hey, FG. Before we answer your question, the Scripting Guy who writes this column has a confession to make: he’s the one who ruined Christmas for everyone. Not that he meant to, mind you, but that doesn’t matter; all that matters is that he ruined Christmas. Last night this Scripting Guy was watching TV when he noticed a commercial featuring a delicatessen where orders were filled like clockwork: you came in, ordered, got your food, paid for your purchase, and then went on your merry way, all in a matter of seconds. And all done to snappy music to boot.

Well, at least until some idiot had the audacity to – believe it or not – pay for his purchase using money instead of a debit card! Not only did the finely-tuned delicatessen machine come to a screeching halt, but the looks of disgust on the faces of both the cashier and the other deli patrons made everyone’s feelings very clear: “Thanks a lot, buddy. You not only ruined Christmas for yourself, but you ruined it for everyone else in the world, too. We hope you’re happy.”

As you probably guessed, the Scripting Guy who writes this column is that idiot. It’s true: although he has a debit card he only uses it at the ATM machine. When it comes to actually buying stuff he always pays cash. Always. And when it comes time to pay the bills he always writes a check, puts the check in an envelope, puts a stamp on the envelope, and mails the thing. (You better sit down; you look a little faint.) No automatic deductions from his checking account, no Internet bill-paying; he still pays his bills the very same way Neanderthals paid their bills.

Note. OK, you got us there: even Neanderthals used pay-by-phone whenever possible.

Needless to say, the Scripting Guy who writes this column humbly apologizes for his boorish and unthinking behavior, and deeply regrets having ruined Christmas for everyone else.

So does this mean that the Scripting Guy who writes this column is going to come to his senses and finally join the modern world? Oh, heavens no; he likes being an anachronism. However, he will at least show you how to write a script that starts a process and then waits around until that process has finished before terminating:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")
objWMIService.Create "notepad.exe", null, null, intProcessID

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

Set colMonitoredProcesses = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA 'Win32_Process'")

Do Until i = 1
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    If objLatestProcess.TargetInstance.ProcessID = intProcessID Then
        i = 1
    End If
Loop

Wscript.Echo "Notepad has been terminated."

As you can see, we start off by connecting to the WMI service on the local computer. Could we also run this script against a remote computer? Sure we could; however, keep in mind that any process you start on a remote computer will, for security reasons, run in an invisible window and will not appear onscreen. That means you could use this script to run command-line tools that do their thing and then automatically terminate themselves upon finishing; those would work just fine. When it comes to GUI applications, which typically don’t terminate themselves, this script is less useful, at least when run against remote computers. That’s because the GUI application will run in an invisible window, making it far more difficult for someone to terminate that application.

But don’t let that deter you: for command-line tools or for any application started on the local computer this approach works great.

Speaking of the WMI service, we should point out that, when making the connection to this service, we need to bind directly to the Win32_Process class:

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

Why? Because in order to call the Create method and create a new process, we need to be connected to the class itself. The typical WMI approach – using ExecQuery to return a collection of all instances of the class – doesn’t do us any good here.

After making the connection we use the following line of code to create an instance of Notepad.exe (a GUI application, but that’s OK: in this example we’re running it locally anyway). With this line of code we call the Create method followed by the executable file we want to run (Notepad.exe). The executable file name is then followed by two Null parameters (startup parameters not used in this script), then followed by an “out parameter” named intProcessID:

objWMIService.Create "notepad.exe", null, null, intProcessID

What was that again: an out parameter? Yes. Typically when you call methods you supply information (in parameters) to the method; for example, the executable file name (Notepad.exe) is an in parameter. With an out parameter, the method supplies information to you; in this case, that will be the process ID (PID) of the new process. All we have to do is provide the name of the variable where we want that information stored and the Create method will take care of the rest.

At this point we want the script to simply sit around and wait until Notepad is done doing whatever it is Notepad does. To do that we create a second WMI object reference (reusing the variable named objWMIService) and then use the following query to monitor process deletion:

Set colMonitoredProcesses = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA 'Win32_Process'")

We won’t discuss monitoring scripts in any detail today; for more information, see the webcast An Ounce of Prevention: An Introduction to WMI Events. Suffice to say that we’re simply asking WMI to notify us any time a process is deleted (terminated). And we’d like WMI to check for any new deletions every second, which is what the Within 1 is for.

After we issue the query we set up the following Do loop, a loop designed to run until a variable named i is equal to 1 (because i has not been initialized, it begins life equal to 0):

Do Until i = 1
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    If objLatestProcess.TargetInstance.ProcessID = intProcessID Then
        i = 1
    End If
Loop

Inside this loop we use the following line of code to wait until WMI notifies us that a process has been deleted:

Set objLatestProcess = colMonitoredProcesses.NextEvent

So what happens if a process does get deleted? Well, in that case we check to see if the ProcessID of the deleted process (objLatestProcess.TargetInstance.ProcessID) is equal to the ProcessID of the process we created:

If objLatestProcess.TargetInstance.ProcessID = intProcessID Then

If the ProcessIDs differ we loop around and wait for notification of the next deleted process. If the ProcessIDs match that can only mean one thing: the process we started has been terminated. In that case we set the value of i to 1, something which, in turn, causes us to exit the loop.

At that point we simply echo back a message stating that Notepad has been terminated. Needless to say, though, you can do anything you want at this point; after all, it’s your script. (Or don’t do anything at all; if you prefer, you can just exit the loop and the script will automatically terminate right then and there.)

So will the Scripting Guy who writes this column even consider using a debit card to pay for purchases from now on? Well, to tell you the truth, that did look a lot faster and a lot easier than paying by cash. Of course, the people on this commercial simply swiped their card and then moved on: they didn’t have to enter a PIN number, they didn’t have to wait for authorization, and they didn’t have to sign anything. And none of them ever asked if they could get cash back, either. But maybe none of that happens anymore. After all, the makers of a commercial would never fudge the truth a little just to make their product look better. Heaven forbid!

Tell you what: Christmas is already ruined, but we’ll see if we can avoid ruining Valentine’s Day for you. We can’t promise anything, but we’ll see.


How Can I Use Windows PowerShell to Get a List of All the Open Windows on a Computer?

Hey, Scripting Guy! Someone told me that there’s a way to use Windows PowerShell to get a list of all the open windows on a computer. Any chance you could show me how to do that?

-- JD

Hey, JD. You bet we can. Before we do that, however, we’d like to take a moment and salute Peter Costantini, the oldest living Scripting Guy. Peter, best-known for being the author of Dr. Scripto’s Script Shop and for being the crazy guy who sings on webcasts, recently accepted a Program Manager position in the Management Practices group at Microsoft. Peter will still be a Scripting Guy; he just won’t be able to devote much time to scripting and to writing Script Center articles.

Which, all things considered, puts him on the same footing as the other Scripting Guys. But at least he has an excuse for not doing anything.

Speaking of the other Scripting Guys, we wish Peter the best of luck, and, as a special tribute, thought we’d recap some of his greatest moments as a Scripting Guy:

· For example, just a few weeks ago he – well, actually, Dean did that.

· But about a year ago Peter – never mind; Jean wrote that, didn’t she?

· Still there was – oh, right. But if it hadn’t set the building on fire that would have been a pretty good little script.

And of course, there are many other great moments, far too, uh, numerous to name.

Note. As you might expect, Peter’s true contributions to scripting and to the Script Center can’t be measured by the amount of work he did. Or at least that’s what he keeps telling the rest of us.

On the bright side, though, the years Peter spent sitting around doing nothing useful or important made him the obvious choice for a management position at Microsoft.

To be honest, our first reaction when Peter announced he was leaving was to simply sit around and mope. (Of course, that’s usually our first reaction to anything.) But Peter wouldn’t have wanted us to act that way; instead, he would have wanted us to get on with our lives. So here you go, Peter; this one’s for you:

Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Select-Object MainWindowTitle

As you can see, this is a pretty simple little command. We start out by calling the Get-Process Cmdlet, which – as the names implies – retrieves a collection of all the processes running on the computer. Of course, we don’t really want a collection of all the processes running on the computer; after all, that collection includes services and other processes that aren’t running in a visible window. Therefore, we “pipe” that collection to the Where-Object Cmdlet, and ask Where-Object to filter out all processes except for those where the MainWindowTitle property is not equal to an empty string.

Note. Yes, we know: in Windows PowerShell you use the –ne operator instead of <>. And no, it doesn’t matter whether we like using –ne; we just have to use –ne.

What’s that going to do for us? That’s going to weed out services and other processes that don’t run in a visible window (if there’s no window then MainWindowTitle will be equal to an empty string). We then pass this filtered collection to the Select-Object Cmdlet and ask Select-Object to display only the MainWindowTitle property:

MainWindowTitle
---------------
open_windows.ps1 - Notepad
Inbox - Microsoft Outlook
Windows PowerShell
more_hey_scripting_guy.doc - Microsoft Word

Nice observation: you won’t find the MainWindowTitle property anywhere in WMI’s Win32_Process class. That’s because the Get-Process Cmdlet doesn’t use WMI to retrieve process information; instead, it uses the .NET Frameowork class System.Diagnostics.Process.

Unfortunately, there’s one minor drawback to this command: it doesn’t return any Windows Explorer windows (for example, a window open to C:\Scripts). But that’s OK; here’s another one-liner that does return Windows Explorer windows:

$a = New-Object -com "Shell.Application"; $b = $a.windows() | select-object LocationName; $b

What we do here is use the New-Object Cmdlet to create an instance of the Shell.Application object. We call the Windows() method to retrieve a collection of Shell windows, then use the Select-Object Cmdlet to echo back the value of the LocationName property. That’s going to result in a report similar to this:

LocationName
------------
Stuff
Hey Scripting Guy
Scripts

Alternatively, you can echo back the LocationURL property to get the complete path. Of course, in that case, you might want to do some cleanup work as well, seeing as how LocationURL returns paths similar to this:

file:///C:/Documents%20and%20Settings/kenmyer/Desktop/Stuff/Hey%20Scripting%20Guy

But here’s a command that might be helpful in trying to clean up that string.

We hope that answers your question, JD. And in case you’re wondering, we gave absolutely no thought to stripping Peter of his Scripting Guy status after he accepted his new job. Was that in recognition of his valuable contributions to the scripting world, contributions that we expect will continue throughout the coming years? You bet it was.

Well, that and the fact that if we got rid of Peter then Greg would become the oldest living Scripting Guy, and there’s no way Greg is going to accept that. So see, Peter? We like to kid you and give you a bad time, but you’ll always fill a very important role on the team. Always.


How Can I Close a Command Window with a Specific Title?

Hey, Scripting Guy! How can I use WSH to close a command window with a specific title?

-- MF

Hey, MF. You know, some things are just really hard to believe, regardless of whether or not they happen to be true. For example, according to the Big Bang Theory the entire universe was created when a miniscule spec of super-dense matter exploded several billion years ago. That’s crazy, but physicists insist it’s true. The University of Washington football team has won only 3 games in the past 2 years? That’s even crazier. But, as Casey Stengal said, you can look it up.

Editor’s Note. Want to hear something really crazy? Today is the one-year anniversary of the Scripting Guys Editor joining the team. If you knew the history of the Scripting Guys and editors, you’d know how incredible it is that one has survived a whole year. Actually, from what you do know of the Scripting Guys, you’re probably wondering how anybody could work with them a whole year. I know this particular editor is wondering….

Or how about this one: you can’t use WSH (or WMI, for that matter) to close a command window with a specific title. WMI enables you to close command windows (or, more correctly, it enables you to terminate processes running under Cmd.exe) but WMI has no knowledge of command window titles. If all you have to go on is the command window title WMI becomes an all-or-nothing proposition: you can either close all the command windows (that is, all processes running under Cmd.exe) or you can close none of the command windows. But you can’t close a specific command window, at least not based on window title alone.

Of course, there’s one thing that is easy to believe: any time the Scripting Guys tell you that something can’t be done the odds are they’ll come up with some crazy workaround that lets you do the task anyway. This is no exception. Maybe you can’t use WSH to close a command window with a specific title; however, you can use Microsoft Word to close a command window with a specific title.

Believe it or not.

For example, here’s a script that closes a command window with the title My Window:

Set objWord = CreateObject("Word.Application")
Set colTasks = objWord.Tasks

If colTasks.Exists("My Window") Then
    colTasks("My Window").Close
End If

objWord.Quit

As you can see, the script begins by creating an instance of the Word.Application object. (See? We told you you could use Microsoft Word to carry out this task!) We then use this line of code to return a collection of all the tasks (processes) running on a computer:

Set colTasks = objWord.Tasks

Note. You might have noticed that, unlike most of the Word scripts we use, we never set the Visible property to True in this script. Why not? Well, setting the Visible property to True causes Word to appear onscreen. In this case we don’t want – or need – Word to show up onscreen; we just need it to close a command window for us. So we keep it hidden.

So what’s the difference between the Tasks collection in Word and the set of processes returned by WMI’s Win32_Process class? The primary difference is that Word returns the “friendly name” for each process. WMI returns the name of the executable file, so you get back information like this:

winword.exe
sol.exe
cmd.exe

With Word, though, you get back information that looks like this:

Test.doc – Microsoft Word
Solitaire
My Window

Generally speaking, the task name returned by Word will be the name that appears in the title bar of the application window. (That obviously won’t be true for processes that run in hidden windows, but we won’t worry about those today.) As you can see, our command window is returned as My Window and not as the generic Cmd.exe. What does that mean? That means Word – unlike WSH and WMI – can locate specific command windows by title.

Furthermore, Word can also close those command windows. After returning the collection of tasks running on the computer we use the Exists method to determine whether or not there is a process (or task) named My Window. If there is, we call the Close method and terminate that process. That takes just three lines of code:

If colTasks.Exists("My Window") Then
    colTasks("My Window").Close
End If

All we have to do then is use the Quit method to terminate our invisible instance of Word and we’re done.

Yes, we know it’s hard to believe (though less hard than believing that Shrek would turn out to be one of the most popular movies of all time). But it’s all true. For more information about the Tasks collection in Word (and what you can do with) take a look at the article Build Your Own Task Manager Using Microsoft Word (No, Really).

And, sadly, it is true: the Husky football team has only won 3 games in the past two years. But the basketball team is already 6-0 this season. Go Dawgs!


How Can I Determine the Account a Process is Running Under?

Hey, Scripting Guy! I’ve got a script that returns information about all the processes running on a computer, except I can’t seem to figure out how to get the name of the user account that these processes are running under. Can you help?

-- DL

Hey, DL. Yes, we can help. It’s actually fairly easy to determine which account a process is running under, it’s just not very obvious how you go about doing that. If you’re like most people, you probably scanned the properties for the Win32_Process class trying to find a property named Account or UserName or something similar. Most likely you didn’t find it. And there’s a good reason for that: the Win32_Process doesn’t have a property that tells you which account a process is running under.

Instead, you need to use a method – GetOwner – to track down this information. Here’s a script that tells you which account Microsoft Word (Winword.exe) is running under:

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

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'Winword.exe'")

For Each objProcess in colProcessList
    objProcess.GetOwner strUserName, strUserDomain
    Wscript.Echo "Process " & objProcess.Name & " is owned by " _
        & strUserDomain & "\" & strUserName & "."
Next

The line of code we’re most interested in is this one:

objProcess.GetOwner strNameOfUser, strUserDomain

What we’re doing here is calling the GetOwner method. GetOwner returns two “out parameters,” one that returns the name of the user responsible for the process, the other returning the domain that user belongs to. In order to capture these two out parameters we need to supply the GetOwner method with two variables. In this sample script, we’ve used variables named strUserName and strUserDomain. The names are arbitrary; you can call the variables A and B or X and Y or anything you want.

However, the order of the variables is not arbitrary: the first value returned will always be the user name, the second value will always will be the domain. Which means that if you want X to represent the user name and Y to represent the domain, then make sure your code looks like this:

objProcess.GetOwner X, Y

After calling GetOwner we simply echo back the process name and the owner. Notice that – to be a little fancy – we use the domain\user format; that way, we echo a name like fabrikam\kenmyer.

Incidentally, here’s a script that lists all the processes on a computer as well as the owner of each process:

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

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process")

For Each objProcess in colProcessList
    objProcess.GetOwner strUserName, strUserDomain
    Wscript.Echo "Process " & objProcess.Name & " is owned by " _
        & strUserDomain & "\" & strUserName & "."
Next

Oh, and in case anyone is wondering, January 3, 2005 happens to be an official day off for Microsoft employees. So why is there a Hey, Scripting Guy! column today? Well, that can only be because of the incredible dedication and devotion to duty shown by the Microsoft Scripting Guys. Either that, or one of the Scripting Guys – who shall remain nameless – didn’t realize it was a holiday and came in anyway (and at 7:00 AM to boot!).


How Can I Determine the Date and Time that a Process Started?

Hey, Scripting Guy! How can I determine the date and time that a process started?

-- BM

Hey, BM. We’ve got good news for you: not only do we have an answer to your question, but we’re not going to bore everyone with any more stories regarding our Colt League All-Star team. Nope, we’re no longer haunted by the fact that, in a tie game, we had the bases loaded, one out, and were unable to score a run, even though we have our three and four hitters up. We are no longer wondering what might have happened had we been able to get that bunt down early in the game. What if our third baseman hadn’t lost that popup in the sun? Hey, it doesn’t matter; it’s just a game, right?

OK, we don’t believe that for a second. Therefore, maybe we better try to forget about baseball and answer your question for you. Here’s a script that can tell you the date and time that Notepad.exe started:

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

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'notepad.exe'")

For Each objProcess in colProcessList
    Wscript.Echo objProcess.CreationDate
Next

Yes, very easy. We start off by binding to the WMI service, then use this query to return a collection of all the processes named notepad.exe:

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'notepad.exe'")

We then set up a For Each loop to cycle through the collection, echoing the value of the CreationDate property for each item in the collection. How complicated could something like that be?

Well, OK, maybe a little more complicated than what we showed you. The preceding script works, but there is a problem. The CreationDate property, like most WMI date-time values, is stored using the Universal Time Coordinate (UTC) format. That means that the process start time is going to be returned looking something like this:

20050718095318.019149-420

How…nice….

We won’t go into all the gory details regarding the UTC time format; you can find a reasonably good discussion of that in the Microsoft Windows 2000 Scripting Guide. Although it might not look like it, our return value actually has all the information we need; for example, the 2005 at the beginning of the string indicates the year (2005), the subsequent 07 indicates the month (July), and so on. All we have to do is parse the string and put the pieces back together in a more-recognizable format.

One way to do that is to use the VBScript string manipulation functions to grab the individual date parts out of the return value and then rearrange them in a standard date-time format. (There’s actually an easier way to do this on Windows XP and Windows Server 2003, but we’ll show you a more generic approach for now.) For example, we know that characters 1 through 4 in our return value represent the year that the process started; therefore, we can use code like this to return just those four characters (assuming that we’ve stored the return value in a variable named dtmStart):

Left(dtmStart, 4)

As you can see, we use the Left function to grab the four leftmost characters (that is, the first four characters in the string). We also know that the month is represented by characters 5 and 6. Therefore we can use the Mid functions to grab the two characters that begin at position 5 in the string:

Mid(dtmStart, 5, 2)

Carried all the way out, we end up with a function similar to this:

Function WMIDateStringToDate(dtmStart)
    WMIDateStringToDate = CDate(Mid(dtmStart, 5, 2) & "/" & _
        Mid(dtmStart, 7, 2) & "/" & Left(dtmStart, 4) _
            & " " & Mid (dtmStart, 9, 2) & ":" & _
                Mid(dtmStart, 11, 2) & ":" & Mid(dtmStart, _
                    13, 2))
End Function

All we have to do now is incorporate this function into our script. To do that we don’t echo the value of the CreationDate property; instead, we store that value in a variable named dtmStartTime. We pass that value to our function, then echo the revised value returned by that function. Here’s what our modified For Each loop looks like:

For Each objProcess in colProcessList
    dtmStartTime = objProcess.CreationDate
    strReturn = WMIDateStringToDate(dtmStartTime)
    Wscript.Echo strReturn
Next

And here’s what the new and improved script looks like:

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

Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'notepad.exe'")

For Each objProcess in colProcessList
    dtmStartTime = objProcess.CreationDate
    strReturn = WMIDateStringToDate(dtmStartTime)
    Wscript.Echo strReturn
Next

Function WMIDateStringToDate(dtmStart)
    WMIDateStringToDate = CDate(Mid(dtmStart, 5, 2) & "/" & _
        Mid(dtmStart, 7, 2) & "/" & Left(dtmStart, 4) _
            & " " & Mid (dtmStart, 9, 2) & ":" & _
                Mid(dtmStart, 11, 2) & ":" & Mid(dtmStart, _
                    13, 2))
End Function

When we run this script we get back output that looks like this:

7/18/2005 9:53:18 AM

That’s a little nicer, almost nice enough to make you wonder what would have happened had we changed pitchers one batter earlier, right before we gave up that bases-loaded triple….


How Can I Find the Process ID Associated with a Batch File?

Hey, Scripting Guy! How can I find the process ID associated with a batch file?

-- MB

Hey, MB. You know, the Scripting Guys are firm believers in recycling. Why throw that printout away when it can be recycled into newsprint? Why throw that aluminum can away when it can be melted down and reused? Why throw away that Hey, Scripting Guy! column when you can use it to answer someone else’s question a few months later?

In other words, today just happens to be Recycling Day here in the Script Center. A long time ago (October 6, 2004, to be precise) we wrote a column telling people how they could determine which scripts (by file name) were running on a computer. Today we’re going to recycle that column, using a slight variation to show you how you can determine the process ID associated with any batch files that are running on your computer. Some of the stuff we’ll show you today is new, but one thing that hasn’t changed is the disclaimer: this solution works only on Windows XP or Windows Server 2003. (We’ll explain why in a bit.) And, no: we don’t really know of a good way to do this on any earlier version of Windows. Sorry.

Assuming you’re running Windows XP or Windows Server 2003, however, the following script reports back the name and process ID of any batch file running on a computer:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_Process")

For Each objItem in colItems
    If InStr(objItem.CommandLine, ".bat") Or InStr(objItem.CommandLine, ".cmd") Then
        Wscript.Echo "Batch file: " & objItem.CommandLine
        Wscript.Echo "Process ID: " & objItem.ProcessID
    End If
Next

So how does this script work? Good question. To begin with, we bind to the WMI service on the local computer. (Although we could easily adapt the script to retrieve the process IDs on any batch files running on a remote computer; all we’d have to do is set the value of the variable strComputer to the name of that remote computer.) After making the connection we then use this line of code to retrieve a collection of all the processes running on the computer:

Set colItems = objWMIService.ExecQuery("Select * From Win32_Process")

So far so good, right? Next we set up a For Each loop to loop through the collection of processes. Inside that loop we use the InStr method to see if the strings .bat or .cmd can be found anywhere within the value of the CommandLine property, which simply reports back the command line value used to start a process. (What if you started the process by double-clicking an icon in Windows Explorer? No problem; in that case CommandLine is equal to the command line value you would have used had you started the process from the command line or from the Run dialog box.)

For example, here’s the kind of information you can expect to see when you look at the value of the CommandLine property:

cmd /c ""C:\Scripts\test.cmd" "
"C:\Program Files\Internet Explorer\IEXPLORE.EXE"
"C:\WINDOWS\system32\NOTEPAD.EXE" C:\Scripts\cmdline.vbs

If you take a look at the first process you can see that we have a batch file named Test.cmd running. By looking for processes where the CommandLine property includes the strings .bat or .cmd (the file extensions used for batch files) we can determine whether any of these processes are actually running batch files. And once we know that, getting the process ID for those batch files is trivial.

Note. Yes, you’re right: a more precise test would be to check and see if the last four characters of the path name are .bat or .cmd. We didn’t do that because the double quotes tacked on the end of the CommandLine value -- cmd /c ""C:\Scripts\test.cmd" " – complicate things a bit. We’re assuming that, in the vast majority of cases, any time you have a .bat or a .cmd in the path then you have a batch file. But if you want to improve upon what we’ve done here, well, who are we to stop you?

In case you’re wondering, this also explains why our script works only on Windows XP and Windows Server 2003: that’s because the CommandLine property is available on only those two platforms.

So what if we do find a batch file? In that case we simply use these two lines of code to echo back the process name and the process ID:

Wscript.Echo "Batch file: " & objItem.CommandLine
Wscript.Echo "Process ID: " & objItem.ProcessID

And there you have it.

Of course, we should point out that, as written, this script returns the name and process ID of all the batch files running on a computer. In your question, MB, you implied that you were interested in only a particular batch file. If that’s the case, then simply modify the If-Then statement so it looks for a CommandLine containing the full batch file name. For example, this modified script checks to see if any processes have the string Test.cmd somewhere in the CommandLine property:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_Process")

For Each objItem in colItems
    If InStr(objItem.CommandLine, "test.cmd") Then
        Wscript.Echo "Process ID: " & objItem.ProcessID
    End If
Next

If found, the script reports back the process ID for only the specified batch file.

Want to get really fancy? Here’s a monitoring script that alerts you any time a new batch file starts up:

strComputer = "."

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

Set colMonitoredProcesses = objWMIService. _
    ExecNotificationQuery("select * from __instanceCreationEvent " _
        & " within 1 where TargetInstance isa 'Win32_Process'")
i = 0

Do While i = 0
    Set objProcess = colMonitoredProcesses.NextEvent
    If InStr(objProcess.TargetInstance.CommandLine, ".cmd") Or _
        InStr(objProcess.TargetInstance.CommandLine, ".bat") Then
        Wscript.Echo "Batch file: " & objProcess.TargetInstance.CommandLine
        Wscript.Echo "Process ID: " & objProcess.TargetInstance.ProcessID
    End If
Loop

We won’t discuss this script in any detail today; for more information see the webcast An Ounce of Prevention: An Introduction to WMI Events. (And yes, we are recycling old material again, this time an old Scripting Week 2 webcast. Didn’t we mention that today was Recycling Day?)


How Can I Lower the Priority of a Process Using a Script?

Hey, Scripting Guy! How can I lower the priority of a process using a script?

-- HS

Hey, HS. As many of you know, all processes are not created equal; instead processes are assigned priority values. Processes with a high priority are executed more often (and thus appear to run faster) than processes with a lower priority. For example, both the keyboard and mouse run in a system process with a very high priority; if they did not you would notice a delay each time you typed a key on the keyboard or moved the mouse.

Generally speaking most processes run with a Normal priority: not too fast, not too slow. That seems to work out pretty good. However, there might be times when you want to change the priority of a particular process. For example, late at night you might want to increase the priority of the Process X; that way you can ensure that whatever task the process is carrying out gets completed faster. Conversely, if Process X is running during the middle of the day you might want to decrease the priority; that way the process won’t interfere with other tasks being carried out on the computer. That’s a quick overview and there are obviously other things to consider before you start changing process priorities. But at least you know what we’re talking about.

So let’s assume that you do want to change the priority of a process. You can do that quite easily using Task Manager; just right-click the process, choose Set Priority, and then set one of the available options:

Nice. But can you do this same thing using a script?

Well, as long as you’re running Windows XP or Windows Server 2003 the answer is yes. (Sorry, Windows 2000 users, but this won’t work for you.) That’s because the WMI class Win32_Process, found in those versions of Windows, includes a method named SetPriority, which allows you to modify the priority of a running process. For example, here’s a script that sets the priority of Notepad.exe to Below Normal:

Const BELOW_NORMAL = 16384

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

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'Notepad.exe'")
For Each objProcess in colProcesses
    objProcess.SetPriority(BELOW_NORMAL)
Next

The script begins by defining a constant named BELOW_NORMAL and setting the value to 16384; as you might expect, we’ll use this constant later on to change the priority of Notepad to Below Normal. Are there other values you can use to assign a different priority to a process? Of course there are:

Priority Class | Value
Normal | 32
Low | 64
Real-time | 128
High | 256
Below Normal | 16384
Above Normal | 32768

After creating the constant the script connects to the WMI service, then uses this line of code to retrieve a collection of all the processes with the name Notepad.exe:

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'Notepad.exe'")

After the collection has been returned we simply use a For Each loop to cycle through all the instances of Notepad.exe. (For this script we’re assuming there’s either only one such instance or, if there all multiple instances, you want all of them to have a Below Normal priority.) Inside that loop we call the SetPriority method, passing as the sole parameter the constant BELOW_NORMAL:

objProcess.SetPriority(BELOW_NORMAL)

If you check the priority of Notepad.exe (either using a script or using Task Manager) you’ll see that it now has a priority of Below Normal.

One caution here: you might want to experiment with different priorities on a test machine before actually trying this script on, say, your file server. Changing the priority of Notepad to Below Normal is probably harmless; changing the priority of your database application to Below Normal might cause problems for your users. Likewise we wouldn’t encourage you to set the process of any priority to Realtime; if you do, it’s very possible that process could gobble up all the system resources and pretty much prevent anything else – including key operating system functions – from running. Just a helpful tip from your friendly neighborhood Scripting Guys!


How Can I Monitor the Activity Level of a Process?

Hey, Scripting Guy! How can I monitor the activity of a process to see if anyone is using it?

-- AJ

Hey, AJ. You know, this is one time when writing the script is actually the easy part of the answer. Although there are a couple different ways to do this, we opted to periodically check the cumulative amount of processor time used by an application. If the amount of processor time never changes, that likely means that no one is using the application.

But if that’s the easy part of the answer then what’s the hard part? Well, in this case the hard part involves deciding what counts (or doesn’t count) as “not being used.” In our sample script, we’re going to measure the cumulative processor use 11 times, pausing 30 seconds between each measurement. When the script finishes we’ll have a set of measurements spanning 5 minutes; our assumption is that if the processor time hasn’t changed in the last 5 minutes then that means no one is using the application. That works fine for our sample script, but might not work as well in real life. After all, suppose your user is talking on the phone, taking a coffee break, or sitting in a meeting; in that case, it wouldn’t be too surprising that an application would sit for 5 minutes (or more) without being used and without using up additional processor time. You’ll need to decide for yourself what’s reasonable for your purposes.

We should also add that there’s no reason why we have to take measurements every 30 seconds; we did that primarily so you could view the cumulative processor time on screen, and so that you’d know for sure that the script was still working. We could just as easily take one measurement, wait five minutes, and then take a second measurement. Five minutes might not seem that long, but it’s practically an eternity when you’re waiting to see whether a script is actually doing anything or not. Again, you’ll need to decide what’s reasonable and what isn’t.

Yes, we know: that’s all well and good, Scripting Guys, but where is this script you keep talking about? Well, right here:

strComputer = "."

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

For i = 1 to 11

    Set colProcesses = objWMIService.ExecQuery _
        ("Select * from Win32_Process Where Name = 'Notepad.exe'")

    For Each objProcess in colProcesses
        sngProcessTime = (CSng(objProcess.KernelModeTime) + _
                CSng(objProcess.UserModeTime)) / 10000000
        Wscript.Echo objProcess.Name, sngProcessTime
    Next

    Wscript.Sleep 30000
Next

Note. This particular script monitors the process Notepad.exe, and assumes that there’s only one instance of Notepad running on your computer; if there are (or could be) multiple instances of Notepad then you should monitor by process ID rather than process name.

The script begins by connecting to the WMI service on the local computer. We then set up a For Next loop to run 11 times, one for each measurement we plan to take. Inside the loop we use this line of code to return a collection of all the processes named Notepad.exe that are running on the computer:

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'Notepad.exe'")

Important. Make sure this line of code is included inside your For Next loop; that’s the only way to ensure that up-to-date process information is retrieved each time your run through the loop.

As it turns out, WMI tracks processor time using two different properties: KernelModeTime and UserModeTime. To track total processor time we need to add the values of these two properties. In addition, WMI tracks processor time in 100-nanosecond units. Because most of us don’t think in terms of 100-nanosecond units (hey, we did say most of us) we also divide the sum of these two properties by 10,000,000, something that will convert 100-nanosecond units to seconds. Sure, it sounds complicated, but it takes only one line of code:

sngProcessTime = (CSng(objProcess.KernelModeTime) + _
    CSng(objProcess.UserModeTime)) / 10000000

If you’d rather calculate processor time in minutes, then divide the sum by 600,000,000 instead (10000000 x 60 seconds per minute).

We echo the process name and total processor time, and then use the Sleep method to pause the script for 30 seconds (30,000 milliseconds). When 30 seconds are up, we loop around and repeat the whole process. After 11 such loops, the script terminates.

When we run the script we get back information similar to this:

notepad.exe 0.1602304
notepad.exe 0.801152
notepad.exe 0.801152
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664
notepad.exe 0.8111664

As you can see, there was a very brief “flurry” of activity when we first began monitoring Notepad; for the last 8 measurements, however, Notepad hasn’t used any processor time at all. (And, overall, the application has still used less than a second of processor time.) Does that mean that no one is using Notepad? Well, like we said, that’s up to you to decide. After all, we work for Microsoft, and no Microsoft employee would ever presume to tell you what to do. (Well, pretty much.)


How Can I Remotely Start an Interactive Process?

Hey, Scripting Guy! How can I remotely start an interactive process?

-- IS

Hey, IS. Well, the short answer is this: you can’t, at least not on Windows XP or Windows Server 2003. On those two versions of Windows any processes started remotely run in a hidden window: those processes can’t be interactive because the user will never see them. The process would do its part; for example, it might pop up a dialog box. However, the user will never see – and thus never respond to – that dialog box. In return, the process will just sit there and wait, like some B-movie actress sitting by the window, waiting for her long-lost lover to return from the sea.

You’re right: that was pretty poetic for a scripting column, wasn’t it? Hey, Scripting Guy!, where you get your daily dose of scripting and fine literature, all in one!

Believe it or not, running processes in a hidden window is – at least in part – a good thing: it helps minimize the risk from hackers who pop up fake dialog boxes that ask users to enter their passwords or credit card numbers. (Which reminds us: if you haven’t already emailed us your passwords and credit card numbers, please – um, never mind. The lawyers are reading over our shoulders again.)

In other words, the good news is that running processes in a hidden window helps protect you and your users; the bad news, of course, is that it severely limits your ability to remotely start scripts or other processes that require user interaction.

But you know how it is with scripting: just because you can’t do something doesn’t mean you can’t do something. Instead, you just have to find a way to work around the issue. And we just happened to have a workaround right here:

Const OverwriteExisting = TRUE

Set objFSO = CreateObject("Scripting.FileSystemObject")

objFSO.CopyFile "C:\Scripts\Interactive.vbs", _
    "\\atl-ws-01\C$\Scripts\", OverWriteExisting

strComputer = "atl-ws-01"

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

Set objNewJob = objWMIService.Get("Win32_ScheduledJob")

errJobCreated = objNewJob.Create _
    ("wscript.exe c:\scripts\interactive.vbs", "********084500.000000-420", _
        False, 2, , True, JobID)

As we noted, processes started remotely always run in an invisible window; there’s no way to get around that. If you want to run a process that a user interacts with that process must be started locally. So that’s exactly what we do here. In the first part of the script we copy a file named C:\Scripts\Interactive.vbs to the Scripts folder on the remote computer atl-ws-01. (Note that we’re using the Administrative share – C$ -- to do this. If C:\Scripts is shared out we could use a “standard” UNC path like \\atl-ws-01\scripts instead.) Here’s the code that copies the file, overwriting any other file named Interactive.vbs that might exist in the Scripts folder:

objFSO.CopyFile "C:\Scripts\Interactive.vbs", _
    "\\atl-ws-01\C$\Scripts\", OverWriteExisting

That copies the file Interactive.vbs to the remote computer. But now how do we run that script and, more important, how do we run it locally?

Although there are a couple of different ways to do this, we opted to create a scheduled task that will start the script on the remote computer. To do that, we bind to the WMI service on the remote machine and then use the Get method to connect to the Win32_ScheduledJob class. That’s what these lines of code do:

strComputer = "atl-ws-01"

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

Set objNewJob = objWMIService.Get("Win32_ScheduledJob")

For our sample script, we’re assuming it’s a Tuesday morning and you want your interactive process to run at 8:45 AM. That’s what this crazy-looking line of code does:

errJobCreated = objNewJob.Create _
    ("wscript.exe c:\scripts\interactive.vbs", "********084500.000000-420", _
        False, 2, , True, JobID)

We don’t have room to explain all the parameters of the Win32_ScheduledJob Create method in today’s column; if you need that level of detail you should take a look at this section of the Microsoft Windows 2000 Scripting Guide. In brief, these are the parameters we configured:

Parameter | Description
"wscript.exe c:\scripts\interactive.vbs" | Path to the script/executable file to be run. Because this is supposed to be an interactive process we specifically called out wscript.exe as the script host; that helps ensure that interactive elements (such as message boxes created using Wscript.Echo) actually appear as interactive elements.
"********084500.000000-420" | Time the job should run, using the UTC (Universal Time Coordinate) format. We won’t explain the UTC format today; for more details, see the Scripting Guide. For now, you only have to worry about two things: the 0845 (which represents 8:45 AM in 24-hour time format) and -420 (which represents your time zone offset from Greenwich Mean Time). Depending on the time you want the script to run and the time zone the computer is in you might need to change one or both of these values.
False | Indicates whether or not the scheduled job should run repeatedly. Because we only want this job to run once we set the value to False.
2 | Days that the scheduled job should run.
 | Specifies the days of the month when the job should run. Because this parameter is used only when a job is scheduled to run more than once we simply leave it blank.
True | Indicates that the scheduled job is interactive and should run in a visible window. If this parameter was set to False (the default value) our script would – alas – run in a hidden window.
JobID | Out parameter that holds the Job ID assigned to the new task.

Run the script, and at 8:45 AM Interactive.vbs should start up on the remote machine atl-ws-01. More important, because Interactive.vbs is started locally it will run in a visible window and thus be totally interactive: any message boxes, dialog boxes or other interactive elements used in the script will be accessible to the user.

Like we said, it’s a workaround and it’s a bit of a hassle. But it works, and that’s all we care about.

Well, that and bringing even more culture and sophistication to Hey, Scripting Guy! Maybe we’ll write the next column entirely in iambic pentameter; that would be very chic and very cool.

Anyone have the slightest idea what “iambic pentameter” is?


How Can I Tell Which Switches Were Used When Starting an Executable File?

Hey, Scripting Guy! Is there any way to tell which command-line arguments (if any) were used when starting an executable file?

-- TO

Hey, TO. We don’t mean to cause a scandal in the scripting world – well, OK, we don’t mean to cause any more scandals in the scripting world. But – gasp! – we’re going to cheat a little bit here. This is the first time we’ve ever been asked this question, and yet, to answer it, we’re going to recycle an answer to a different question we were asked a few months ago.

So here goes: as long as you’re running Windows XP or Windows Server 2003, then, yes, it’s very easy to tell which command-line arguments (if any) were used when starting an executable file. That’s because in these two versions of Windows (but not in previous versions), the WMI class Win32_Process includes a property named CommandLine that provides the complete command line (including switches) used to start an executable file.

For example, suppose Netstat.exe is running on a computer, and you want to know which command-line switches were used to start the program. Here’s a script that can tell you that:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'netstat.exe'")

For Each objItem in colItems
    Wscript.Echo objItem.CommandLine
Next

As you can see, a pretty simple little script: we return a collection of all the processes that have the name Netstat.exe, and then we echo the CommandLine property for each one. Suppose we started Netstat using this command: netstat.exe –a 30. In that case, here’s what our script would report for the CommandLine property:

netstat.exe –a 30

Just what you hoped it would report.

The interesting thing about the CommandLine property is that it will often give you useful information even if you never touch the command line. For example, suppose you have a script named C:\Scripts\Inventory.vbs, and suppose you right-click the file in My Computer and choose Edit. That will open the script up in Notepad (assuming Notepad is your default script editor). And here’s the CommandLine property for that instance of Notepad:

C:\WINDOWS\System32\Notepad.exe C:\scripts\inventory.vbs

In other words, not only do we know that Notepad is open, but we know which file (inventory.vbs) is open. This same approach will work will a number of different applications although, for some reason, it doesn’t work with Microsoft Word or Excel. (Interestingly enough, though, it will with PowerPoint.) With Word and Excel applications, you get the CommandLine for starting the program, but no indication as to what document is open. Instead, you get back a generic command-line string like this:

"C:\Program Files\Microsoft Office\OFFICE11\EXCEL.EXE" /e

A mystery we’ll have to investigate some other time.

By the way, if the suspense is killing you, we used this same answer (the CommandLine property of the Win32_Process class) in response to this question: How can I tell which scripts are running on a computer? To do that, you look at the CommandLine property for any Wscript.exe and CScript.exe processes; the CommandLine will include the name of the script being run under one of those script hosts. Or, to put it programmatically:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where Name = 'wscript.exe' OR Name = ‘cscript.exe’")

For Each objItem in colItems
    Wscript.Echo objItem.CommandLine
Next


How Can I Terminate a Process with a Specific PID?

Hey, Scripting Guy! If I have the PID, is there a way to kill a process using a script?

-- JV

Hey, JV. You bet there is. For those of you who aren’t familiar with the acronym, PID is short for Process Identifier, a unique ID number assigned to a process when it gets created. (Well, temporarily unique: as soon as the process is destroyed, the number is recycled and can be assigned to another new process.) The WMI class Win32_Process has a property – ProcessId – that corresponds to the PID. Suppose you want to terminate a process that has a PID of 2576. Here’s a script that will do that very thing:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where ProcessID = 2576")
For Each objProcess in colProcessList
    objProcess.Terminate()
Next

Of course, keep in mind that PIDs are ever-changing: although the process you want to terminate today might have a PID of 2576, the odds are that it will not have the same PID the next time it gets created. Therefore, instead of hard-coding in the PID value, you might want to modify the script so that it can accept any PID as a command-line argument:

If Wscript.Arguments.Count = 0 Then
    Wscript.Echo "You must enter a PID."
    Wscript.Quit
End If

intPID = Wscript.Arguments.Item(0)

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where ProcessID = " & intPID & "")
For Each objProcess in colProcessList
    objProcess.Terminate()
Next

In this script, we check to see if any command-line arguments were entered; if there aren’t any, we echo an error message (“You must enter a PID.”) and then quit. If an argument is entered, we assign the value of the first argument to the variable intPID using this code:

intPID = Wscript.Arguments.Item(0)

What if we enter more than one command-line argument? Well, in this simple script we just ignore additional arguments. But you could easily modify this script to terminate as many processes as you have PIDs for. For more information on how to do that, check out this Tales from the Script column.

After assigning a value to intPID we then use the Win32_Process class to locate the process and terminate it. Note that we don’t hard-code in the PID, but instead use the variable intPID:

("Select * from Win32_Process Where ProcessID = " & intPID & "")

And while you didn’t ask this, you can also terminate processes by name rather than PID. For example, this script terminates all the instances of Notepad.exe running on a computer:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'Notepad.exe'")
For Each objProcess in colProcessList
    objProcess.Terminate()
Next

Like we said, that terminates all instances of Notepad. If you want to terminate only a specific instance, use the PID. PIDs are unique to the collection of running processes; names are not. In other words, there will only be one process with a PID of 2576, but there could be scores of processes with the Name Notepad.exe.

For more information about managing processes using scripts, take a look at this portion of the Microsoft Windows 2000 Scripting Guide.


How Can I Execute a Scheduled Task in Windows Vista?

Hey, Scripting Guy! I understand that, in Windows Vista, I can write a script that will cause a scheduled task to immediately execute. How do I do that?

-- KW

Hey, KW. Before we answer today’s question, we’d like to take a moment to say a few things in memory of our colleague, Scripting Guy Dean Tsaltas. Not that Dean has died or anything; physically he’s in great shape. (Well, OK: physically he’s in as good a shape as he ever is.) No, as it turns out, Dean has suffered a fate far worse than death: he and his family are moving back to Halifax, Nova Scotia.

Sample Nova Scotian humor. Did you hear about the war between Newfoundland and Nova Scotia? The Newfies were lobbing hand grenades; the Nova Scotians were pulling the pins and throwing them back.

OK, well, we assume this joke is like curling: you probably have to be Canadian to truly appreciate it.

Now, for those of you who worry about things like this, don’t worry: Dean was, is, and always will be a Scripting Guy. In addition, Dean is still working for Microsoft, and he’s working on the WMI SDK; the only difference is that he’ll be doing this work in Halifax, where you’re much more likely to find a slice of Rappie Pie than you are here in Redmond.

Nova Scotian recipe. One traditional recipe for Rappie Pie includes, among other things, the following ingredients: 2 pounds of fatty pork; ¼ pound of pork fat; ¼ pound of fried salt port. And if you get tired of Rappie Pie (as if!), well, in that case you can always run down to the grocery store and get a box of salted codfish. As the name implies, salted codfish is nothing more than codfish that has been immersed in salt and then dried in the sun. Do those Nova Scotians know how to live or what?

At any rate, we Scripting Guys would like to take a moment to pay tribute to Dean and all the many wonderful things he’s done for the Script Center and for the scripting world at large. Unfortunately, however, while we’d like to take a moment to pay tribute to Dean and all the many wonderful things he’s done for the Script Center and the scripting world at large, we couldn’t actually think of anything he’s done, wonderful or otherwise. But, then again, he only worked here for six years.

Oh, wait, we just thought of something: Dean did get us all in trouble when he said the word “pee” during one of our Scripting Week 1 webcasts. And he did write the Scriptomatic, the tool that, more than anything else, helped bring WMI into the world of system administration. And if that wasn’t enough, you could also count on Dean to go along whenever it was time for lunch; in fact, Dean would happily take two or three lunches a day if necessary.

And once Dean is gone, how will the rest of ever know what’s going on in the world of Ultimate Fighting?

In other words, this is a sad day for the Scripting Guys. But things will work out: after all, Dean might be gone, but he won’t be forgotten. Instead, he’ll remain foremost in our minds, the very same way that … um … that the … uh … other Scripting Guy, the one who became a PM or something, has remained foremost in our minds.

Besides, there’s nothing to prevent all of us from going to Halifax to visit Dean, is there?

Nova Scotian Weather Report. In January, 1993 the temperature at the Halifax Airport reached a low of -28.5 degrees Celsius (-19.3 degress Fahrenheit). On February 19, 2004, the airport was inundated with 66 centimeters (26 inches) of snow.

Of course, sad day or not, duty calls. With that in mind, here’s a little farewell gift to Dean from his fellow Scripting Guys; a script that, under Windows Vista, can immediately execute a scheduled task:

Set objTaskService = CreateObject("Schedule.Service")
objTaskService.Connect

Set objRootFolder = objTaskService.GetFolder("\")
Set objTask = objRootFolder.GetTask("Test Task")

objTask.Run vbNull

If this looks like a pretty simple little script, well, there’s a good reason for that: it is a pretty simple little script. As you can see, we start out by creating an instance of the Schedule.Service object, and then call the Connect method; that connects us to the Task Scheduler service on the local computer. Can we perform this same task on a remote computer, say, a computer named atl-fs-01? You bet we can; all we have to do is pass the name of that computer to the Connect method, like so:

objTaskService.Connect("atl-fs-01")

After connecting to the Task Scheduler service, we use the GetFolder method to bind to the Task Scheduler’s root folder:

Set objRootFolder = objTaskService.GetFolder("\")

What’s that? What if your task is in a subfolder of the root folder; you know, a folder with a name like Admin Tasks? No problem; just specify the complete path to the folder, like so:

Set objRootFolder = objTaskService.GetFolder("\Admin Tasks")

Once we connect to the proper folder we can then call the GetTask method to create an object reference to the desired task; to do this, we simply pass GetTask the name of the task we want to run:

Set objTask = objRootFolder.GetTask("Test Task")

And then to run the task we just have to call the Run method, passing the VBScript constant vbNull as a way to indicate that we want to run the task without any special parameters:

objTask.Run vbNull

Just like that, the scheduled task Test Task will run.

Note. You’re absolutely right: the Task Scheduler in Windows Vista is way cooler – and way more scriptable – than the Task Scheduler in other versions of Windows. For a quick introduction to writing scripts that interact with the Windows Vista Task Scheduler take a peek at our article Task Scheduler: Part 1.

And just like that, we’re done for today. We’ll be back tomorrow, although with a heavy heart: tomorrow is Dean’s last day. But to tell you the truth, between Rappie Pie, salted codfish, and the Duck Trolling Retriever (the official dog of the province of Nova Scotia), it’s amazing to us that everyone hasn’t decided to move to Nova Scotia. Good luck, Dean, and remember: although you might be in Halifax, there will always be a place for you here in the Script Center.

Well, as long as you don’t use the word “pee.” We’d just as soon not go through all that again.


How Can I Manage Scheduled Tasks Using Scripts?

Hey, Scripting Guy! Is there an easy way to manage scheduled tasks using scripts?

-- RC

Hey, RC. You just had to ask that question, didn’t you? To be honest, this is not one of scripting’s strong points, and you might be more confused after hearing the answer than you were before asking the question. But here goes.

The problem we have here is that Windows actually has two different – and, sadly, not fully-compatible – APIs (Application Programming Interfaces) used to manage scheduled tasks. For one, there are the Task Scheduler APIs used by both the Task Scheduler and by Schtasks.exe, a command-line task management tool that ships with Windows XP and Windows Server 2003. For another, there are the so-called At APIs used by At.exe and by WMI’s Win32_ScheduledJob class.

So what’s the problem? Well, let’s say you use WMI to create a scheduled task (and we’ll show you a sample script for doing so in a minute). If you do that, you can then use a script similar to this to retrieve information about that task and about any other tasks created using the At APIs:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colScheduledJobs = objWMIService.ExecQuery _
    ("Select * from Win32_ScheduledJob")
For Each objJob in colScheduledJobs
    Wscript.Echo "Caption: " & objJob.Caption
    Wscript.Echo "Command: " & objJob.Command
    Wscript.Echo "Days Of Month: " & objJob.DaysOfMonth
    Wscript.Echo "Days Of Week: " & objJob.DaysOfWeek
    Wscript.Echo "Description: " & objJob.Description
    Wscript.Echo "Elapsed Time: " & objJob.ElapsedTime
    Wscript.Echo "Install Date: " & objJob.InstallDate
    Wscript.Echo "Interact with Desktop: " & objJob.InteractWithDesktop
    Wscript.Echo "Job ID: " & objJob.JobID
    Wscript.Echo "Job Status: " & objJob.JobStatus
    Wscript.Echo "Name: " & objJob.Name
    Wscript.Echo "Notify: " & objJob.Notify
    Wscript.Echo "Owner: " & objJob.Owner
    Wscript.Echo "Priority: " & objJob.Priority
    Wscript.Echo "Run Repeatedly: " & objJob.RunRepeatedly
    Wscript.Echo "Start Time: " & objJob.StartTime
    Wscript.Echo "Status: " & objJob.Status
    Wscript.Echo "Time Submitted: " & objJob.TimeSubmitted
    Wscript.Echo "Until Time: " & objJob.UntilTime
Next

On top of that, if you open up the Task Scheduler, you’ll see the task in there as well. So far so good, huh?

However, suppose you say to yourself, “Hey, as long as I have the Task Scheduler open I might as well schedule a second task.” Let’s say you do so. That task, created by the Task Scheduler, will not be visible to your WMI scripts. The Task Scheduler can deal with tasks created by WMI, but WMI cannot deal with tasks created by the Task Scheduler.

Even worse, suppose you use Task Scheduler to modify the task you originally created with WMI. That task will be successfully modified, but you’ll no longer be able to access it using WMI, even though WMI created the thing in the first place. Yes, we know. And, hopefully, this problem will be corrected in future versions of Windows. And, yes, we know: that doesn’t really help you much right now, does it? Sorry.

So what does all this mean? Well, it means that managing scheduled tasks using WMI is kind of a hit-or-miss proposition. If you can be assured that no one is scheduling tasks using the Task Scheduler then WMI works great. But if people are using both WMI scripts and the Task Scheduler, well, then management becomes a bit more complicated, to say the least.

So how do you create a scheduled task using a script? Try this example for starters:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set objNewJob = objWMIService.Get("Win32_ScheduledJob")
errJobCreated = objNewJob.Create _
    ("Cleanup.exe", "********123000.000000-420", _
        True , 1 OR 4 OR 16, , , JobID)
Wscript.Echo errJobCreated

A bit cryptic, isn’t it? The preceding script schedules a hypothetical program named Cleanup.exe to run at 12:30 PM every Monday, Wednesday, and Friday. For more information about the different parameters used in this script, you might want to check out this section of the Microsoft Windows 2000 Scripting Guide.


How Can I Determine if a Particular Patch is Installed?

Hey, Scripting Guy! How can I use a script to determine if a particular patch has been installed?

-- GM

Hey, GM. You know, not too long ago this would have been a question we might have “accidentally” dropped in the wastebasket and pretended never to have seen. Why? Well, back in the old days the only way to get information about patches, hot fixes, and other updates was to use the WMI class Win32_QuickFixEngineering. That was OK, but for various reasons Win32_QuickFixEngineering would occasionally miss a patch that really was installed; even worse, on Windows 2000 machines Win32_QuickFixEngineering would sometimes hang and not return any information. (How do you fix that problem? By installing a patch, of course.) Not a pretty picture by any means.

But those days are gone. Now, thanks to the new and very-much improved Windows Update service, it’s very easy to determine which updates have and have not been installed on a computer. For example, here’s a script that tells us whether the patch Security Update for Windows XP (KB899587) has been installed on a computer:

Set objSession = CreateObject("Microsoft.Update.Session")
Set objSearcher = objSession.CreateUpdateSearcher
Set objResults = objSearcher.Search("Type='Software'")
Set colUpdates = objResults.Updates

For i = 0 to colUpdates.Count - 1
    If colUpdates.Item(i).Title = _
        "Security Update for Windows XP (KB899587)" Then
        If colUpdates.Item(i).IsInstalled <> 0 Then
            Wscript.Echo "This update is installed."
            Wscript.Quit
        Else
            Wscript.Echo "This update is not installed."
            Wscript.Quit
        End If
    End If
Next

Wscript.Echo "This update is not installed."

We won’t go through each line of code in detail; explaining the ins and outs of the Windows Update service goes a bit beyond the scope of this column. If you’d like more information about Windows Update (and, in particular, objects like Microsoft.Update.Session) please see our Tales from the Script column I’ll Get You, My Pretty…And We’ll Manage Windows Update, Too!

We will note, however, that despite the presence of the Search method we aren’t really searching for a particular update. Searching implies going out and, with pinpoint accuracy, locating just the item of interest. We can’t really do that. Instead, what we’re doing here is returning a collection of all the updates and then sifting through that entire collection, looking for an update with the title Security Update for Windows XP (KB899587). The end result is the same, but the process by which we achieve that end result is a little different.

No, that doesn’t make any difference. Just thought you’d like to know.

So what is the process we employ here? Well, we begin by using these four lines of code to retrieve the updates collection for the local computer:

Set objSession = CreateObject("Microsoft.Update.Session")
Set objSearcher = objSession.CreateUpdateSearcherSet
objResults = objSearcher.Search("Type='Software'")
Set colUpdates = objResults.Updates

Note. Yes, this script can be run against a remote computer. For more information, see the Tales from the Script column.

After retrieving the collection we set up a For Next loop to walk through all the items. Inside that loop we use this line of code to determine whether the update Title is equal to Security Update for Windows XP (KB899587):

If colUpdates.Item(i).Title = _
    "Security Update for Windows XP (KB899587)" Then

Let’s suppose that the Title is equal to Security Update for Windows XP (KB899587). In that case, we next check the value of the IsInstalled property. If IsInstalled is equal to 0, then the update is not actually installed (installation might have failed, or the update might have been installed but then removed). If IsInstalled does not equal 0, then the update is installed. We check the value, and then echo the appropriate message:

If colUpdates.Item(i).IsInstalled <> 0 Then
    Wscript.Echo "This update is installed."
    Wscript.Quit
Else
    Wscript.Echo "This update is not installed."
    Wscript.Quit
End If

You might notice that, after echoing our message, we terminate the script. Why? Well, update titles are unique: now that we’ve found the update in question there’s no need to continue walking through the rest of the collection. Therefore, we might as well terminate the script and get on with our lives.

And what if the Title isn’t equal to Security Update for Windows XP (KB899587)? In that case we simply loop around and check the next item in the collection. If we never do find an update with the specified title we’ll eventually exit the loop, and then execute the last line of code, which simply reports that the update is not installed.


How Can I Determine When the Last Patch From Windows Update was Applied?

Hey, Scripting Guy! How can I determine when the last patch from Windows Update was applied?

-- JP

Hey, JP. You know, you picked a good time to ask this question; that’s because we just published a Tales from the Script column that introduces scripters to the Windows Update object model, and provides a number of sample scripts for managing Automatic Updates. And while the article doesn’t have a script that returns information for just the last update applied to a computer, well, it was easy enough to come up with one:

Set objSession = CreateObject("Microsoft.Update.Session")
Set objSearcher = objSession.CreateUpdateSearcher

Set colHistory = objSearcher.QueryHistory(1, 1)

For Each objEntry in colHistory
    Wscript.Echo "Title: " & objEntry.Title
    Wscript.Echo "Update application date: " & objEntry.Date
Next

What’s going on in this little script? Funny you should ask: we were just about to explain that. We begin by creating an instance of the Microsoft.Update.Session object. Once we have that object in hand we can call the CreateUpdateSearcher method. This gives us an instance of the Searcher object, an object that enables us to search through all the updates that have been applied to a computer. Which is exactly the thing we want to do.

Next we use the QueryHistory method to retrieve the desired updates:

Set colHistory = objSearcher.QueryHistory(1, 1)

As you can see, in this script we pass QueryHistory two parameters: 1 and 1. The first 1 tells the script to begin its search with record 1 in the update history; the second 1 tells the script to stop its search after record 1. (That is, before it gets to record 2.) Why do we do that? Well, updates are stored in reverse chronological order, with the most recent update being record 1 and the very first update ever applied being the last record in the collection. Because we want information about only the most recent update, we can make the script run a tad bit faster by limiting the returned data to just that one record.

Note. We should point out that it’s very easy to retrieve the complete update history for a computer; in fact, the Tales from the Script column includes a script that does just that.

After calling the QueryHistory method we’ll get back a collection (named colHistory) consisting of a single item, an item which just happens to represent the last update that was applied to the computer. All that’s left now is to walk through that collection (and, yes, technically it’s still a collection even though it contains just one item) and echo the title of the update and the date the update was applied:

For Each objEntry in colHistory
    Wscript.Echo "Title: " & objEntry.Title
    Wscript.Echo "Update application date: " & objEntry.Date
Next

In other words, tell us the last time Windows Update did something to this particular computer.

Incidentally, the Windows Update object model is a mixed-bag: some of the objects can be created on – and thus used against – remote computers while others can’t. Fortunately, this happens to be a script that will run against a remote machine. To do that you simply need to add the remote computer name as the second parameter to CreateObject. For example, this line of code causes the script to run against the remote computer atl-dc-01:

Set objSession = CreateObject("Microsoft.Update.Session", "atl-dc-01")

And the complete script for retrieving the last update from atl-dc-01 would thus look like this:

Set objSession = CreateObject("Microsoft.Update.Session", "atl-dc-01")
Set objSearcher = objSession.CreateUpdateSearcher

Set colHistory = objSearcher.QueryHistory(1, 1)

For Each objEntry in colHistory
    Wscript.Echo "Title: " & objEntry.Title
    Wscript.Echo "Update application date: " & objEntry.Date
Next

And people think that patch management is hard. Not anymore!


How Can I Tell Which Service Packs Have Been Installed on a Computer?

Hey, Scripting Guy! How can I tell which service packs have been installed on my computers?

-- FR

Hey, FR. That’s a very good question; as you know better than we do, in this day and age it’s really important to make sure all your computers have the latest service pack, the latest hot fixes, and the latest downloads from Windows Update installed. But, like you said, how can you do that?

Ah, you’re way ahead of us: yes, scripting is a good way to track this information. For example, this simple script reports the latest service pack installed on a computer:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colOperatingSystems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")
For Each objOperatingSystem in colOperatingSystems
    Wscript.Echo objOperatingSystem.ServicePackMajorVersion  _
        & "." & objOperatingSystem.ServicePackMinorVersion
Next

Now you might be thinking, “Oh, the latest service pack. But I need to know about all the service packs that have been installed on a computer.” If that’s the case, relax; when you install, say, Service Pack 3.0, you automatically get everything that was included in Service Packs 1.0 and 2.0. Because of that, it doesn’t really matter whether Service Pack 1.0 was installed; by installing Service Pack 3.0, you got all the functionality of Service Pack 1.0. Which is a somewhat convoluted way of saying that all you really care about is the latest service pack that was installed on a computer. And the preceding script will do just that.

But what about those hot fixes we mentioned; how can you audit a computer and see which hot fixes have been installed? Try this script:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colQuickFixes = objWMIService.ExecQuery _
    ("Select * from Win32_QuickFixEngineering")
For Each objQuickFix in colQuickFixes
    Wscript.Echo "Computer: " & objQuickFix.CSName
    Wscript.Echo "Description: " & objQuickFix.Description
    Wscript.Echo "Hot Fix ID: " & objQuickFix.HotFixID
    Wscript.Echo "Installation Date: " & objQuickFix.InstallDate
    Wscript.Echo "Installed By: " & objQuickFix.InstalledBy
Next

Two things to keep in mind here. First, hot fixes haven’t always been released in standard fashion. That doesn’t mean they don’t work, it just means that some hot fixes might not populate all the above fields; for example, many hot fixes won’t return a value for the InstallDate property. Second, hot fix information is recorded in several places on your computer and, because there hasn’t always been a standard way of doing this, WMI checks all of these locations and retrieves hot fix information. That’s good, except that once in awhile a hot fix will actually record information in multiple locations. Again, that’s not really a problem; it just means that when you get your information back from WMI you might see or 2 or 3 instances of hot fix QFE 1111111. That’s because QFE 1111111 recorded information about itself in several different spots.

Hey, that’s right: we did mention Windows Update, didn’t we? Well, if you’ve installed the latest version of Windows Update, you might very well have a new COM object that enables you to retrieve update information. For example:

Set objSession = CreateObject("Microsoft.Update.Session")
Set objSearcher = objSession.CreateUpdateSearcher
intHistoryCount = objSearcher.GetTotalHistoryCount

Set colHistory = objSearcher.QueryHistory(1, intHistoryCount)

For Each objEntry in colHistory
    Wscript.Echo "Operation: " & objEntry.Operation
    Wscript.Echo "Result code: " & objEntry.ResultCode
    Wscript.Echo "Date: " & objEntry.Date
    Wscript.Echo "Title: " & objEntry.Title
    Wscript.Echo "Description: " & objEntry.Description
    Wscript.Echo "Client application ID: " & objEntry.ClientApplicationID
    Wscript.Echo "Server selection: " & objEntry.ServerSelection
    Wscript.Echo "Service ID: " & objEntry.ServiceID
    i = 1
    For Each strStep in objEntry.UninstallationSteps
        Wscript.Echo i & " -- " & strStep
        i = i + 1
    Next
    Wscript.Echo "Uninstallation notes: " & objEntry.UninstallationNotes
    Wscript.Echo "Support URL: " & objEntry.SupportURL
    Wscript.Echo
Next

To be honest, we only know for sure that the Microsoft.Update.Session object gets installed on Windows XP (and is automatically installed when you install XP Service Pack 2). If you’re running Windows 2000, you might want to visit Windows Update, install the latest version, then run the preceding script and see if it works.


How Can I Change a Service Description?

Hey, Scripting Guy! How can I change a service description?

-- DD

Hey, DD. Well, we hope you’re happy, DD. Now the Scripting Guy who writes this column will never be able to sleep again – and it’s all your fault!

Sure, we’ll explain what we mean by that; after all, now that we are no longer able to sleep we have plenty of time for stuff like that. Many years ago the Scripting Guy who writes this column saw a movie about some boogeyman-like character. The premise of the movie was that this boogeyman existed only as long as people believed in him; if people stopped believing in him he’d disappear forever.

Of course, most people went about their daily lives without ever giving the boogeyman a single thought; needless to say, that’s not a very good thing if you happen to be a boogeyman who will disappear if people stop believing in you. Therefore this boogeyman (and, yes, we suppose we should call it a boogeyperson) started a sort of public relations campaign to get people to believe in him. And once people started believing in him, well ….

Oh, and for some reason the boogeyman has to wait for people to fall asleep so he can enter the world through their dreams and then kill the unsuspecting sleeper (which is the sort of thing that boogeypeople tend to do). To tell you the truth, the Scripting Guy who writes this column was never really sure how this enter-the-world-through-someone’s-dreams thing worked. But, then again, he was never really sure how a boogeyman who allegedly could enter the real world only through someone’s dreams somehow managed to enter the real world and start a public relations campaign, either.

And no, we don’t believe this movie did win an Academy Award for Best Picture. In fact, there’s a chance it was never even nominated for such an award. Go figure.

Anyway, the point is this: once your beliefs change you don’t dare fall asleep; do that and the boogeyman is likely to come and get you. Which means that, thanks to this question about service descriptions, the Scripting Guy who writes this column doesn’t dare fall asleep ever again.

Note. Admittedly, that doesn’t make any sense. But, then again, the fact that things don’t make any sense is how you separate the real Hey, Scripting Guy! columns from their cheap imitations.

As it turns out, the Scripting Guy who writes this column used to go about his daily life without ever thinking about changing the description of a service. After all, why would he think about something like that; considering the fact that you can change service start modes and service passwords and service thises and thats, well, how hard could it be to change a service description? But then he read your question and – foolishly – opened up the WMI SDK to get the exact syntax for changing a service description.

Much to his surprise, there is no WMI method for changing the description of the service. (Hear that ominous music in the background?) The Scripting Guy who writes this column always believed that you could change a service description using a script, but he was wrong. So now he can never go to sleep again, lest the boogeyman come.

On the other hand, if all the boogeyman wants is a script that can change the description of a service, well, he can use this one:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry
= GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "System\CurrentControlSet\Services\SerialKeys"
strValueName = "Description"
strValue = "This is the SerialKeys
service."

objRegistry.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

And no, you’re not dreaming. The Scripting Guy who writes this column was correct: WMI doesn’t provide a straightforward method for changing the description of a service. But that doesn’t mean that you can’t change the description of a service; it just means that you have to use a script that goes out and changes the description in the registry. Like this one does.

So how does this script go out and change the description in the registry? Well, to begin with, it defines a constant named HKEY_LOCAL_MACHINE and sets the value to &H80000002; this tells the script which registry hive to work with. The script then connects to the WMI service on the local computer and, in particular, to the root\default namespace and the StdRegProv class.

Note. Sure, you can use this script to change the description of a service on a remote computer. To do that, just assign the name of the remote machine to the variable strComputer.

After connecting to the WMI service we then assign values to three variables:

· strKeyPath, which represents the path (within HKEY_LOCAL_MACHINE) to the registry key for the service we want to modify. This value should be set to System\CurrentControlSet\Services\ plus the name of the service. Thus: System\CurrentControlSet\Services\SerialKeys.

· strValueName, the name of the registry value to be modified. As you might expect, with this script that happens to be Description.

· strValue, which holds the new description for the service.

After assigning values to those variables all we have to do is pass the variable names and the constant HKEY_LOCAL_MACHINE to the SetStringValue method and we’re done:

objRegistry.SetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

If you now open the Services console you’ll see a new description for the SerialKeys service. Kind of eerie to even think about, isn’t it?

Incidentally, we hope none of you are worried about the health of the Scripting Guy who writes this column; the truth is, he should be able to fall asleep again without any problem. In fact, as near as we can tell he must have dozed off two or three times just trying to finish this column.


How Can I Disable a Service?

Hey, Scripting Guy! How can I disable a service?

-- DS

Hey, DS. You know, the Scripting Guys seem to have stumbled upon a great idea. In our last column, we showed people how to disable the LMHosts file. Today, we’re going to show everyone how to disable a service. If we continue down this path, we’ll soon have shown you how to disable everything on a computer. Just think of that: no more problems with the network, no more help desk calls, no more users overwriting files that shouldn’t have been overwritten. All we have to do is disable everything and all those problems will disappear! This could be our ticket to the big time.

Admittedly, disabling everything on all your computers might introduce some other problems in your organization. But that’s really an internal thing you guys will have to deal with.

Besides, we’ll probably be too busy counting our money to help you with that.

Of course, until that money starts to roll in we still have to write this column and we still have to pay the bills. With that in mind, here’s a script that disables the Alerter service on a computer:

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

Set colServiceList = objWMIService.ExecQuery _
    ("Select * from Win32_Service where Name = 'Alerter'")

For Each objService in colServiceList
    If objService.State = "Running" Then
        objService.StopService()
        Wscript.Sleep 5000
    End If
    errReturnCode = objService.ChangeStartMode("Disabled")
Next

As you can see, it doesn’t take much code to disable a service, and the little bit of code that is required is pretty basic. There is one tricky part, however, which is why we’ll take a few minutes to walk you through the script.

Besides, the Rolls Royce dealership doesn’t open for another hour anyway.

There’s nothing too special about the way the script starts: we simply connect to the WMI service on the local computer (although we could also disable a service on a remote computer). We then issue this query, which returns a collection consisting of just one item: the Alerter service:

Set colServiceList = objWMIService.ExecQuery _
    ("Select * from Win32_Service where Name = 'Alerter'")

Incidentally, make sure you include a Where clause similar to the one above. Why? Well, suppose you leave the Where clause off. In that case, your collection will consist of all the services installed on the computer, and your script will then dutifully try to disable every single service you have.

You’re right: that’s usually not a good thing. Besides, the Scripting Guys would have to sue you; after all, disabling everything on a computer is our idea!

After we get back our collection, we set up a For Each loop to walk through all the items in that collection. (And yes, we know: there’s only one item in the collection. But we still need to use a For Each loop.)

Here’s the tricky part. You can disable a service if it’s running; however, the service will not actually be disabled until it stops. If you disable a running service, the service will continue running the same as ever; the change doesn’t take effect until the service stops.

Because of that, we use this line of code to check and see if the Alerter service is running:

If objService.State = "Running" Then

If the service is running, we then call the StopService() method in order to stop the thing. After that we pause for 5 seconds (5,000 milliseconds); that gives the service time to actually stop before we go ahead and disable it. That’s what we do here:

objService.StopService()
Wscript.Sleep 5000

Note. In the interests of keeping this sample script as short as possible, we’re cheating a little bit. We’re assuming that your service will either be Running or Stopped. In reality, a service can have other states, most notably Paused or Continuing. To make a more bulletproof script, you should probably check for those conditions (and take the appropriate action) as well. But because 99.9% of the time services are either running or stopped, you can usually get by with the sample script we showed you.

Once the service has stopped we then use this line of code to disable it:

errReturnCode = objService.ChangeStartMode("Disabled")

As you can see, we’re simply using the ChangeStartMode method to change the value of the StartMode property. We want to disable the service so we pass the parameter “Disabled” to ChangeStartMode. We could also set the StartMode to “Manual” or “Automatic”; that would, well, set the StartMode to either Manual or Automatic.

Remember, once a service has been stopped and disabled it can no longer be started; the only way to restart the service is to re-enable it (by changing the StartMode to Manual or Automatic). But we’ll wait until we’ve talked everyone into disabling everything on their computers before we tell you how to re-enable stuff. After all, we can probably charge double for those scripts. Do you think it would look weird if all of us got the same diamond-studded Rolex?


How Can I Get the Uptime of a Service?

Hey, Scripting Guy! How can I get the uptime of a service?

-- JC

Hey, JC. You know, if you could have seen the Scripting Guy who writes this column this morning you would have reconsidered asking him a question about uptime. Although he woke up in plenty of time, even turning off the alarm before it was due to sound, he had a … little … trouble actually getting up out of bed. Five minutes past the appointed hour and he was still lying there; ten minutes past the appointed hour and he was still lying there – if he hadn’t had to go to the bathroom he might still be in bed. And while he eventually did get out of bed, whether or not he actually woke up is debatable.

Of course, with this Scripting Guy it’s almost-always debatable whether he’s awake or asleep anyway. For example, last year the Scripting Son had a high school baseball game out of town. At the start of the game the Scripting Guy who writes this column was the only fan in the stands. A short time later another fan showed up and somewhat hesitantly asked the Scripting Guy if he knew who was winning. “It’s 2-to-2 in the top of the second. Brandon hit the ball hard his first time out, but the shortstop made a nice play and threw him out at first. However, he did get an RBI by knocking Barrett in.”

The other fan was taken aback. “Oh, I didn’t know you knew anything about baseball,” she said. “You’re always so quiet, even when we score; I assumed you didn’t know anything about the game. To tell you the truth, we always wondered why you even came to the games.”

That’s OK; he’s used to it. Besides, you have no idea how many times a hearse has come to haul this Scripting Guy away, despite his insistence that he was still alive. (Editor’s Note: What he doesn’t know is that the hearse is always sent by the other Scripting Guys.)

But enough with all that; let’s get to work here. The Scripting Guys have always bragged that they could write system administration scripts in their sleep. We’re about to put that notion to the test.

To begin with, let’s take a look at a script that lets you know the date and time that a service first started. As you’ll soon see, there’s a bit of a problem with this script: it doesn’t report the service start time in user-friendly fashion. But that’s OK; after we explain the basic concept behind determining the start time for a service we’ll show you a much friendlier version.

Here’s the basic script for determining the date and time a service first started:

strComputer = "."

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

Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where Name = 'Spooler'")

For Each objService in colServices
    intProcessID = objService.ProcessID

    Set colProcesses = objWMIService.ExecQuery _
        ("Select * From Win32_Process Where ProcessID = " & intProcessID)

    For Each objProcess in colProcesses
        Wscript.Echo objProcess.CreationDate
    Next
Next

As you can see, we start out by connecting to the WMI service on the local computer, although – that’s right, you can run this script against a remote machine. (Hmmm, you don’t think that, after two-and-half years, we’re beginning to repeat ourselves in this column, do you?) Because we’re interested in the uptime for just one service (in our example, the Print Spooler service), we then issue this query to return a collection of all the services that have the name Spooler (and, yes, because service names must be unique there will be only one such service):

Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where Name = 'Spooler'")

Now, ideally, we’d be done at this point: we’d simply report back the date and time that the service started. That would be nice, but, for better or worse, the Win32_Service class doesn’t include a property that tells us when the service started. So does that mean that all is lost? Yes, yes it does.

Ha-ha; sometimes we crack ourselves up. No, in the wonderful world of scripting nothing is ever lost. While it’s true that the Win32_Service class doesn’t have a property that tells us when a service was started, it does have a property that tells us the process ID (PID) of the process in which the service is running. Is that useful? You bet it: we can take that PID and use it to retrieve process information from the Win32_Process class. Is that useful? Of course it is: after all, the Win32_Process class just happens to have a property (CreationDate) that tells us when the process started.

Note. OK, admittedly, this is not 100% foolproof. Why not? Because it’s not unusual for services to run in shared processes; a single process might host multiple services, some of which might be running and some of which might not be running. However, this is the best we can do short of searching through the System event log and looking for service-related events involving, in this case, the Print Spooler service. If you’re interested in trying that, search the System event log for events with an EventCode of 7036, which tracks changes in service status.

After we get back our collection of services we use this line of code to grab the service PID and store it in a variable named intProcessID:

intProcessID = objService.ProcessID

Once we’ve done that we can then retrieve a collection of all the processes that have that particular PID (again, there will be only one item in the resulting collection):

Set colProcesses = objWMIService.ExecQuery _
    ("Select * From Win32_Process Where ProcessID = " & intProcessID)

And then we set up a For Each loop to loop through the one-item collection. Inside that loop all we do is echo back the value of the CreationDate property:

Wscript.Echo objProcess.CreationDate

In turn that gives us output that looks something like this:

20070227074100.796875-480

Yuck. That’s what we were talking about when we said that the information reported back by our first script wasn’t very user-friendly. This isn’t really the fault of the script; the problem is that WMI always reports dates and times as UTC (Universal Time Coordinate) values. Is it possible to convert a UTC value to something a little easier to understand? You bet it is:

strComputer = "."

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

Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where Name = 'Spooler'")

For Each objService in colServices
    intProcessID = objService.ProcessID

    Set colProcesses = objWMIService.ExecQuery _
        ("Select * From Win32_Process Where ProcessID = " & intProcessID)

    For Each objProcess in colProcesses
        dtmStartTime = objProcess.CreationDate

        dtmConvertedDate = CDate(Mid(dtmStartTime, 5, 2) & "/" & _
            Mid(dtmStartTime, 7, 2) & "/" & Left(dtmStartTime, 4) _
                & " " & Mid (dtmStartTime, 9, 2) & ":" & _
                    Mid(dtmStartTime, 11, 2) & ":" & Mid(dtmStartTime,13, 2))

        Wscript.Echo DateDiff("n", dtmConvertedDate, Now)
    Next
Next

We’ve done two things with this modified script. First, we’ve used this chunk of code to take the value of the CreationDate property and convert it to a “real” date-time value:

dtmStartTime = objProcess.CreationDate

dtmConvertedDate = CDate(Mid(dtmStartTime, 5, 2) & "/" & _
Mid(dtmStartTime, 7, 2) &
"/" & Left(dtmStartTime, 4) _
& " " & Mid (dtmStartTime, 9, 2) & ":" & _
Mid(dtmStartTime, 11, 2) & ":" &
Mid(dtmStartTime,13, 2))

We won’t discuss this code in any detail today; if you’d like to know more about converting a UTC value to a standard date-time value take a look at this portion of the Microsoft Windows 2000 Scripting Guide. Once we do convert the UTC value to a plain old date-time value we can then use the DateDiff function to report, in this case, the number of minutes that the process has been running:

Wscript.Echo DateDiff("n", dtmConvertedDate, Now)

As you can see, we pass DateDiff three parameters:

· “n” indicating the time interval to report. Here we’re reporting the uptime in minutes; for other time intervals (seconds, hour, days, etc.) see the Scripting Guide.

· dtmConvertedDate, the date-time value we derived from the CreationDate property.

· Now, the current date and time.

This modified script reports the amount of time, in minutes, that the process has been running:

225

Like we said, it’s not the perfect solution, but it’s still a useful script to have in your arsenal. And it does work exceedingly well for services that run in their own process; that’s because stopping a service also terminates the process. And how do you know which services run in their own process? Why, you use this script, of course:

strComputer = "."

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

Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where ServiceType = 'Own process'")

For Each objService in colServices
    Wscript.Echo objService.DisplayName
Next

There you have it, JC. As for the Scripting Guy who writes this column, he’s ready to call it a day. Is he all worn out after the fun and excitement of the 2007 Winter Scripting Games? No, not really; he’s just lazy. But, in his defense, if he wasn’t lazy then he really wouldn’t be a Scripting Guy, would he?


How Can I Tell Which Account a Service is Running Under?

We have services that run under a particular user account. Each time we change the password for that user account, we need to change the password for any services that run under that account as well. But how can we tell which account a service is running under?

-- SA

Hey, SA. By and large WMI is a pretty transparent technology. To manage services, you use a class named Win32_Service. To manage processes, you use a class named Win32_Process. Not exactly rocket science, right?

Of course, every now and then WMI throws you a curve, and the service account name happens to be one of those instances. It’s actually easy to determine the account name under which a service runs, provided you know that this information is found using the StartName property:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service")
For Each objService in colServices
    Wscript.Echo objService.Name, objService.StartName
Next

The preceding code shows you the name and account name for each service installed on a computer. If you only want a list of services running under a specific account (e.g., KenMyer), then use code similar to this:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where StartName = 'KenMyer'")
For Each objService in colServices
    Wscript.Echo objService.Name
Next

And then how do you change the password using a script? Why, by using a script like this one:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where StartName = 'KenMyer'")
For Each objService in colServices
    errReturn = objService.Change( , , , , , , , "fgT54ghde*&")
Next

Note that when using the Change method, the password is the eighth parameter passed. That means you must preface the new password ("fgT54ghde*&") with seven commas. A bit odd, but it works.


How Can I Use Windows PowerShell to Start a Service on a Remote Computer?

Hey, Scripting Guy! How can I use Windows PowerShell to start a service on a remote computer?

-- NA

Hey, NA. You know, a few years ago a professor named Francis Fukuyama wrote a book called The End of History and the Last of Man, a book which stated (well, sort of) that now that the Cold War was over nothing particularly interesting and exciting would ever happen again. What do the Scripting Guys have to say to Dr. Fukuyama? Just this: ha!

Why are the Scripting Guys (of all people!) willing to challenge one of the foremost intellects of our generation? Well, for one thing, we’re willing to bet that Francis Fukuyama doesn’t read this column very often. More important, however, is the fact that we have proof that interesting and exciting things still do happen. After all, not only are we in the midst of the Hey, Scripting Guy! 500th column celebration, but now we’re answering our first-ever Windows PowerShell question. Take that, Francis Fukuyama!

Note. You say you’ve never even heard of Windows PowerShell? You say you have no idea what you can even do with Windows PowerShell? We say you should take a look at the new Script Center series What Can I Do With Windows PowerShell?

As you noted, NA, you’re trying to use the Start-Service cmdlet to restart a service on a remote computer, something like this (for educational purposes, we’ve simplified your script a little):

Get-WmiObject -computer atl-fs-01 Win32_Service -Filter "Name='Alerter'" | Start-Service

In other words, you’re trying to connect to the Alerter service on the computer atl-fs-01, and then “pipe” that object to the Start-Service cmdlet. As you noted, you’re able to connect to the Alerter service on the remote computer without any problem. When you call Start-Service, however, the command fails, even though the Start-Service cmdlet works just fine on your local computer.

And that’s the problem right there. Windows PowerShell is an interesting and powerful new technology, but (at the time we’re writing this column of course) it’s still in beta; among other things, that means that some of the features that are expected to appear in the released version of the product aren’t ready just yet. Unfortunately, one of those features is the ability to run cmdlets against remote computers. Someday you should be able to start a service on a remote computer using a command similar to this:

Start-Service alerter –computer atl-fs-01

But not today.

So then why are you able to get information about the Alerter service on the remote computer? Well, as it turns out, the Get-WMIObject cmdlet is an exception to the rule: because it piggybacks onto WMI, Get-WMIObject can do anything WMI can do. That’s why Get-WMIObject can operate against remote computers: rather than relying on Windows PowerShell cmdlets to make the connection and carry out its tasks Get-WMIObject uses WMI to take care of business.

A general rule of thumb: you can use Get-WMIObject to work with remote computers, providing, of course, that you’re using WMI properties and methods. Otherwise, you’re limited to working against the local computer when using Windows PowerShell. (And, yes, that will change, although we can’t say for sure when remote computer capabilities will be added to Windows PowerShell.)

So does that mean we can use Get-WMIObject and a standard WMI method (like, say, StartService) to start a service on a remote computer? As a matter of fact it does, although even here there’s a bit of a trick involved. Let’s show you the Windows PowerShell script/command that will start a service on a remote computer, and then see if we can explain how it works:

(Get-WmiObject -computer atl-fs-01 Win32_Service -Filter "Name='Alerter'").InvokeMethod("StartService",$null)

There are really two parts to this command, the part that connects to the Alerter service and the part that starts the service. Let’s start with Part 1, the part that connects us to the WMI service:

(Get-WmiObject -computer atl-fs-01 Win32_Service -Filter "Name='Alerter'")

What we’re doing here is connecting to the WMI service on the computer atl-fs-01 and retrieving all instances of the Win32_Service class where the Name property is equal to Alerter. What we have here is equivalent to the following VBScript code:

Set objWMIService = GetObject("winmgmts:\\atl-fs-01")

Set colServiceList = objWMIService.ExecQuery _
    ("Select * From Win32_Service Where Name='Alerter'")

Notice that we don’t call the ExecQuery method in our Windows PowerShell command; instead we use the –Filter parameter, specifying that the only services we want back are those where the Name is equal to Alerter.

Good question: why did we put parentheses around our Get-WMIObject statement? We did that to ensure that Windows PowerShell treats the returned object as an object, that is, as something we can call a method on. And once we have an object we can then follow the closing parenthesis with a dot (.), just like we would using VBScript. In our Windows PowerShell command we do this:

(Get-WmiObject -computer atl-fs-01 Win32_Service -Filter "Name='Alerter'").InvokeMethod("StartService",$null)

Whereas in VBScript we’d do something like this (most often inside a For Each loop):

objItem.StartService

Got that? OK. Now it gets tricky. As you can see, in the second part of our command, we don’t directly call the StartService method. Instead, we do this:

InvokeMethod("StartService",$null)

What’s the deal with that? Well, as it turns out, we do get an object back when we call Get-WMIObject; in this case, however, it’s a .NET Framework object (specifically, a System.Management.ManagementObject#root\cimv2\Win32_Service object). We can’t call the StartService method for one simple reason: this type of object doesn’t have a StartService method. Instead, we call InvokeMethod, passing two parameters:

· “StartService”, the name of the WMI method we want to invoke.

· $Null, which simply indicates a Null value.

InvokeMethod expects two parameters: the method to be called, followed by any additional arguments needed by the method. In this case there are no method arguments; however, InvokeMethod expects two parameters whether those parameters exist or not. To satisfy InvokeMethod we pass a null value as the second parameter.

That’s another good question: how did we know that the object we got back was a System.Management.ManagementObject#root\cimv2\Win32_Service object, and that this object didn’t support the StartService method? Well, we’d like to tell you that this is because we’re all really smart and we know anything; however, we didn’t think anyone would buy that. Therefore, we’ll tell you the truth: we grabbed the object in question and ran it through the Get-Member cmdlet in order to get back a list of its properties and methods. In other words, we did this:

Get-WmiObject Win32_Service -Filter "Name='Alerter'" | Get-Member

From there we simply engaged in a little old-fashioned trial and error until we figured out how to start the service.

A caveat or two. The command we came up with works just fine, but we wouldn’t go so far as to say that it’s the best way to do this, or even that it’s the only way to do this. If anyone knows of a better way to carry out this same task using Windows PowerShell, please let us know; after all, we’re still learning about this new technology ourselves. (Francis Fukuyama, we’re especially interested in hearing from you!)

Likewise, in the interest of keeping this discussion at a relatively superficial level, we skipped over some technical details concerning what Windows PowerShell does, and why. But those aren’t important right now; for now, we just wanted you to give a basic idea of how Windows PowerShell works, as well as why it doesn’t work (at least not yet) in certain situations. Oh: and also show you how to use Windows PowerShell to start a service on a remote computer. That was all we were hoping to accomplish today.

Well, that and prove to the world that interesting and exciting things still happen. And we consider that to be a job well-done.


How Can I Configure the System Failure Options on a Windows Server 2003 Computer?

Hey, Scripting Guy! How can I configure the system failure options on a Windows Server 2003 computer? I’d like to configure the machine to do a small memory dump, and disable both the sending of an administrative alert and the automatic reboot.

-- JS

Hey, JS. You know, the Scripting Guys happen to all be in a good mood today, so we’re going to go the extra mile here: not only are we going to show you how to configure system failure options on a Windows Server 2003 computer, but – for no extra charge – we’ll show you how to perform this task on a Windows XP computer or even on a Windows 2000 computer as well. Let’s see any other daily scripting column beat an offer like that!

OK, if you want to get technical we’re only making that offer because a script that sets system failure options on a Windows Server 2003 computer can – without any changes – do the exact same thing on a Windows XP or Windows 2000 computer as well. But maybe we could all just pretend that the Scripting Guys are doing some extra work out of the goodness of their hearts.

Besides, it’s still more than you get out of any other daily scripting column. Or at least any other daily scripting column found on TechNet.

As far as we know, that is.

But who cares about motivations anyway? After all, our guess is that you just want a script that changes the system failure options:

Const SMALL_MEMORY_DUMP = 3

strComputer = "."

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

Set colRecoveryOptions = objWMIService.ExecQuery _
    ("Select * From Win32_OSRecoveryConfiguration")

For Each objOption in colRecoveryOptions
    objOption.DebugInfoType = SMALL_MEMORY_DUMP
    objOption.AutoReboot = FALSE
    objOption.SendAdminAlert = FALSE
    objOption.Put_
Next

As you can see, even though this script works on Windows Server 2003 and Windows XP and Windows 2000 there really isn’t much to it. We start out by defining a constant named SMALL_MEMORY_DUMP and setting the value to 3; we’ll use this constant to specify how we want to write debugging information in case of a system failure (more popularly known as a “blue screen”). We can configure any of the debugging types shown in the following table:

Value | Meaning
0 | None
1 | Complete memory dump
2 | Kernel memory dump
3 | Small memory dump

After defining out constant we connect to the WMI service on the local computer (although, as is the case with almost all WMI scripts, we could also run this script against a remote computer). We then use this line of code to retrieve all the instances of the Win32_OSRecoveryConfiguration class, which just happens to be the class used for managing system failure options:

Set colRecoveryOptions = objWMIService.ExecQuery _
    ("Select * From Win32_OSRecoveryConfiguration")

We should point out that, because we use the ExecQuery method, we’ll get back a collection and, because we have a collection, we need to set up a For Each loop to walk through all the items in that collection. However, there’s only one set of system failure options per computer, which means the collection will never have more than one item in it.

Speaking of For Each loops, that’s what we do next: we set up a For Each loop to walk through all the system failure options in the collection. (Again, we’ll have only one such set.) Inside that For Each loop we configure values for three different properties: DebugInfoType, AutoReboot, and SendAdminAlert. That’s what we do here:

objOption.DebugInfoType = SMALL_MEMORY_DUMP
objOption.AutoReboot = FALSE
objOption.SendAdminAlert = FALSE

That little block of code should be fairly straightforward. We start off by setting the value of the DebugInfoType property to our constant SMALL_MEMORY_DUMP. Because AutoReboot and SendAdminAlert are both Boolean values, we can disable sending an email and automatically rebooting the computer simply by setting these values to False. After configuring the property values we then use this line of code to write the changes back to the computer:

objOption.Put_

Whatever you do don’t leave out that line. The Put_ method is roughly equivalent to the Save command in Microsoft Word. If you don’t choose the Save command any changes you make in Word aren’t going to be saved. Likewise, if you don’t call the Put_ method any changes you make to WMI property values won’t be saved, either.

Scripting Guys anecdote. Right before the Microsoft Windows 2000 Scripting Guide went to press, an editor we had at the time ran a spell-check on the entire book and changed every instance of Put_ to Put. We never would have caught the mistake had she not pointed out the fact that we needed to be more careful with our spelling, lest the book end up riddled with errors.

As long as we’re on the subject, we should mention that the Scripting Guide actually includes a section on managing system failure options. We were going to keep that a secret but, seeing as how we’re feeling so chipper today, well, what the heck.

So there you have it, JS; aren’t you glad we were in a good mood today? And trust us, that doesn’t happen very often. For at least one of the Scripting Guys his good moods pretty much coincide with the life cycle of the 20-year locust. Fortunately, the other Scripting Guys all respect and admire him anyway, despite his occasional grumpiness.

Or at least he’s pretty sure that they respect and admire him. Hey, why wouldn’t they?


How Can I Enable System Restore Using a Script?

Hey, Scripting Guy! How can I enable Windows XP’s System Restore using a script?

-- RL

Hey, RL. Thanks for the question; the Scripting Guy who writes this column is running way late today, the clock is ticking, and he was desperately in need of a question a he could answer without having to use a lot of code. A question about enabling System Restore is just what the doctor ordered, or at least it would be if the doctor happened to be writing a daily scripting column. To tell you the truth, we don’t really know whether doctors write daily scripting columns or not. We’re guessing that most do, but that’s just a guess.

But here’s one thing we do know, for sure: contrary to popular belief, the international distress code SOS is not short for Save Our Ship. Instead, the letters S-O-S were chosen because they represent simple, all-but-impossible to misinterpret Morse code: three dots, three dashes, three dots. The letters don’t actually stand for anything at all.

Pardon? You’re suggesting that, if we really are way behind schedule (which we are), it might be better for us to talk about scripting rather than talk about some arcane trivia no one else cares about? You know, that’s good advice, and we’ll do it … assuming we ever stumble upon some arcane trivia that no one else cares about. But we hardly think the origin of SOS falls into that category.

On the other hand, now that we’ve pretty much exhausted our knowledge both of the SOS distress call and of Morse code, well, about all we can do is talk about the script. With that in mind, here’s a script that enables System Restore on a computer running Windows XP:

strComputer = "."

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

Set objItem = objWMIService.Get("SystemRestore")
errResults = objItem.Enable("")

You can see why, on a busy day, we chose to answer this particular question: just a few lines of code and we’ve got ourselves a column. The script itself starts out in typical fashion, connecting to the WMI service on the local computer. (Although, like all good WMI scripts, this one will also work against remote computers; just assign the name of that computer to the variable strComputer.) One thing to watch out for, however: unlike most WMI scripts this one doesn’t connect to the root\cimv2 namespace; instead, it connects to the root\default namespace. Why? That’s easy: that’s where all the System Restore WMI classes reside.

After making the connection we use the Get method to bind to the SystemRestore class; that’s what we do here:

Set objItem = objWMIService.Get("SystemRestore")

At that point we can enable System Restore on all drives by simply calling the Enable method and passing an empty string (“”) as the sole parameter.

Anyway, thanks again for the question, RL, we have to – well, yes, like we said, that does enable System Restore on all drives. Could we enable System Restore on a particular drive? Well, like we said we’re running kind of late here – oh, what the heck. Here’s a script that enables System Restore only for drive D on the local computer:

strComputer = "."

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

Set objItem = objWMIService.Get("SystemRestore")
errResults = objItem.Enable("D:\")

Yes, it’s remarkably similar to the first script we showed you; in fact, the only difference lies with the parameter passed to the Enable method. To enable System Restore on all drives we pass an empty string to this method; to enable System Restore only on drive D we pass the Enable method this parameter: D:\. In other words, we pass the drive letter (D) followed by :\. To enable System Restore on drive E and drive E only we’d use this line of code:

errResults = objItem.Enable("E:\")

Etc.

We really have to go now, so – what’s that? What if you later want to disable System Restore on a computer? You know, we really don’t have time to …. Look, we’ll make you a deal: one more script and then we have to go. OK? OK. Here’s a script that disables System Restore for all the drives on a computer:

strComputer = "."

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

Set objItem = objWMIService.Get("SystemRestore")
errResults = objItem.Disable("")

Again, very similar to the first script we showed you; in fact, the only difference is that this time we call the Disable method instead of the Enable method. And, yes, as a matter of fact you can disable System Restore for a specific drive. Tired of running System Restore on drive D? Then don’t:

strComputer = "."

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

Set objItem = objWMIService.Get("SystemRestore")
errResults = objItem.Disable("D:\")

That’s all we have time for; we absolutely have to go. Although we probably should add that, prior to SOS, the international distress call was CQD. The letters CQ represented a general message that was sent to all stations, with the letter D indicating “distress.” In other words, CQD meant “Attention all stations, we have a distress call.” In fact, the ill-fated Titanic originally sent out several CQD calls before trying SOS. As it turns out – wait a second, where are you going? What do you mean you’re too busy to stay and listen to our story? Come on, surely you can make time for – this will only take a minute – are you sure you can’t stay just – oh. Well, OK. We can always save this story for a future column.


How Can I Verify the System Time on a Remote Computer?

Hey, Scripting Guy! How can I verify the system time on a remote computer?

-- MN

Hey, MN. You know, recently the Scripting Guy who writes this column was reading about the “law of attraction,” which is supposedly a natural law of the universe that, if you understand how to use it, can give you anything your heart desires. To quote from a recent book on the subject:

"The law of attraction is a law of nature. It is impersonal and it does not see good things or bad things. It is receiving your thoughts and reflecting back to you those thoughts as your life experience. The law of attraction simply gives you whatever it is you are thinking about."

Note. Yes, there’s always a catch: the law of attraction gives you whatever it is you are thinking about. No wonder the Scripting Guys never get what they want!

Now, to be honest, the Scripting Guy who writes this column tends to be a bit skeptical. The earth is round? Well, maybe. By using a microwave you can cook an entire burrito in less than a minute? That seems too good to be true. The law of attraction simply gives you whatever it is you are thinking about? Hmmm ….

Interestingly enough, however, it was shortly after reading about the law of attraction that the Scripting Guy who writes this column actually started thinking: “I don’t know how to verify the system time on a remote computer,” he thought. “And I don’t want to waste my afternoon trying to figure out how I can do it. I’m thinking it would be nice if the universe would just give me a script that can verify the system time on a remote computer.”

Well, lo and behold:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_LocalTime")

For Each objItem in colItems
    strTime = objItem.Hour & ":" & objItem.Minute & ":" & objItem.Second
    dtmTime = CDate(strTime)
    Wscript.Echo FormatDateTime(dtmTime, vbFormatLongTime)
Next

Kind of spooky, isn’t it?

Best of all, this isn’t a bad little script; in fact, it’s even a bit fancier than anything the Scripting Guys were likely to come up with. Not that it’s perfect, mind you; after all, it only runs on Windows XP and subsequent versions of Windows. (Apparently the universe doesn’t realize that some people are still using Windows 2000.) But that’s OK; before we call it a day we’ll show you an alternate method for getting the system time that will work on Windows 2000. That’s the least we could do, seeing as how it was probably our muddled thinking that led the universe to ignore Windows 2000 users in the first place.

Speaking of the universe, its script starts off by connecting to the WMI service on the local computer. But wait, you ask: didn’t we want to get the system time on a remote computer? Don’t worry; the universe has you covered. To get the time on a remote computer simply assign the name of that machine to the variable strComputer, like so:

strComputer = "atl-fs-01"

After connecting to the WMI service we then use the following query to return all the instances of the Win32_LocalTime class:

Set colItems = objWMIService.ExecQuery("Select * From Win32_LocalTime")

Incidentally, this explains why the universe’s script won’t work on Windows 2000: the Win32_LocalTime class wasn’t introduced until Windows XP.

As you might expect, Win32_LocalTime is a “singleton” class; that means it returns only one item. (In general, a computer can only have one system time. We say “in general” because Scripting Guy Peter Costantini insists that on his home planet, located somewhere in the 35th dimension, computers can have more than one system time. Needless to say, we’re a bit skeptical about that, too.) However, even though we only have one item the ExecQuery method always returns information in the form of a collection; in turn, that means we still have to set up a For Each loop to loop through all the items in the collection:

For Each objItem in colItems

And what is the universe going to have us do inside this loop? Well, for starters, we’re going to grab the values of the Hour, Minute, and Second properties and string them together, storing the result in a variable named strTime:

strTime = objItem.Hour & ":" & objItem.Minute & ":" & objItem.Second

To tell you the truth, the Scripting Guys would have called it good at this point and simply echoed back the value of strTime. However, the Win32_LocalTime class uses a 24-hour clock; that means that 2:00 PM is actually rendered as 14:00. If you don’t mind the 24-hour format then you’re done. The universe, however, decided to add a couple extra lines of code in order to format this as a standard time.

What do we mean by that? Well, suppose the local time on the computer is 2:33:16 PM; that means strTime will be equal to this

14:33:16

In order to convert this to a standard time format the universe first uses the CDate function to convert the character value 14:33:16 to a date-time value:

dtmTime = CDate(strTime)

After that the universe then uses the FormatDateTime function to echo the value of dtmTime using the time settings specified in your computer’s Regional and Language Settings. (That’s what the VBScript constant vbLongTme does.) On the Scripting Guys’ test computer, that’s going to look like this:

2:33:16 PM

That’s a little nicer, especially if you aren’t comfortable with the 24-hour clock.

Well, it’s nicer as long as you’re not using Windows 2000; if you are, the preceding script won’t help you much. But don’t despair: Windows 2000 types can always use the Win32_OperatingSystem class and the LocalDateTime property to determine the system time. The LocalDateTime property stores time using the infamous UTC (Universal Time Coordinate) format, but we can do some fancy footwork and convert these UTC values to something a bit more user-friendly:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_OperatingSystem")

For Each objItem in colItems
    strTime = objItem.LocalDateTime
    dtmTime = CDate(Mid (strTime, 9, 2) & ":" & Mid(strTime, 11, 2) & ":" & _
        Mid(strTime, 13, 2))
    dtmTime = CDate(dtmTime)
    Wscript.Echo FormatDateTime(dtmTime, vbFormatLongTime)
Next

We won’t discuss this script in any detail today, but you can learn more about the UTC time format by thumbing through the Microsoft Windows 2000 Scripting Guide.

As for the Scripting Guys, we’re assuming this is our last column; that’s because we’re thinking we’d each like to have 10 million dollars so that we can all retire. If this law of attraction thing really works, well, tomorrow we’ll all be lying on the beach drinking Mai Tais. It’s been fun, but all good things must come to an end

Well, unless some of you are thinking, “Gee, I hope there’s a Hey, Scripting Guy! column tomorrow.” How does the law of attraction deal with a situation where one person thinks X and another person thinks Y? We’ll have to think about that one.


How Can I Determine Whether Terminal Services is Enabled on a Windows Server 2003 Computer?

Hey, Scripting Guy! How can I determine whether Terminal Services is enabled on a Windows Server 2003 computer?

-- DD

Hey, DD. You know, it turns out to be true: good things will come to those who wait. According to the front page of The Seattle Times, people who are grouchy and cantankerous today are more likely to be considered wiser and more intelligent when they get older. If that’s true, and if we project his current level of grouchiness forward, in another 30 years or the Scripting Guy who writes this column should be working on his third or fourth Nobel Prize. Vindication at last!

What that also means, DD, is this: if you’re willing to wait another 30 years or so we could give you a very wise, very intelligent answer to your question. If you aren’t willing to wait, then you’ll have to settle for this:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_TerminalServiceSetting")

For Each objItem in colItems
    If objItem.AllowTSConnections = 0 Then
        strStatus = "Disabled"
    Else
        strStatus = "Enabled"
    End If
    Wscript.Echo "Terminal Services status: " & strStatus
Next

As you can see, there really isn’t much to this script. We start out, as we usually do, by connecting to the WMI service on the local computer. Fortunately, this script can be easily modified to connect to the WMI service on a remote computer; after all, you probably don’t need to go to all the trouble of writing a script in order to determine whether Terminal Services is enabled on your local machine. To connect to a remote computer using this script – or pretty much any other WMI script you’ll find in the Script Center – just assign the name of that remote machine to the variable strComputer. For example, this line of code results in the script connecting to the WMI service on the computer atl-ts-01:

strComputer = "atl-ts-01"

Needless to say, the ability to do remotely pretty much everything you can do locally is one of the cooler features of WMI.

After making the connection we use the following query to retrieve all the instances of the Win32_TerminalServiceSetting class:

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_TerminalServiceSetting")

Note. This is probably a good time to mention that the Win32_TerminalServiceSetting class is only available on computers running Windows Server 2003 or Windows XP. Among other things, that means this script can’t be used to determine if Terminal Services is enabled on a Windows 2000 computer. That shouldn’t be a problem for you, DD; you specifically asked about Terminal Services on Windows Server 2003. But it’s something for other people to keep in mind.

After we run the query we’ll get back a collection of all the Terminal Services settings for the computer in question. Although there will be only one item in the collection (that is, one group of Terminal Services settings), this is a collection; that means we still need to set up a For Each loop in order to get at the individual items and property values. In particular, we want to examine the value of the AllowTSConnections property, something we do here:

If objItem.AllowTSConnections = 0 Then

If AllowTSConnections is equal to 0 that means Terminal Services is disabled; if AllowTSConnections is equal to 1 that means Terminal Services in enabled. All we do in the remainder of the script is check the value of AllowTSConnections and then, based on that value, assign the string Disabled or Enabled to a variable named strStatus. We then use this line of code to echo back the Terminal Services status:

Wscript.Echo "Terminal Services status: " & strStatus

That’s all there is to it.

Wait, wait: don’t say it. You were just about to ask if it’s possible to use a script to enable or disable Terminal Services, weren’t you? (Hmmm, maybe there is something to this correlation between grouchiness and intelligence.) You bet you can: all you have to do is call the SetAllowTSConnections method, passing the method one of two possible parameters:

· 0, to disable Terminal Services.

· 1, to enable Terminal Services.

For example, this script enables Terminal Services on the local computer:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_TerminalServiceSetting")

For Each objItem in colItems
    objItem.SetAllowTSConnections(1)
Next

You know, you’re right: that was pretty nice of us to give you a bonus script, wasn’t it? Especially for as grouchy and cantankerous an old coot as the Scripting Guy who writes this column is. But we’re going to let you in on a little secret: the Scripting Guy who writes this column is nowhere near as grouchy and hard-to-deal-with as his reputation would suggest. Instead, like many great figures in history, he’s just a little misunderstood.

Yes, just like the Roman emperor Caligula; Attila the Hun; Jack the Ripper; and, of course, Rasputin, the Mad Monk.


How Can I Determine if a Computer is Running Windows Server 2003 and if Service Pack 1 is Installed?

Hey, Scripting Guy! How can I determine whether or not a computer is running Windows Server 2003 and, if so, whether Service Pack 1 is installed?

-- TW

Hey, TW. You know, we Scripting Guys always get a kick out of questions like this. Not because there’s anything wrong with the question, mind you. It’s just that many of the people we work with would have no idea what you’re talking about here. After all, why would you need a script like this one: of course you have Windows Server 2003 installed, and of course you’ve got Service Pack 1 on there as well. I mean, even your laptops are running Windows Server 2003, Service Pack 1. Aren’t they?

We Scripting Guys like to think we’re a little more in touch with reality than some of our fellow Microsoft employees. (Now that’s something you don’t hear every day!) We understand that people – for many valid reasons – don’t always upgrade the very second that Microsoft releases a new product or a new version of a product. That’s why, whenever possible, we try to provide a Windows 2000 workaround in those columns where we show people a Windows XP or Windows Server 2003 solution. And that’s why we’re more than happy to show you a script that can tell you whether a computer is running Windows Server 2003 and, if so, whether it has Service Pack 1 installed:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

For Each objItem in colItems
    If InStr(objItem.Caption, "Server 2003") Then
        strText = objItem.Caption
        If objItem.ServicePackMajorVersion = "1" Then
            strText = strText & ", Service Pack 1"
        End If
        Wscript.Echo strText
    Else
        Wscript.Echo "This computer is not running Windows Server 2003."
    End If
Next

Pretty exciting, huh? The script starts out by connecting to the WMI service on the local computer; for those of you with multiple machines in multiple locations, this script can also be used to determine the operating system installed on a remote computer. We then use this line of code to retrieve a collection of all the operating systems installed on the computer:

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

Note. OK, we admit it: this query doesn’t return a collection of all the operating systems installed on a computer; it only returns information about the operating system currently in use. That’s true even if you have a multi-boot computer: you’ll only be told about the current operating system, and won’t even be aware that other OSes are installed on the machine.

That’s just the way it goes.

Even though there will be only one operating system in our collection we still need to set up a For Each loop to walk through that collection. Logically enough, then, we set up a For Each loop and then run this line of code:

If InStr(objItem.Caption, "Server 2003") Then

What we’re doing here is using the InStr function to determine whether or not the string Server 2003 can be found anywhere in the Caption property. We do this because Windows Server 2003 comes in various flavors: Standard, Advanced, Enterprise, Windows Server 2003 R2, Caramel Nut Fudge. The one thing all these versions have in common (well, except for Caramel Nut Fudge, which might not actually be a version of the operating system) is that the string Server 2003 appears somewhere in the caption. So that’s what we check for: if the Caption has Server 2003 in it then we assume it must be some flavor of Windows Server 2003. (And when we echo back the operating system information we’ll specify which flavor.)

Note. In case you’re wondering, no, we can’t just use the BuildNumber property, because the build numbers can also vary depending on the operating system version.

So what happens if we find the string Server 2003 in the Caption? That’s easy: we then store the entire Caption in a variable named strText. And because we did find the target string, that means that the computer in question must be running Windows Server 2003. Therefore, our next step is to see if the value of the ServicePackMajorVersion property is equal to 1; if it is, that means that Service Pack 1 is installed. That’s what we do here:

If objItem.ServicePackMajorVersion = "1" Then

If this is true (that is, if Service Pack 1 is installed), then we tack a note to that effect to end of the variable strText. That means that a computer that has Service Pack 1 installed will echo back information similar to this:

Microsoft(R) Windows(R) Server 2003, Standard Edition, Service Pack 1

If the computer is running Windows Server 2003 but doesn’t have Service Pack 1 installed then it will echo back something like this:

Microsoft(R) Windows(R) Server 2003, Standard Edition

See how that works? That’s how we distinguish between a computer with Service Pack 1 and a computer without Service Pack 1.

But what if the computer isn’t running Windows Server 2003 at all? Well, in that case our script will skip down to the Else statement and echo this message:

This computer is not running Windows Server 2003.

Sure, we know: nobody would ever have a computer that wasn’t running Windows Server 2003. But, just in case, we added this little feature to the script anyway. After all, if someone was abducted by aliens 6 years ago and were just recently returned, they might only now be getting around to installing Windows Server 2003 on all their machines. You never know.


How Can I Determine the Grace Period for a Computer Running Windows Vista?

Hey, Scripting Guy! How can I determine the remaining operating system grace period for a computer running Windows Vista?

-- LD

Hey, LD. You know, a number of years ago the Scripting Guy who writes this column came outside after work only to discover that his car wouldn’t start. At that time he was working as a sports writer, which occasionally meant working very late at night. This, of course, happened to be one of those occasions: it was 2 o’clock in the morning, no one else was around, and his car wouldn’t start.

To make matters worse, the Scripting Guy who writes this column knows even less about cars than he does about scripting. Not knowing what else to do he opened the hood and took a peek inside. Everything looked fine. although, to tell you the truth, unless something was on fire he wouldn’t know the difference anyway. And even then he might have assumed, “Well, that thing is probably supposed to be on fire.”

Feeling the need to do something the Scripting Guy jiggled a few wires, closed the hood, and tried starting the car again. Much to his surprise, the car started right up. Not only did he make it home safe and sound that evening, but he never had problems starting the car again. Problem solved!

Now, unlike most of the stories that appear in Hey, Scripting Guy! there’s actually a point to this one: sometimes you can fix something even if you don’t have the slightest idea what you’re doing. That’s as true with scripting as it is with cars. After looking under the hood and jiggling a few wires we were able to come up with a way to determine the remaining grace period for Windows product activation; however, we can’t say for sure how we came up with that solution, nor can we tell what it all means.

The problem we ran into is that we don’t fully understand Windows Vista licensing. (And yes, we know: we probably are the only people in the world who don’t fully understand Windows Vista licensing.) Among other things, Windows Vista has a new feature known as Volume Licensing, feature designed for large enterprises like, well, Microsoft. When we check for licensing and grace period information on our test computer we get back a report that looks like this:

Windows Operating System - Vista, RETAIL channel
Grace period remaining (hours): 0
Licensed

Windows Operating System - Vista, OEM_COA_NSLP channel
Grace period remaining (hours): 0
Unlicensed

Windows Operating System - Vista, OEM_SLP channel
Grace period remaining (hours): 0
Unlicensed

Windows Operating System - Vista, RETAIL channel
Grace period remaining (hours): 0
Licensed

Windows Operating System - Vista, OEM_COA_SLP channel
Grace period remaining (hours): 0
Unlicensed

What does all that mean? Well, to tell you the truth, we aren’t sure. In addition, we aren’t totally sure what the returned data will look like on a Vista machine that doesn’t use Volume Licensing. But hey, half of the fun of scripting is simply closing your eyes, running the script, and waiting to see what happens.

Note. Shouldn’t the Scripting Guys make an effort to learn about Volume Licensing, especially in light of the fact that Vista includes a huge new WMI class devoted to software licensing? Yes, we should, and we will. Until that happens, however, you might want to take a few minutes to read up on Volume Licensing and the SoftwareLicensingProduct class on your own.

At any rate, and with no further ado (mainly because we don’t have any further ado) here’s a script for determining the remaining grace period on a Windows Vista computer:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From SoftwareLicensingProduct")

For Each objItem in colItems
    Wscript.Echo objItem.Description

    intGracePeriod = Int(objItem.GracePeriodRemaining / 60)
    Wscript.Echo "Grace period remaining (hours): " & intGracePeriod

    Select Case objItem.LicenseStatus
        Case 0 Wscript.Echo "Unlicensed"
        Case 1 Wscript.Echo "Licensed"
        Case 2 Wscript.Echo "Out-Of-Box Grace Period"
        Case 3 Wscript.Echo "Out-Of-Tolerance Grace Period"
        Case 4 Wscript.Echo "Non-Genuine Grace Period"
    End Select

    Wscript.Echo
Next

OK, let’s see if we can at least kind of figure out what’s going on here. As you can see, we start out by connecting to the WMI service on the local computer (although this script works equally well against remote computers). We then use this line of code to retrieve all the instances of the SoftwareLicensingProduct class 9which, in case you’re wondering, replaces the Win32_WindowsProductActivation class found in Windows XP and Windows Server 2003:

Set colItems = objWMIService.ExecQuery _
    ("Select * From SoftwareLicensingProduct")

OK, so maybe that part was pretty easy, wasn’t it? After issuing the query we then set up a For Each loop to walk through all the items in the returned collection. Inside that loop, the very first thing we do is echo back the value of the Description property:

Wscript.Echo "Description: " & objItem.Description

Still pretty easy, isn’t it? OK, let’s take a look at the GracePeriodRemaining property. As it turns out, this property tells you the remaining time (in minutes) before the grace period expires. (Or, on Volume Licensing clients, the remaining time, again in minutes, before re-activation is required.) Just for the heck of it, we grab the value of this property and then divide it by 60; that allows us to express the remaining grace period in hours:

intGracePeriod = Int(objItem.GracePeriodRemaining / 60)
Wscript.Echo "Grace period remaining (hours): " & intGracePeriod

Would you rather have this value expressed in days? No problem; just divide by 1440 instead (60 minutes in an hour times 24 hours in a day yields 1440 minutes in a day):

intGracePeriod = Int(objItem.GracePeriodRemaining / 1440)
Wscript.Echo "Grace period remaining (hours): " & intGracePeriod

That’s actually all LD asked for, but we decided to go one step further and report back the license status as well. To do that we simply use a Select Case statement that takes action based on the value of the LicenseStatus property:

Select Case objItem.LicenseStatus
    Case 0 Wscript.Echo "Unlicensed"
    Case 1 Wscript.Echo "Licensed"
    Case 2 Wscript.Echo "Out-of-Box Grace Period"
    Case 3 Wscript.Echo "Out-of-Tolerance Grace Period"
    Case 4 Wscript.Echo "Non-Genuine Grace Period"
End Select

Again, the code itself isn’t very complicated. LicenseStatus returns a value of 0, 1, 2, 3, or 4. All we do is take a look at this value and then echo back the actual license status. For example, if LicenseStatus equals 2 that means that the operating system is in the Out-of-Box Grace period. Fittingly enough, we then echo back that very fact:

Case 2 Wscript.Echo "Out-of-Box Grace Period"

So what exactly does it mean to be in the Out-of-Box Grace period, and what are the implications of that? Well, that’s the problem: we don’t really know. But with any luck you can find the answers here.

We hope that this points you in the right direction, LD. As soon as we get home tonight we’ll start digging a little deeper into Vista licensing.

Well, assuming we get the car started and can actually get home, that is. Hmmm, wonder if anything bad would happen if we jiggled this wi--

Determining the Grace Period Using Windows PowerShell

Note. We can’t promise to do this every day, but whenever possible we’ll try to provide a Windows PowerShell solution along with the VBScript solutions typically used in the Hey, Scripting Guy! column. Here’s a sample solution from PowerShell pro June Blender.

I knew that I this task could be done easily in Windows PowerShell, because PowerShell can access any WMI class. (There are a few caveats, but I don't think this one qualifies.) And the Scripting Guy did all of the heavy lifting: he determined which WMI class to use and found interesting properties to display.

Here's a simple Windows PowerShell command that displays the grace period for Windows Vista license:

get-wmiobject -class SoftwareLicensingProduct | format-List -property Name, Description, 'GracePeriodRemaining, LicenseStatus

Here's a more complex version that calculates the grace period in days, instead of minutes, and translates the LicenseStatus property integer values to their meanings:

$L = get-wmiobject -class SoftwareLicensingProduct
$L | format-list -property Name, Description, `
             @{Label="Grace period (days)"; Expression={ $_.graceperiodremaining / 1440}}, `
             @{Label= "License Status"; Expression={switch (foreach {$_.LicenseStatus}) `
              { 0 {"Unlicensed"} `
                1 {"Licensed"} `
                2 {"Out-Of-Box Grace Period"} `
                3 {"Out-Of-Tolerance Grace Period"} `
                4 {"Non-Genuine Grace Period"} `
              } } }

Here's the output. It's not exactly like the VBScript output, but it will do:

Name                : Windows(TM) Vista, Enterprise edition
Description         : Windows Operating System - Vista, VOLUME_KMS channel
Grace period (days) : 0
License Status      : Unlicensed

Name                : Windows(TM) Vista, Enterprise edition
Description         : Windows Operating System - Vista, VOLUME_MAK channel
Grace period (days) : 0
License Status      : Unlicensed

Name                : Windows(TM) Vista, OCUR add-on for Ultimate,HomePremium
Description         : Windows Operating System - Vista, RETAIL channel
Grace period (days) : 0
License Status      : Unlicensed

Name                : Windows(TM) Vista, Enterprise edition
Description         : Windows Operating System - Vista, RETAIL channel
Grace period (days) : 0
License Status      : Unlicensed

Name                : Windows(TM) Vista, Enterprise edition
Description         : Windows Operating System - Vista, VOLUME_KMSCLIENT channe
                      l
Grace period (days) : 173.5
License Status      : Licensed

Name                : Windows(TM) Vista, Enterprise edition
Description         : Windows Operating System - Vista, OEM_SLP channel
Grace period (days) : 0
License Status      : Unlicensed

Windows PowerShell makes this otherwise complex task rather easy. The Get-WmiObject cmdlet has a Class parameter (-class) that lets you specify any WMI class. The Scripting Guy told us to use the SoftwareLicensingProduct class, and we always do what he says. I saved the output in a variable named $L (for "license"):

$L = get-wmiobject -class SoftwareLicensingProduct

At this point we have all of the data that we need in the $L variable; all that's left to do is to format the output. I chose to format the data as a list, because the long values would be truncated in a table.

The Format-List cmdlet has a Property parameter (-property) that lets you select the properties that are displayed in the list. And I copied the properties that the Scripting Guy used in his script:

format-List -property Name, Description, GracePeriodRemaining, LicenseStatus

This command is designed to find the grace period on my local computer, but I could easily add the ComputerName parameter of Get-WmiObject to run the same command on a remote computer, such as one named Vista2.

get-wmiobject -class SoftwareLicensingProduct -computername Vista2 | format-List Name, Description, GracePeriodRemaining, LicenseStatus

I'm done with the task, but I'm still curious about this new SoftwareLicensingProduct object. Perhaps, just perhaps, the Scripting Guy didn't show all of the cool properties of the class. Or, maybe the objects in this class have methods that I might want to use later.

Because I have objects, not just strings, I don't need to retrieve the data again; I just need to re-format the display. First, I'll look at the properties that we didn't use in this display.

I can easily view all of the properties and methods of the objects in $L by using the Get-Member cmdlet. In this command, I use a pipeline operator (|) to send the objects in the $L variable to Get-Member:

$L | get-member

And here's the result: the properties and methods of a SoftwareLicensingProduct object.

TypeName: System.Management.ManagementObject#root\cimv2\SoftwareLicensingPro
duct

Name                         MemberType   Definition
----                         ----------   ----------
Activate                     Method       System.Management.ManagementBaseOb...
DepositOfflineConfirmationId Method       System.Management.ManagementBaseOb...
UninstallProductKey          Method       System.Management.ManagementBaseOb...
ApplicationID                Property     System.String ApplicationID {get;s...
Description                  Property     System.String Description {get;set;}
EvaluationEndDate            Property     System.String EvaluationEndDate {g...
GracePeriodRemaining         Property     System.UInt32 GracePeriodRemaining...
ID                           Property     System.String ID {get;set;}
LicenseStatus                Property     System.UInt32 LicenseStatus {get;s...
MachineURL                   Property     System.String MachineURL {get;set;}
Name                         Property     System.String Name {get;set;}
OfflineInstallationId        Property     System.String OfflineInstallationI...
PartialProductKey            Property     System.String PartialProductKey {g...
ProcessorURL                 Property     System.String ProcessorURL {get;set;}
ProductKeyID                 Property     System.String ProductKeyID {get;set;}
ProductKeyURL                Property     System.String ProductKeyURL {get;s...
UseLicenseURL                Property     System.String UseLicenseURL {get;s...
__CLASS                      Property     System.String __CLASS {get;set;}
__DERIVATION                 Property     System.String[] __DERIVATION {get;...
__DYNASTY                    Property     System.String __DYNASTY {get;set;}
__GENUS                      Property     System.Int32 __GENUS {get;set;}
__NAMESPACE                  Property     System.String __NAMESPACE {get;set;}
__PATH                       Property     System.String __PATH {get;set;}
__PROPERTY_COUNT             Property     System.Int32 __PROPERTY_COUNT {get...
__RELPATH                    Property     System.String __RELPATH {get;set;}
__SERVER                     Property     System.String __SERVER {get;set;}
__SUPERCLASS                 Property     System.String __SUPERCLASS {get;set;}
ConvertFromDateTime          ScriptMethod System.Object ConvertFromDateTime();
ConvertToDateTime            ScriptMethod System.Object ConvertToDateTime();
Delete                       ScriptMethod System.Object Delete();
GetType                      ScriptMethod System.Object GetType();
Put                          ScriptMethod System.Object Put();

Hmm, Activate and UninstallProductKey methods. An OfflineInstallationID property. Okay, these are interesting, but I need to see an example.

Because the Get-WmiObject cmdlet retrieved more than one object, I actually have a group or array of objects. I can use the . (that's a dot) property operator and the Count property of an array to find out how many objects I have.

$L.count
6

And I discover that I have six of them; a bit too much for one display.

I'll select the most interesting one -- the one with the license -- and display the values of its properties. This command uses the Where-Object cmdlet (alias = "where") to select one of the objects, the one with the word "client" somewhere in the description text. Again, I use the Format-List cmdlet and use its Property parameter with a value of all (*).

$L | where {$_.description -like "*client*"} | format-list -property *

__GENUS               : 2
__CLASS               : SoftwareLicensingProduct
__SUPERCLASS          :
__DYNASTY             : SoftwareLicensingProduct
__RELPATH             : SoftwareLicensingProduct.ID="cfd8ff08-c0d7-452b-9f60-ef
                        5c70c32094"
__PROPERTY_COUNT      : 14
__DERIVATION          : {}
__SERVER              : JUNEB-TESTV
__NAMESPACE           : root\cimv2
__PATH                : \\JUNEB-TESTV\root\cimv2:SoftwareLicensingProduct.ID="c
                        fd8ff08-c0d7-452b-9f60-ef5c70c32094"
ApplicationID         : 55c92734-d682-4d71-983e-d6ec3f16059f
Description           : Windows Operating System - Vista, VOLUME_KMSCLIENT chan
                        nel
EvaluationEndDate     : 16010101000000.000000-000
GracePeriodRemaining  : 249840
ID                    : cfd8ff08-c0d7-452b-9f60-ef5c70c32094
LicenseStatus         : 1
MachineURL            : http://go.microsoft.com/fwlink/?LinkId=57203
Name                  : Windows(TM) Vista, Enterprise edition
OfflineInstallationId : 013421174935129263064954303550341742525985120465630192
PartialProductKey     : 4BWMV
ProcessorURL          : http://go.microsoft.com/fwlink/?LinkId=57201
ProductKeyID          : 89579-00142-236-020020-03-1033-6000.0000-0292007
ProductKeyURL         : http://go.microsoft.com/fwlink/?LinkId=57204
UseLicenseURL         : http://go.microsoft.com/fwlink/?LinkId=572

Now, I can display some of these other properties, just by changing the Format-List command. This time, I'll pipe the objects in the $L variable to a Format-List command with a different set of properties:

$L | format-List Name, Description, GracePeriodRemaining, LicenseStatus, __SERVER, ProductKeyURL

Here's the result:

Name                 : Windows(TM) Vista, Enterprise edition
Description          : Windows Operating System - Vista, VOLUME_KMS channel
GracePeriodRemaining : 0
LicenseStatus        : 0
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Name                 : Windows(TM) Vista, Enterprise edition
Description          : Windows Operating System - Vista, VOLUME_MAK channel
GracePeriodRemaining : 0
LicenseStatus        : 0
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Name                 : Windows(TM) Vista, OCUR add-on for Ultimate,HomePremium
Description          : Windows Operating System - Vista, RETAIL channel
GracePeriodRemaining : 0
LicenseStatus        : 0
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Name                 : Windows(TM) Vista, Enterprise edition
Description          : Windows Operating System - Vista, RETAIL channel
GracePeriodRemaining : 0
LicenseStatus        : 0
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Name                 : Windows(TM) Vista, Enterprise edition
Description          : Windows Operating System - Vista, VOLUME_KMSCLIENT chann
                       el
GracePeriodRemaining : 258900
LicenseStatus        : 1
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Name                 : Windows(TM) Vista, Enterprise edition
Description          : Windows Operating System - Vista, OEM_SLP channel
GracePeriodRemaining : 0
LicenseStatus        : 0
__SERVER             : JUNEB-TESTV
ProductKeyURL        : http://go.microsoft.com/fwlink/?LinkId=57204

Okay, it's not much more useful in this case. Once again, we'll have to concede to the Scripting Guy.

One last issue is how I managed the fancy formatting in the second version of the command:

$L = get-wmiobject -class SoftwareLicensingProduct
$L | format-list Name, Description, `
             @{Label="Grace period (days)"; Expression={ $_.graceperiodremaining / 1440}}, `
             @{Label= "License Status"; Expression={switch (foreach {$_.LicenseStatus}) `
              { 0 {"Unlicensed"} `
                1 {"Licensed"} `
                2 {"Out-Of-Box Grace Period"} `
                3 {"Out-Of-Tolerance Grace Period"} `
                4 {"Non-Genuine Grace Period"} `
              } } }

To manipulate a property value before displaying it, I used a calculated property. This takes the form of a "hash table," (formally called an associative arrayto avoid any hint of impropriety). Each hash table can have Label, Expression, Format, and Alignment keys. You can use them with Select-Object and the formatting cmdlets (Format-*). For examples, see "get-help format-table -examples".

Here's the format:

@{<key>=<value>; [<key>=<value>…]}

such as:

@{Label="Name"; Expression={$_.Property * Int}}

(The tricky part is remembering the correct number of parentheses and curly braces…)

In this case, to convert the value of the GracePeriodRemaining property from minutes to days, I divided the value by 1440, the number of minutes in a day. The expression starts with a $_ , the symbol that represents the current object. You need it whenever you are displaying more than one object (in this case, 6 of them).

@{Label=" Grace period (days)"; Expression={$_.graceperiodremaining / 1440}}

To change the integer values of the LicenseStatus properties to meaningful names, I typed a switch statement in the value of the Expression key. Windows PowerShell uses a pretty standard form of the switch statement. For a detailed description of switch statements in PowerShell, at the Windows PowerShell command prompt, type "get-help about_switch".

These simple commands can be very powerful. Again, the trick was finding the SoftwareLicensingProduct class of WMI. In this case, like many others, I relied on the venerable Scripting Guy, but if he was off at a baseball game -- I mean, occupied in an important meeting -- you can use the List parameter of Get-WmiObject to list all of the available WMI classes.

get-wmiobject -list

I'll continue to rely on the Scripting Guy.


How Can I Determine Which Version of Windows Vista is Installed on a Computer?

Hey, Scripting Guy! How can I determine which version of Windows Vista is installed on a computer?

-- GC

Hey, GC. You know, Microsoft is one of the largest and most successful companies the world has ever known. And yet, despite all our successes, we’re really no different than any anyone else: for example, from time-to-time we get a little jealous of other companies and what they have achieved. But that’s understandable after all, for years we Microsoft employees have had to endure the indignity of driving past Baskin-Robbins ice cream stores and seeing the sign advertising their 31 different flavors of ice cream. Thirty-one different flavors! How in the world can we compete with that?

But, then again, we are Microsoft. You say that Baskin-Robbins has 31 different flavors of ice cream? Good for them; just wait and see how many different flavors of Windows Vista we can release. Let’s see, there’s Windows Vista Home Basic and Windows Vista Home Premium. There’s Windows Vista Enterprise edition, Windows Vista Ultimate, Windows Vista White Chocolate and Macadamia Nuts. There’s – well, you get the idea. Thanks to Windows Vista, all us Microsoft employees can now drive past Baskin-Robbins with our heads held high.

Note. OK, technically some of us don’t actually drive past a Baskin-Robbins; instead, we pull in and order two or three scoops of Chocolate Fudge ice cream. But you know what we mean.

As a general rules, more choices are better than fewer choices, on the other hand, it’s also true that having 31 flavors of Windows Vista can complicate life for system administrators. This is especially true in light of the fact that these different flavors of Vista have different capabilities. (For example, do you want BitLocker drive encryption? Then you need to install Windows Vista Ultimate.) But how in the world can you determine whether or not a computer is even running Windows Vista, let alone which flavor of Vista that computer is running?

Funny you should ask:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * From Win32_OperatingSystem")

For Each objItem in colItems
    If Instr(objItem.Caption, "Vista") Then
        Select Case objItem.OperatingSystemSKU
             Case 0 strVersion = "Undefined."
             Case 1 strVersion = "Ultimate Edition."
             Case 2 strVersion = "Home Basic Edition."
             Case 3 strVersion = "Home Basic Premium Edition."
             Case 4 strVersion = "Enterprise Edition."
             Case 5 strVersion = "Home Basic N Edition."
             Case 6 strVersion = "Business Edition."
             Case 7 strVersion = "Standard Server Edition."
             Case 8 strVersion = "Datacenter Server Edition."
             Case 9 strVersion = "Small Business Server Edition."
             Case 10 strVersion = "Enterprise Server Edition."
             Case 11 strVersion = "Starter Edition."
             Case 12 strVersion = "Datacenter Server Core Edition."
             Case 13 strVersion = "Standard Server Core Edition."
             Case 14 strVersion = "Enterprise Server Core Edition."
             Case 15 strVersion = "Enterprise Server IA64 Edition."
             Case 16 strVersion = "Business N Edition."
             Case 17 strVersion = "Web Server Edition."
             Case 18 strVersion = "Cluster Server Edition."
             Case 19 strVersion = "Home Server Edition."
             Case 20 strVersion = "Storage Express Server Edition."
             Case 21 strVersion = "Storage Standard Server Edition."
             Case 22 strVersion = "Storage Workgroup Server Edition."
             Case 23 strVersion = "Storage Enterprise Server Edition."
             Case 24 strVersion = "Server For Small Business Edition."
             Case 25 strVersion = "Small Business Server Premium Edition."
        End Select
        Wscript.Echo "This computer is running Windows Vista " & strVersion
    Else
        Wscript.Echo "This computer is not running Windows Vista."
    End If
Next

What’s that? Can we explain how this script work? You know what? Not only can we explain how this script, we will explain how this script works.

Note. Which, we might add, puts us yet another step ahead of Baskin-Robbins. How often do the folks at Baskin-Robbins offer to tell you how they make their Old-Fashioned Butter Pecan Ice Cream?

The script starts out by connecting to the WMI service on the local computer. Can we run this same script against a remote computer? Of course we can; after all, the script wouldn’t be particularly useful if you couldn’t. To run this script against a remote computer (and thus determine the flavor of Vista running on that remote machine), simply assign the name of the remote computer to the variable strComputer:

strComputer = "atl-ws-01"

Note. Yes, you can also run this script against an entire flock of computers (e.g., all the computers in an OU or all the computers listed in a text file). For sample code take a peek at our Multiple/Remote Computer Scripting Templates.

After we connect to the WMI service we then call the ExecQuery method and return a collection of all the operating systems currently in use on the computer:

Set colItems = objWMIService.ExecQuery("Select * From Win32_OperatingSystem")

Admittedly, there will only be one item in the collection; that’s because only one operating system can be in use at any given time. However, because this is a collection we still need to set up a For Each loop to loop through each and every operating system in the collection:

For Each objItem in colItems

And what are we going to do inside that loop? Well, the first thing we’re going to do is determine whether or not our computer is running any flavor of Windows Vista. That’s what this line of code is for:

If Instr(objItem.Caption, "Vista") Then

All we’re doing here is looking at the value of the operating system’s Caption property and checking to see if the word Vista appears anywhere within that caption. (That’s what the InStr function does; it checks to see if a given substring can be found anywhere inside a string value.) If InStr returns a 0 that means that the word the Vista couldn’t be found anywhere in the Caption. In that case, we assume that the computer is not running Windows Vista, and we echo back the appropriate message:

Wscript.Echo "This computer is not running Windows Vista."

But suppose InStr returns a value other than 0; for example, suppose our operating system caption is this:

Microsoft Windows Vista Enterprise

In that case, that means that the computer is running Windows Vista. And, in turn, that means we need to try and determine which version of Vista we’re dealing with.

But don’t worry: we don’t have to write some code that tries to tease the word Enterprise out of the Caption property. Instead, we use a brand-new property added to the Win32_OperatingSystem class: OperatingSystemSKU. The OperatingSystemSKU property returns an integer value that identifies the “SKU” (stock-keeping unit) for the flavor of Vista running on the computer. Admittedly, we could just report back one of these integer values (e.g., a 4 for a computer running Windows Vista Enterprise edition); that’s probably what Baskin-Robbins would do. But Microsoft is not Baskin-Robbins.

Note. Do the Scripting Guys wish Microsoft was Baskin-Robbins? We probably shouldn’t answer that question ….

Because we are Microsoft, however, we’ve taken the extra step and set up a Select Case statement that determines the value of the OperatingSystemSKU property and then assigns the corresponding operating system flavor to a variable named strVersion. For example, if the OperatingSystemSKU really does return the value 4, then the following line of code is executed (and the value gets assigned to the variable strVersion):

Case 4 strVersion = "Enterprise Edition."

After we dispose of the Select Case block all that’s to do is run the following line of code:

Wscript.Echo "This computer is running Windows Vista " & strVersion

In turn, that’s going to cause a message similar to this one to appear onscreen:

This computer is running Windows Vista Enterprise Edition.

And there you have it.

Of course, we realize that many of you might be a little worried about Microsoft; after all, having successfully matched Baskin-Robbins flavor-for-flavor, well, maybe we’ll get a little complacent, maybe we’ll just sit back and rest on our laurels. Take it from the Scripting Guys: you have nothing to worry about. True, we now have as many flavors of Vista as Baskin-Robbins does ice cream. On the other hand, Baskin-Robbins still holds the world record for the largest Ice Cream Scoop Pyramid (achieved on May 18, 2000 using 3,100 scoops of ice cream). Does that mean that Microsoft is busy working on a Windows Vista Ice Cream Scoop Pyramid? Well, we’re not allowed to discuss unreleased and unannounced products. At this point in time all we can say is this: at Microsoft, we never back away from a challenge.

Never.


How Can I Tell Which of Our Windows Server 2003 Computers are Running R2?

Hey, Scripting Guy! We have a bunch of Windows Server 2003 computers in our organization. Some of these computers are running Windows Server 2003 R2 and some are not. How can I tell which of our Windows Server 2003 computers are running R2?

-- TX

Hey, TX. You know, that’s a question that people have been trying to answer for hundreds and hundreds of years: how can I determine whether a computer is running Windows Server 2003 R2 or not? And we’re not exaggerating here, either: after all, who could ever forget Julius Caesar’s last words: “R2, Brute?”

Now, if a Julius Caesar had no idea how to determine whether or not a computer is running Windows Server 2003 R2, what makes anyone think the Scripting Guys can succeed where even the mighty Caesar failed? Well, it’s definitely true that Caesar had wealth, power, and a genius for military tactics. What he didn’t have, however, was WMI:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

For Each objItem in colItems
    If InStr(objItem.OtherTypeDescription, "R2") Then
        WScript.Echo "This computer is running Windows Server 2003 R2."
    Else
        WScript.Echo "This computer is not running Windows Server 2003 R2."
    End If
Next

Yes, we know: it seems like this script should be a little fancier than that, doesn’t it? But this will do the trick, and in just a second we’ll explain how, and why.

The script starts off like most WMI scripts: it simply makes a connection to the WMI service on the local computer. (And, like most WMI scripts it can easily be modified to run against a remote computer: just assign the name of that remote computer to the variable strComputer.) We then use this line of code to retrieve information about the operating system currently in use on the machine:

Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

Now comes the part that will have people singing the praises of the Scripting Guys long after they have forgotten Julius Caesar. As it turns out, the Win32_OperatingSystem class includes a property named OtherTypeDescription. Throughout recorded history this property has always returned a Null value. (Try it on a computer running Windows 2000 Server or Windows XP or regular old Windows Server 2003. You’ll always get back the same value: Null.) On a computer running Windows Server 2003 R2, however, this property does return a value. Not only that, but that value includes the string R2. And that’s how you determine whether or not a computer is running R2: you simply check to see if the string R2 can be found anywhere within the OtherTypeDescription property.

Needless to say, then, that’s what we do with the rest of the script. We set up a For Each loop to walk through the collection of operating systems returned by our query. (There will be only one such operating system, but we still need to use a For Each loop, something which will not be the case in Windows Vista.) We then use the InStr function to check for the presence of the string R2 in the OtherTypeDescription property:

If InStr(objItem.OtherTypeDescription, "R2") Then

If InStr returns True we report back the fact that the computer is running R2; otherwise we report back the fact that the computer is not running R2. That’s all we have to do.

So is this the most important script ever written? Well, we wouldn’t go quite that far. On the other hand, Julius Caesar didn’t know about the OtherTypeDescription property and look what happened to him. So on second thought ….


How Can I Tell Which Version of the .NET Framework is Installed on a Computer?

Hey, Scripting Guy! How can I tell which version of the .NET Framework is installed on a computer?

-- MW

Hey, MW. You know, every now and then people say to us, “Wow, you guys write a daily column about system administration scripting; you must be really smart.” We wish. The truth is, we write a daily column about system administration because we aren’t smart. When we first began this column (our one-year anniversary is fast approaching) we assumed that it would be easy: people would ask us questions like, “How do I display information in a message box?” and we’d respond by writing a script like this:

Msgbox "Here is information in a message box."

The rest of the day would then be free for playing Internet poker and watching baseball. Sweet.

What we failed to take into account, though, is the fact that while Windows is an excellent platform for system administration scripting it’s not without its weaknesses. And, needless to say, those weak spots represent the things nearly everyone asks questions about. (Come on, doesn’t anyone want to know how to display information in a message box?) As a result, we have to spend a lot of time researching various problems, trying out possible solutions, and, well, working. Meanwhile, the World Series of Poker goes on without us.

Scripting Guys Opinion. No, we do not believe that the World Series of Poker should be seen on ESPN; the same goes for the National Spelling Bee, the X Games, the NBA, and other pseudo-sports. (We have nothing against these events, but come on: not at the expense of baseball or college basketball.) Of course, if ESPN is willing to broadcast Scripting Week 3, well, we’re willing to reconsider our position. ESPN, we’re waiting to hear from you.

Editor’s Note. Now for the legal stuff: The opinions expressed here are not necessarily the opinions of the Microsoft Corporation or anyone else affiliated with Microsoft, etc. etc. As a matter of fact, not all of the Scripting Guys even agree on what a “sport” is. (But really, poker and spelling?)

MW’s question about the .NET Framework is a good example of the dilemma we sometimes face. It’s very important to know which version of the .NET Framework is installed on a computer, especially when you’re working with custom applications written in-house; the Scripting Guys know from painful experience that custom applications often require a very specific version of the .NET Framework. Any version earlier – or later – and the application might not run.

Because of that you might think there’s a very quick, easy way to determine which version (or versions) of the .NET Framework is installed on a computer. Well, if there is, we couldn’t find it: we were unable to locate a COM object or a specific registry key that could tell us, once and for all, which version is installed on a computer. The best we could do was this brute force method, a method which is accompanied by a caveat we’ll talk about momentarily. First let’s take a look at the script:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_Product")

For Each objItem in colItems
    If InStr(objItem.Name, "Microsoft .NET Framework") > 0 Then
        Wscript.Echo objItem.Version
    End If
Next

As you can see, the script itself is pretty simple. We begin by connecting to the WMI service and then use the ExecQuery method to retrieve a collection of the all the instances of the Win32_Product class. Win32_Product is designed to retrieve information about all the software on a computer that was installed using Windows Installer (i.e., a .MSI file). Win32_Product is not foolproof – it sometimes misses an application that really was installed using Windows Installer – but it seems to have no problem identifying most major software programs, including the .NET Framework.

After issuing the query we set up a For Each loop to cycle through the collection of software products installed using .MSI files. Inside this loop we use this line of code and the InStr function to see if the application name includes the string Microsoft .NET Framework:

If InStr(objItem.Name, "Microsoft .NET Framework") > 0 Then

Note. We called this a brute force method because we’re returning all the installed software products and laboriously checking each name. We could have used the LIKE operator to return only software that has the string Microsoft .NET Framework somewhere in its name. Unfortunately, though, the LIKE operator is found only on Windows XP and Windows Server 2003. Because of that, we went with the brute force method to ensure that the script would also run on Windows 2000.

All we’re doing here is passing InStr two parameters: objItem.Name (a variable which contains the name of the application) and our target string (Microsoft .NET Framework). If our target string cannot be found in objItem.Name, then the return value of InStr will be 0; if the target string can be found, then InStr will be a value other than 0. To be specific, InStr will equal the character position where our target string begins. For example, suppose objItem.Name equals this:

The Microsoft .NET Framework Version 2.0

In that case, InStr will equal 5, because the string Microsoft .NET Framework begins at the fifth character. If InStr is greater than 0 we echo the Version property of the application; in other words, we’ll get back information similar to this:

1.1.4322

And, of course, if we have multiple version of the .NET Framework installed we’d get back more than one version number.

As you can see, this actually works quite nicely. So what’s the problem? Well, there are really two issues. For one, this can be a little slow: the Win32_Product class is no speed demon, and it could take 30 seconds or so before you get any information back (depending in large part on how many applications are installed on your computer). More important, the Win32_Product class is not installed by default on Windows Server 2003. If you want to run the script against a Windows 2003 machine you’ll need to do the following:

1. In Add or Remove Programs, click Add/Remove Windows Components.

2. In the Windows Components Wizard, select Management and Monitoring Tools and then click Details.

3. In the Management and Monitoring Tools dialog box, select WMI Windows Installer Provider and then click OK.

4. Click Next.

Yes, kind of a hassle, but probably worth it. After all, the Win32_Product class can be used to get at more information than just the version of the .NET Framework installed on a computer.

Oh, and did we mention that we use this line of code to display information in a message box?

Wscript.Echo objItem.Version

Then again, if you’re running under CScript, information will display in the command window instead of a message box. OK, so maybe it isn’t so bad that people don’t ask us questions like, “How do I display information in a message box?” Even that isn’t as simple as we thought it was.


How Can I Add a Network Printer at Logon and, Optionally, Make That the Default Printer?

Hey, Scripting Guy! How can I add a network printer at logon? I’d also like to make that the default printer unless the user has a local printer. In that case I don’t want to make the network printer the default printer.

-- DB

Hey, DB. So, how are … things ….? That’s really some kind of weather we’ve/you’ve been having lately, isn’t it?

Sorry; this is proving to be a very difficult column for us to write. As we noted yesterday, the Scripting Guy who writes this column is on vacation; in fact, the column you’re reading right now was actually written several days ago. You’d think that this wouldn’t make much difference when it comes to a daily scripting column; after all, there isn’t a lot of breaking news in the scripting world. But we Scripting Guys don’t want to be like other daily scripting columns; to help differentiate ourselves from the competition we like to sprinkle our columns with references to current events and to the outside world. But it’s hard to reference current events when the column is being written in advanced, and those events haven’t actually happened yet.

Note. Well, sure, of course Peter can do it. But Peter, as the oldest living Scripting Guy, also has an unfair advantage: he’s so much older than the rest of us that he’s already seen everything that could ever happen, usually more than once. That’s not true for youngsters like the other Scripting Guys.

What that all means, of course, is that we have absolutely nothing to write about today. Sorry.

Pardon? Write about scripting? Well, that seems a bit out-of-place for a daily scripting column. But, then again, we are a little desperate today. So what the heck:

strComputer = "."

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = TRUE")

Set objNetwork = CreateObject("WScript.Network")
objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"

If colPrinters.Count = 0 Then
    objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"
End If

As you can see, we start out by connecting to the WMI service on the local computer. Can we also use this script against remote computers? Actually no; that’s because we’re going to use Windows Script Host (WSH) to map the network printer, and WSH can perform that task on only the local machine. But that’s OK, because, as a logon script, we don’t need to use this against remote machines. Logon scripts always run locally.

After connecting to the WMI service we use the following query to return a collection of all the local printers on the computer:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = TRUE")

Keep in mind that the preceding query (and thus the preceding script) works only on Windows XP, Windows Server 2003, and Windows Vista computers. Why? Because those are the only operating systems in which the Win32_Printer class includes the Local property. But don’t fret; we’ll show you a Windows 2000 version of this script in just a moment.

After we issue our WQL query we create an instance of the Wscript.Network object, then use the AddWindowsPrinterConnection method to add the network printer \\PrintServer1\Xerox300:

Set objNetwork = CreateObject("WScript.Network")
objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"

This gives us a connection to our network printer. Now the question is this: should we make this printer the default printer? To answer that we need to know whether or not the user has any local printers installed. How do we know that? Remember the query we ran, the one that returned a collection of all the local printers? All we have to do is check the value of the collection’s Count property. If the Count is equal to 0 that means no local printers are installed. In that case, we can then use this line of code to set this new printer as the default printer:

objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"

If the Count is not equal to 0 that means we have at least one local printer installed. And so we do nothing at all.

Speaking of nothing at all, we won’t bother to explain how to perform this same task on a Windows 2000 computer. However, we’ll at least give you a script that does so:

blnLocal = FALSE
strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

For Each objPrinter in colPrinters
    If objPrinter.Attributes And 64 Then
        blnLocal = TRUE
    End If
Next

Set objNetwork = CreateObject("WScript.Network")
objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"

If blnLocal = True Then
    objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"
End If

Incidentally, as we were writing this we realized, “Hey, we don’t have to talk about current events. If something happened in the past we can bring it up and no one will ever notice.” With that in mind, how about that French Revolution, huh? Was that crazy or what?


How Can I Change the Printer Port for a Printer?

Hey, Scripting Guy! How can I change the printer port for a printer? For example how can I change it from FILE: to LPT1:?

-- HK

Hey, HK. Despite the Scripting Guys reputation as sophisticated intellectuals – really? We don’t have a reputation as sophisticated intellectuals? Really? This is very distressing.

Anyway, what we were going to say is that, despite the reputation we don’t seem to have, we do watch a lot of television. Because of that we get to see plenty of those truth-in-advertising drug commercials, the ones where drug companies tout the wonders of their new miracle cure and then, in a quiet voiceover, note that, “Side effects may include dizziness, nausea, drowsiness, and headaches; might also turn you into a newt. Should not be taken by women who are pregnant, who may become pregnant, who know someone who knew someone who was pregnant, or whose mother was once pregnant. Should not be taken by men unless they want to become pregnant.”

We were reminded of these standard drug company disclaimers because we need to start this column off with a standard disclaimer of our own: “The following script can be used to change the printer port for a printer, provided the script is run on a Windows XP or Windows Server 2003 computer. This script will not work on Windows 2000, Windows NT 4.0, Windows 98, Windows 95, Windows 3.1, or Windows 3.0 computers.”

Windows Me? Well, if you ever find someone who actually uses Windows Me, they’re out of luck, too. Sorry.

In other words, you can change the printer port for a printer, but only on Windows XP and Windows Server 2003 computers. And here’s a sample script that will do it:

strComputer = "."

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID='Art Department Printer'")

For Each objPrinter in colPrinters
    objPrinter.PortName = "LPT1:"
    objPrinter.Put_
Next

As you can see, we connect to the WMI service and then use the ExecQuery method to retrieve a collection of all the printers with a DeviceID equal to Art Department Printer. If you try this script on your own, make sure you use a Where clause that will limit the returned collection to a single printer; otherwise you run the risk of changing the printer port for all the printers on your computer. (And, yes, DeviceID is a good way to go because DeviceIDs must be unique on a computer.)

After we get back our collection we set up a For Each loop to walk through all the printers in that collection. (If we construct our query correctly, there will be just one item in the collection.) Inside the For Each loop we change the value of the PortName property, then call the Put_ method to write those changes to the printer object. That’s all it takes.

So the next time you – wait a second: who just said that this column can cause dizziness, nausea, drowsiness, and headaches? Hey, we’re supposed to be the funny ones around here.

Well, we did say supposed to be….


How Can I Disable the Keep Printed Documents Attribute on a Printer?

Hey, Scripting Guy! How can I disable the Keep Printed Documents attribute on a printer?

-- JJ

Hey, JJ. You didn’t mention which operating system you’re trying to do this on, but we hope it’s a Windows XP or Windows Servers 2003 computer. Why? Well, on a Windows XP or Windows Server 2003 machine, this is easy; on any other version of Windows it’s a bit harder. How much harder? Well, to tell you the truth, it’s downright impossible.

Which definitely makes the task a bit harder to carry out.

Yes, we know: that isn’t fair. But such is life. With Windows XP, the WMI class Win32_Printer was upgraded to include a new read/write property: KeepPrintedJobs. As you might have guessed, we can simply toggle this property value back-and-forth to keep (or not keep) printer jobs for a specified printer. Unfortunately, this property does not exist on any version of Windows prior to Windows XP. With, say, Windows 2000, we can use a script to tell whether or not this attribute is enabled for a printer, but we can’t actually change the value.

Note. Sadly, ADSI – which often provides a handy workaround for printer management in Windows 2000 – is of little use to us here. There is a new ADSI attribute for configuring the Keep Printed Documents property but it won’t do you any good on a Windows 2000 computer, either.

Assuming that you are using Windows XP or Windows Server 2003, however, then you can disable the Keep Printed Documents property by using a script like this:

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'ArtDepartmentPrinter'")

For Each objPrinter in colPrinters
    objPrinter.KeepPrintedJobs = False
    objPrinter.Put_
Next

We know: it seems really unfair that you can’t do this at all on Windows 2000 yet the script is so remarkably simple for Windows XP and Windows Server 2003. (Did we mention that life isn’t fair? Well, it’s not.) Our script for modifying the KeepPrintedJobs property is a run-of-the-mill WMI script, one that starts out by connecting to the WMI service on the local computer. (Although, as usual, this works just as well on remote computers.) We then issue this query to return a collection of all the printers on the computer that have a DeviceID of ArtDepartmentPrinter:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'ArtDepartmentPrinter'")

Keep in mind that we did this because you specifically asked about changing the property value on a printer. In some cases, however, you might want to modify this property value for all the printers on a print server. If that’s the case, then simply remove the Where clause from the query. This query returns a collection of all the printers on a computer, enabling you to change the KeepPrintedJobs property for, say, each for the 150 or so printers on a print server:

Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

And yes, that is why we like WMI so much.

As soon as we have our collection we set up a For Each loop to walk us through all the printers in the collection (again, for this particular script we’ll have only one printer, the printer with the DeviceID ArtDepartmentPrinter). Inside our For Each loop we find these two lines of code:

objPrinter.KeepPrintedJobs = False
objPrinter.Put_

Here we’re doing nothing more than setting the value of the KeepPrintedJobs property to False, then calling the Put_ method to actually save those changes to the printer. From this point on, ArtDepartmentPrinter will no longer save a copy of printed documents. That’s all there is to it.

As we noted earlier, you can’t modify this property on a Windows 2000 computer; you can, however, determine whether or not this property has been enabled on a Windows 2000 computer. This isn’t much of a consolation prize, but here’s a script that will do just that:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'ArtDepartmentPrinter'")

For Each objPrinter in colPrinters
    If objPrinter.Attributes And 256 Then
        Wscript.Echo "Printed documents are kept."
    Else
        Wscript.Echo "Printed documents are not kept."
    End If
Next

On a Windows 2000 computer, the KeepPrintedJobs property is part of a read-only “bitmask” property called Attributes. We won’t discuss bitmasks or the Attributes property in any detail today; instead we’ll simply point out that the KeepPrintedJobs property has an attribute value of 256. Because of that, we can use Boolean logic to determine whether KeepPrintedJobs is enabled or not. If this line of code is true, that means KeepPrintedJobs has been enabled:

If objPrinter.Attributes And 256 Then

Sure, it looks weird, but think of it in these terms: imagine there’s a switch on the computer that has a value of 256. If that switch is on then KeepPrintedJobs is enabled; if that switch is off then KeepPrintedJobs is disabled. All we’re doing is checking to see whether or not that switch is on or off.

Incidentally, and before you ask, we’re afraid the answer is no: it’s highly unlikely the version of WMI found on Windows 2000 will ever be updated to allow you to modify the KeepPrintedJobs property. Sorry.

Now, let’s see, is there any more bad news we can tell you about today? Well, now that you mention it….


How Can I Let Users Choose a Default Printer From a List of Printers?

Hey, Scripting Guy! In a logon script, how can I enable users to choose a default printer from a list of the printers installed on their computer?

-- JD

Hey, JD. You know, a lot of people write in to say, “Hey, Scripting Guys! Why do you limit yourselves to system administration scripting? Don’t you realize that you guys, and you guys alone, have the power to make the world a better place?”

Now, to be honest, sometimes we – what’s that? How many people write in to say that we, and we alone, have the power to make the world a better place? Well, it’s not really our style to keep count of that sort of thing. Nevertheless, we’re sure that it’s – hey, you know what? Numbers are really meaningless when it comes to stuff like this. The important thing is that even though we maintain a laser-like focus on system administration scripting we can still have an impact, we can still help make the world a better place.

Case in point? Following this year’s Super Bowl we made particular note of one of the commercials aired during the game. In this commercial, a robot that worked for a US auto maker made a simple mistake, and was immediately fired. (Interestingly enough, Microsoft has the exact same policy; fortunately, though, none of the Scripting Guys have ever made a mistake.) After failing at a number of new jobs our robot finds himself (herself?) standing atop a bridge, ready to jump off and end it all. At that very moment, however, he (she?) wakes up and realizes that it was all a bad dream.

As we noted back in February, the Scripting Guys were a little perplexed by the message sent by this commercial. As near as we could tell, the auto maker was telling its employees this: if you make a mistake we’re going to fire you and, when that happens, you might as well just kill yourself. That’s pretty inspiring and uplifting. We thought that was an odd message to send, especially in light of the fact US auto makers have laid off an awful lot of people over the past few years.

Now, we know what you’re thinking: that is an odd message to send, isn’t it? But what can one person do about that, even if that one person is a Scripting Guy? Well, here’s what one person can do about that. Last night the Scripting Guy who writes this column saw the robot commercial for the first time since the Super Bowl. And guess what? The ending has been changed. In the new version the robot doesn’t stand atop a bridge ready to jump off and commit suicide. That can mean only one thing: the auto maker read our Hey, Scripting Guy! column, realized the error of its ways, and changed the commercial.

OK, granted, in the new version the robot sees a similar robot being unceremoniously tossed onto a scrap heap. But this is at least a step in the right direction.

The moral of this story (and yes, all Hey, Scripting Guy! stories have a moral … somewhere) is that one person can make a difference, one person does have the power to make the world a better place. Maybe you can do this by saving a fictional robot from thoughts of suicide. Or, even better, maybe you can do this by coming up with a way for users to choose a default printer from a list of the printers installed on their computer:

<SCRIPT Language="VBScript">

Sub Window_Onload
strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer &
"\root\cimv2")
Set colPrinters = objWMIService.ExecQuery("Select * From
Win32_Printer")

For Each objPrinter in colPrinters
strPrinter = objPrinter.Name
Set objOption = Document.createElement("OPTION")
objOption.Text = strprinter
objOption.Value = strPrinter
AvailablePrinters.Add(objOption)
Next
End Sub

Sub SetDefault
strPrinter = AvailablePrinters.Value
Set WshNetwork = CreateObject("Wscript.Network")
WshNetwork.SetDefaultPrinter strPrinter
Msgbox strprinter & " has been set as your default printer."
End Sub

</SCRIPT>

<select size="5"
name="AvailablePrinters"></select><p>
<input type="button"
value="Set as Default" onClick="SetDefault">

Let’s talk about what we have here; what we do have here is an HTA (HTML Application). HTAs are simply a way for script writers to create graphical user interfaces; they are essentially Web pages (albeit with a .hta file extension) designed to run off your local machine. (Yes, we know: that’s not a very good explanation. If you need more information about HTAs, well, that’s what the HTA Developers Center is for.) VBScript doesn’t have a native method for creating graphical user interfaces. That’s a problem, but we can work around that problem by creating an HTA, and then calling that HTA from within our logon script.

And that’s exactly what we’ve done here: we’ve created a .hta file, a file that we’ll need to copy to each of our computers. (But that’s no problem; your logon script can do that.) Inside our logon script we then need just two lines of code to pop up this HTA and, in turn, let users select a default printer:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "C:\Scripts\Test.hta"

So what will people see when this HTA pops up? They’ll see – in all its barebones splendor – something that looks like this:

Yes, we know. But if you want beautiful you’re on your own.

Now that you understand the basic scenario let’s take a closer look at the HTA itself. As far as the HTML tagging goes, well, there really isn’t much: all we have is a list box named AvailablePrinters (added to the HTA by using the <SELECT> tag) and a button (<INPUT TYPE = "BUTTON">):

<select size="5"
name="AvailablePrinters"></select><p>
<input type="button" value="Set as Default"
onClick="SetDefault">

The idea here is that you select a printer from the list box and then click the button; upon doing so, the printer you selected will be configured as your default printer.

That’s right: just like magic.

If you’ve done much HTML tagging you probably noticed that, as configured, our list box is empty; it doesn’t include a single installed printer. Why not? Well remember, that’s the whole idea here: we need a dynamic list box, one that, each time the HTA starts, gets populated with the printers (and only those printers) installed on the computer in question. How do we propose to do that? Why, by using this subroutine, of course:

Sub Window_Onload
strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" &
strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery("Select *
From Win32_Printer")

For Each objPrinter in colPrinters
strPrinter = objPrinter.Name
Set objOption = Document.createElement("OPTION")
objOption.Text = strPrinter
objOption.Value = strPrinter
AvailablePrinters.Add(objOption)
Next
End Sub

This, needless to say, is a subroutine named Window_Onload; in an HTA (or in a Web page) any subroutine with that name automatically runs whenever the HTA is opened or refreshed. Inside this subroutine, we first use this block of code to retrieve a collection of all the printers installed on the local computer:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

That was easy, wasn’t it? The question now is this: what do we do with this collection of printers?

To begin with, we set up a For Each loop to loop through each printer in the collection. For each printer we grab the value of the Name property and store it in a variable named strPrinter. We then run headlong into this line of code:

Set objOption = Document.createElement("OPTION")

What we’re doing here is creating an instance of the HTML Option object; as the name implies, this object represents an individual option in a list box. After creating a “blank” option we then assign the printer name to the option’s Text and Value properties:

objOption.Text = strPrinter
objOption.Value = strPrinter

In case you’re wondering, the Text property represents the actual text displayed in the list box; the Value property is, well, the value assigned to that option. For this particular list box we made the Text and Value identical, but they don’t have to be. For example, suppose you had a list box that allowed people to select US states. You could set the Text of an option to Washington; that way people would see the name Washington in the list box. At the same time, however, you could set the value of that option to WA. That way people would see a “friendly” name in the list box, but you could still use the official postal abbreviation in your script.

After we assign the property values we then call the Add method and add the option (that is, the name of the first printer) to the list box:

AvailablePrinters.Add(objOption)

And then we repeat the process with the next printer in the collection.

The Window_Onload subroutine populates our list box with the names of all the printers installed on the local computer. To make one of these printers the default printer we need to select a the printer from the list box and then click the Set as Default button. Clicking the button causes the following subroutine to run:

Sub SetDefault
    strPrinter = AvailablePrinters.Value
    Set WshNetwork = CreateObject("Wscript.Network")
    WshNetwork.SetDefaultPrinter strPrinter
    Msgbox strPrinter & " has been set as your default printer."
End Sub

As you can see, we’re not doing anything too-terribly fancy in this subroutine. To begin with, we grab the Value from the item selected in the list box and assign that to a variable named strPrinter:

strPrinter = AvailablePrinters.Value

We use these two lines of code to create an instance of the Wscript.Network object and then call the SetDefaultPrinter method in order to make the selected printer the default printer:

Set WshNetwork = CreateObject("Wscript.Network")
WshNetwork.SetDefaultPrinter strPrinter

And then, just for the heck of it, we display a message stating that the selected printer is now the default printer.

And that’s basically all we have to do. Admittedly we don’t have any error-handling here and we don’t have an obvious way for users to close the dialog box without choosing a new default printer. Likewise we could get a little fancier and, say, display the name of the current default printer in a different color. But look at it this way: if we did all the work you might start to feel like no one needed you, and you might be tempted to jump off a bridge or throw yourself on a scrap heap. Which means that all these things we don’t do are actually something we do for your own good.

You’re welcome.

So which world problem will the Scripting Guys tackle next? Well, there’s definitely no shortage of problems in the world. However, we have our sights fixed on the biggest one of all: those commercials aimed at male … dysfunctions …. Although we try not to be prudish, well, have you ever seen some of these commercials? Could you imagine meandering through a bookstore, just hoping to find a book, and instead stumbling upon – well, you know. Or trying to eat a sandwich in a restaurant only to look up and see two people – people old enough to know better – mauling each other? When is a moment the right moment? Trust is here: for some things, never.


How Can I List All the Printers on a Remote Computer?

Hey, Scripting Guy! How can I list all the printers – both local printers and network printers – on a remote computer?

-- CH

Hey, CH. You know, back when he worked at the University of Washington the Scripting Guy who writes this column was asked to speak at a symposium being held as part of a conference on computers and education. To say this was a last-minute invitation would be an understatement; in fact, our hero had to start putting together his talk on the plane ride from Seattle to Florida, then finish it when he reached the convention center. When he arrived at the symposium itself, he discovered that his fellow speakers were hardly as harried as he was. Their secret: instead of creating a new talk for the symposium they decided to just deliver talks that they were already giving at the conference. In other words, they were going to give the exact same talk on two different days, but to two different audiences (or so they hoped).

Note. As a matter of fact it was a very interesting symposium, if only because the future Scripting Guy was the only person on the panel who had a talk even remotely connected to the topic. Needless to say, it proved to be the lowest-rated session at the conference. (And, let’s face it: being voted the most-boring and least-useful session at an academic conference isn’t easy.)

Now, if this future Scripting Guy was smart he would have learned that clever people can do twice the work with half as much effort. Your question, CH, is a good example of that. Because you specifically asked about listing printers on a remote computer, a smart person would write a column about that, then come back a few months later and reprint the column, only this time telling people how to list printers on the local computer. (And a smart person could get away with that because the two scripts are all-but identical.)

Of course, as people who read this column know, the Scripting Guy responsible for all this isn’t particularly smart. Instead of writing two separate columns he could only come up with one, thanks to a script that lists the printers on either a remote computer or a local computer:

strComputer = "atl-ps-01"

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

Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

For Each objPrinter in colPrinters
    If objPrinter.Attributes And 64 Then
        strPrinterType = "Local"
    Else
        strPrinterType = "Network"
    End If
    Wscript.Echo objPrinter.Name & " -- " & strPrinterType
Next

As you can see, we begin by assigning the name of the remote computer (in this case, atl-ps-01) to a variable named strComputer. What if we did want to list the printers on the local machine? No problem, and no need to read a separate column, either. Instead, just assign strComputer the name of the local machine, or assign it a dot (WMI shorthand for the local computer):

strComputer = "."

After connecting to the WMI service on the desired computer, we then issue the following query, a query that returns a collection of all the printers installed on the computer:

Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

If you weren’t sure just how … smart … the Scripting Guy who writes this column really is, well, what comes next should help convinced you. CH only asked for a list of printers, both local and network; however, no one mentioned whether or not the script could specify which printers were which. That could have been a whole ‘nother column; instead, we simply added a few lines of code that will make that distinction for you.

To make that distinction we first set up a For Each loop that loops through the collection of printers. Inside that loop the very first thing we do is execute this line of code:

If objPrinter.Attributes And 64 Then

What we’re doing here is checking the bitmask property Attributes to see whether bit 64 has been set. If it has, then we know that this is a local printer; in that case we set the value of the variable strPrinterType to Local:

strPrinterType = "Local"

If bit 64 has not been set then this must be a network printer; consequently we assign strPrinterType the value Network:

strPrinterType = "Network"

Two notes about this process. First, we aren’t going to talk about bitmask attributes today; for a reasonably good explanation of these babies take a look at this section of the Microsoft Windows 2000 Scripting Guide.

Second, you might be wondering why we didn’t just check the values of the Win32_Printer properties Local and Network; after all, isn’t the sole purpose of those properties to tell you whether a printer is a local printer or a network printer? Yes it is, and using these two properties is a bit easier and a bit more straightforward than using the Attributes property. However, the Local and Network properties are supported only on Windows XP and Windows Server 2003. By using the Attributes property we were able to create a generic script that will work on any version of Windows that supports WMI.

Which, come to think of it, means we missed out on yet another column: How Can I Determine Printer Types on a Windows 2000 Computer?

Sigh ….

All we have left to do now is echo back the printer name and the printer type:

Wscript.Echo objPrinter.Name & " -- " & strPrinterType

And that should do it.

Incidentally, a professor from the UW happened to attend the symposium where the future Scripting Guy spoke. As she told him the following day, she was shocked, absolutely shocked that the Scripting Guy had poked fun at the university. However, she added, “Seeing as how you’re not a professor and really not all that smart I’m not going to report you to anyone.” You can imagine how gratified this Scripting Guy was to hear that. (Editor’s Note: People at Microsoft say pretty much the same things about this Scripting Guy. At least he’s consistent.)


How Can I List the Network Printers For the Logged-On User?

Hey, Scripting Guy! How can I create a logon script that lists the network printers for the logged-on user, then writes that information to a text file?

-- RC

Hey, RC. You know, the nice thing about coming to work after a few days off is – well, OK, so maybe we can’t actually think of anything nice about coming to work after a few days off. If you happen to work at Microsoft, however, you’re at least likely to hear something interesting the moment you get back. For example, this morning the Scripting Guy who writes this column was in the bathroom when he overheard the following conversation:

“So how did he get a window office?”

“I had to give him a window office. He told me he gets violent and angry if he doesn’t have a window office. I didn’t want him to get violent and angry, so I gave him a window office.”

Needless to say, window offices are more valuable than gold around here; for the most part they’re only awarded to the best and the brightest. (And, for some reason, to the Scripting Editor, who also has a window office.) As you might expect, the Scripting Guy who writes this column doesn’t have a window office. Of course, up until now, he wouldn’t expect to have a window office; after all, his name rarely comes up any time people talk about the best and the brightest. But apparently you don’t have to be among the best and the brightest, at least not as long as you’re willing to be violent and angry. Hmmm ….

Ah, who are we trying to kid? For one thing, the Scripting Guy who writes this column is too lazy to be violent. For another, how could he ever be angry, especially when he has the opportunity to do things like write a logon script that can list all the network printers for the logged-on user:

Const ForWriting = 2

Set objNetwork = CreateObject("Wscript.Network")

strName = objNetwork.UserName
strDomain = objNetwork.UserDomain
strUser = strDomain & "\" & strName

strText = strUser & vbCrLf

strComputer = "."

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = FALSE")

For Each objPrinter in colPrinters
    strText = strText & objPrinter.Name & vbCrLf
Next

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.CreateTextFile _
    ("C:\Scripts\Printers.txt", ForWriting, True)

objFile.Write strText

objFile.Close

Two quick caveats before we explain how the script works. First, in its current incarnation this script won’t work on Windows 2000 or any earlier versions of Windows; that’s because those flavors of Windows don’t include the Local property as part of the WMI class Win32_Printer. But don’t get violent and angry; before we call it a day we’ll show you a modified script that will work on Windows 2000.

Second, this script simply logs the information to a text file named C:\Scripts\Printers.txt; each time the script runs it overwrites the information in Printers.txt with the most recent collection of printer information. That’s fine, but it might not be the behavior you had in mind. If that’s the case then you’ll have to modify the code that writes to the text file. But, again, there’s no need to get violent and angry (violent or angry, maybe, but not violent and angry). After all, we have plenty of information about working with text files. (And that doesn’t even include the Microsoft Windows 2000 Scripting Guide, which also has information about working with text files.)

OK, now we’ll explain how the script works. To begin with, with define a constant named ForWriting and set the value to 2; we’ll use this constant when we write the printer information to the text file. Following that we create an instance of the Wscript.Network object, then execute these three lines of code:

strName = objNetwork.UserName
strDomain = objNetwork.UserDomain
strUser = strDomain &
"\" & strName

What are we doing here? Well, all we’re doing is determining the name of the logged-on user (using the UserName property) and the domain of the logged-on user (UserDomain); that’s what the first two lines of code are for. In line three we simply concatenate the user name and domain, giving us a user name in the format DOMAIN\username. In other words:

FABRIKAM\kenmyer

We then take this information plus a carriage return-linefeed (using the VBScript constant vbCrLf) and stash it in a variable named strText:

strText = strUser & vbCrLf

After that we connect to the WMI service on the local computer (because this is a logon script we don’t care about connecting to the WMI service on a remote computer). We then use this line of code to retrieve a collection of all the network printers on the computer:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = FALSE")

Here we’re simply selecting all the instances of the Win32_Printer class, or at least those instances where the Local property is False. As you can probably guess, that results in a collection of all the network printers; if the Local property was True we’d have a local printer rather than a network printer. That’s why the script won’t work on Windows 2000; the Win32_Printer class in Windows 2000 doesn’t support the Local property.

But don’t worry: we said we’d show you an alternate script that will work on Windows 2000 and, by golly, we’re going to do just that.

But not yet. Before we do that, let’s finish explaining how the original script works. After we retrieve the printer information we set up a For Each loop that lets us walk through the entire collection; for each printer in the collection we use this line of code to add the Name of the printer to the variable strText:

strText = strText & objPrinter.Name & vbCrLf

This should be pretty easy to follow: all we’re doing is assigning a new value to strText. And just what is that new value? That’s easy: it’s the existing value of strText plus the name of the printer plus a carriage return-linefeed.

The rest is easy. First we create an instance of the Scripting.FileSystemObject object; we then use the CreateTextFile method to create a new text file named C:\Scripts\Printers.txt:

Set objFile = objFSO.CreateTextFile _
    ("C:\Scripts\Printers.txt", ForWriting, True)

Note the three parameters we pass to the CreateTextFile method:

· The full path to the new file (C:\Scripts\Printers.txt).

· The constant ForWriting, which indicates that we want to write to the file.

· The value True, which tells the script to overwrite an existing instance of C:\Scripts\Printers.txt. If the file Printers.txt exists then the script will overwrite it; if the file doesn’t exist then the script will create it.

After that we call the Write method to write the printer information to the file, then use the Close method to close the file. If we open the text file we should see information similar to this:

FABRIKAM\kenmyer
\\atl-ps-01\b43-2927-a
\\atl-ps-01.fabrikam.com\b43-2315-a
\\atl-ps-02\HP LaserJet 5MP

Which is exactly what we wanted to see.

Now, what about Windows 2000? Well, without explaining how this works, here’s a logon script that will run on Windows 2000 and Windows NT:

Const ForWriting = 2

strComputer = "."

Set objNetwork = CreateObject("Wscript.Network")

strName = objNetwork.UserName
strDomain = objNetwork.UserDomain
strUser = strDomain & "\" & strName

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

strText = strUser & vbCrLf

Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

For Each objPrinter in colPrinters
    If Not objPrinter.Attributes And 64 Then
        strText = strText & objPrinter.Name & vbCrLf
    End If
Next

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.CreateTextFile _
    ("C:\Scripts\Printers.txt", ForWriting, True)

objFile.Write strText

objFile.Close

The big difference here? This time we need to return all the printers, both local and network. We then check the value of the bitmask property Attributes. If the “switch” equal to 64 is on then this is a local printer; if it’s not, then this must be a network printer. In that case, we then add the Name of the printer to the variable strText.

Incidentally, we need to issue a correction before we go. Earlier we stated that window offices were distributed to “the best and the brightest.” As it turns out, window offices are distributed on the basis of seniority. In other words, the Scripting Editor didn’t get a window office because she’s really, really smart; instead, she got one because she’s really, really old.

Just don’t tell her we said that.

Editor’s Note: The fact that the Scripting Guy who writes this column is much older than the Scripting Editor sort of blows away that last theory. Unless we’re talking about mental age, in which case the Scripting Editor has at least managed to reach adulthood, unlike the Scripting Guy who writes this column.


How Can I Map a Printer Based on the Computer’s OU?

Hey, Scripting Guy! How can I map a printer based on the local computer’s OU?

-- TG

Hey, TG. Before we begin, we’d like to offer a sincere and heartfelt apology to the Scripting Editor. In a recent column, we made it sound as if, in her words, the Scripting Editor was a “ditzy teenager.” That, of course, is not the case. Instead, we were just poking a little good-natured fun at our beloved Scripting Editor. In addition, we assumed that, seeing as how she is well into her twilight years, the Scripting Editor would be flattered to know that someone thought of her as being young, vibrant, and alive. That was an assumption we should not have made without asking, and we apologize for that. In other words, we’re sorry, Scripting Editor; you are not a ditzy teenager.

Well, you’re not a teenager, anyway.

OK, now that the Scripting Editor is happy let’s move on to the business at hand: writing a script that can map a printer based in the local computer’s OU. Following our usual modus operandi (a Latin phrase meaning “modus operandi”), let’s take a look at the script, then see if we can explain how it all works.

Note. Funny you should mention that, but, yes, Latin was the language the Scripting Editor spoke as a girl. But back in those days, that was the language most people spoke.

Here’s the script:

Set objSysInfo = CreateObject("ADSystemInfo")
strName = objSysInfo.ComputerName

arrComputerName = Split(strName, ",")
arrOU = Split(arrComputerName(1), "=")
strComputerOU = arrOU(1)

Set objNetwork = CreateObject("WScript.Network")

Select Case strComputerOU
    Case "Client"
        objNetwork.AddWindowsPrinterConnection "\\PrintServer1\ClientPrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer1\ClientPrinter"
    Case "Finance"
        objNetwork.AddWindowsPrinterConnection "\\PrintServer2\FinancePrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer2\FinancePrinter"
    Case "Human Resources"
        objNetwork.AddWindowsPrinterConnection "\\PrintServer3\HRPrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer3\HRPrinter"
    Case "Research"
        objNetwork.AddWindowsPrinterConnection "\\PrintServer4\ResearchPrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer4\ResearchPrinter"
    Case Else
        objNetwork.AddWindowsPrinterConnection "\\PrintServer5\GenericPrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer5\GenericPrinter"
End Select

As you can see, we start out by creating an instance of the ADSystemInfo object, an object that returns a lot of useful information about the logged-on user, his or her domain, and the local computer. And yes, we are talking the local computer; the ADSystemInfo object cannot be created on a remote computer. That means the preceding script can be run only on the local machine. Which, among other things, makes the code a good candidate for inclusion in a logon script.

After creating the ADSystemInfo object we next assign the value of the ComputerName property to a variable named strName:

strName = objSysInfo.ComputerName

If you aren’t familiar with the ADSystemInfo object, we should note that the ComputerName property returns the distinguished name of the local computer; that’s going to be a value similar to this one:

CN=ATL-WS-01,OU=Research,OU=North America,DC=fabrikam,DC=com

If you take a moment to look over this value, you’ll see that the OU where the computer account resides is embedded within the distinguished name: the first item in the string contains the computer’s CN (CN=ATL-WS-01) and the second item contains the computer’s OU (OU=Research). That’s great, except for one thing: how do we extract the OU from the distinguished name?

Here’s one way:

arrComputerName = Split(strName, ",")
arrOU = Split(arrComputerName(1), "=")
strComputerOU = arrOU(1)

What are we doing here? Well, in the first line we’re using the Split function to transform the computer’s distinguished name into an array; by splitting the value on the comma we end up with an array containing the following elements:

· CN=ATL-WS-01

· OU=Research

· OU=North America

· DC=fabrikam

· DC=com

As you can see, the second item in the array (an item which has an index number of 1) is the information we’re looking for. Unfortunately, though, it’s not exactly in the desired format; we’d prefer to have the name Research rather than the value OU=Research. So now what do we do?

Well, what we do now is use the Split function once more, this time to split the value of array item 1, and this time using the equals sign as the character to split on. That’s what our second line of code does:

arrOU = Split(arrComputerName(1), "=")

Upon execution, that’s going to give us another array (an array named arrOU) that includes these items:

· OU

· Research

And yes, you’re way ahead of us: the second item in this array (the item with an index number of 1) is the very value we’re looking for. Consequently, we assign the value of that item to a variable named strComputerOU:

strComputerOU = arrOU(1)

Now that we know the computer’s OU we can create an instance of the Wscript.Network object (used for mapping printers); after that we can then set up a Select Case statement and map a printer based on the OU name. That’s why we have code similar to this:

Select Case strComputerOU
    Case "Client"
        objNetwork.AddWindowsPrinterConnection "\\PrintServer1\ClientPrinter"
        objNetwork.SetDefaultPrinter "\\PrintServer1\ClientPrinter"

This block of code should be self-explanatory. If the value of strComputerOU is equal to Client (that is, if the computer account resides in the Client OU) we use the AddWindowsPrinterConnection method to map a printer to \\PrinterServer1\ClientPrinter; once the printer has been added we then call the SetDefaultPrinter method to make this the default printer.

Ah, but what if the computer name isn’t equal to Client? That’s fine; we simply check the next value in the Select Case block. If the OU name can’t be found at all, then we default to the Case Else section and map a “generic” printer instead:

Case Else
    objNetwork.AddWindowsPrinterConnection "\\PrintServer5\GenericPrinter"
    objNetwork.SetDefaultPrinter "\\PrintServer5\GenericPrinter"

Believe it or not, that’s all we have to do.

We hope that gets you started, TG. In the meantime, we’d like to apologize once more to the Scripting Editor. As part of his penance, the Scripting Editor suggested that the Scripting Guy who writes this column wash her car and clean her office. As a gesture of good faith, we’ve decided to go along with that: the Scripting Guy who writes this column will wash the Scripting Editor’s car the next time he washes his own car, and he will clean her office the next he cleans his office. Based on current trends, she can expect to get her car washed sometime in early 2013. As for cleaning the office, well, let’s put it this way: let’s hope that she eats right and gets plenty of exercise. Otherwise, she probably won’t be around long enough to experience a newly-cleaned office.


How Can I Map a Printer, But Only If the User Doesn’t Have a Local Printer?

Hey, Scripting Guy! I’d like to have a logon script that maps a network printer for each user unless that user already has a local printer. If they have a local printer, I don’t want to add the network printer. Is that possible?

-- AG

Hey, AG. Not only is that possible, but it’s actually pretty easy, especially if you’re running Windows XP or Windows 2003. We’ve got two issues here: we need some code that maps a network printer, and we need some code that figures out whether or not the user has a local printer. Let’s start with the code for mapping a network printer:

Set objNetwork = CreateObject("WScript.Network")
objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"
objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"

If you’re sitting there waiting patiently for the rest of the code, well, it’s going to be a long wait: that’s all the code you need to map a network printer. In the first line, we create an instance of the WSH Network object. In the second line, we use the AddWindowsPrinterConnection method to make a connection to the printer Xerox300 (found on the print server named PrintServer1). That’s really all we need. Line 3, which makes the new printer the default printer, is optional. We added it just to show you how easy it is to make a printer the default printer.

Now, what about determining whether or not there are any local printers connected to a computer? As it turns out, in Windows XP and Windows 2003 the WMI class Win32_Printer has a property named Local. If this property is TRUE, then the printer in question is a local printer; if this property is FALSE, then the printer in question is a network printer. If we want to know if there are any local printers attached to a computer, all we have to do is query for all the printers where the Local property is TRUE:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = TRUE")

We can then check the Count property to see how many local printers were found. If the Count equals 0, then no local printers were found. In that case, we’d go ahead and map a network printer. If the Count is greater than 0, at least one local printer was found. In that case, we don’t want to map a network printer, so we just don’t do anything at all (something we Scripting Guys are incredibly good at).

Here’s a complete script that – on Windows XP and Windows 2003 – maps a network printer if no local printers are found:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Local = TRUE")

If colPrinters.Count = 0 Then
    Set objNetwork = CreateObject("WScript.Network")
    objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"
    objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"
End If

The more observant (or paranoid) among you might have noticed that we keep talking about Windows XP and Windows 2003, but we’ve been strangely silent when it comes to other versions of Windows. Does that mean you can’t perform this task on, say, Windows 2000? No, it just means that – prior to Windows XP – the Win32_Printer class did not include a Local property. Consequently, we can’t query for all printers where Local = TRUE.

But that’s OK; as usual, we can find a way around this limitation. Previous versions of the Win32_Printer do include a property named Attributes. One of the “switches” housed within the Attributes property tells you whether or not the printer is a local printer. If this particular bit (equal to 64) is enabled, then the printer is local; if it is not enabled, then the printer is a network printer.

Therefore, we can use some Boolean logic to test whether or not a computer has any local printers. We don’t have time to explain Boolean logic today; for that you might want to check out this section of the Microsoft Windows 2000 Scripting Guide. In the meantime, here’s a script that will work on Windows 2000:

blnLocal = FALSE
strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery("Select * From Win32_Printer")

For Each objPrinter in colPrinters
    If objPrinter.Attributes And 64 Then
        blnLocal = TRUE
    End If
Next

If blnLocal = FALSE
    Set objNetwork = CreateObject("WScript.Network")
    objNetwork.AddWindowsPrinterConnection "\\PrintServer1\Xerox300"
    objNetwork.SetDefaultPrinter "\\PrintServer1\Xerox300"
End If

In this script, we set a variable named blnLocal to FALSE. We then retrieve a list of all the printers on a computer and check each one to see if the local printer bit has been set. That’s what this line of code does:

If objPrinter.Attributes And 64 Then

If a local printer is found, we set the value of blnLocal to TRUE. At the end of the script, if blnLocal is TRUE, we know that a local printer was found and the script simply ends. If blnLocal is FALSE, however, that means that no local printer was found. Therefore, we go ahead and map the network printer.

Whew! But it works.


How Can I Print a Test Page to a Printer?

Hey, Scripting Guy! How can I print a test page to a printer?

-- RR

Hey, RR. As long as you’re running Windows XP this is an easy one; that’s because the revised Win32_Printer class introduced in Windows XP includes a method called PrintTestPage. As you might expect, this means you can bind to a given printer on a computer and then call the PrintTestPage method to, well, print a test page.

Here’s a sample script that does just that:

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

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where DeviceID = '\\\\atl-ps-01\\color-printer'")

For Each objPrinter in colPrinters
    errReturn = objPrinter.PrintTestPage
    If errReturn = 0 Then
        Wscript.Echo "The test page was printed successfully."
    Else
        Wscript.Echo "The test page could not be printed."
    End If
Next

All we do in this script is connect to the WMI service, then run a query that binds us to the printer with the DeviceID \\atl-ps-01\color-printer. Note that because we are using the DeviceID as part of a Where clause (Where DeviceID =) we need to “escape” each \ in the printer path. Consequently we need to use this as the DeviceID:

\\\\atl-ps-01\\color-printer

Just another one of the little eccentricities we’ve come to love about WMI: any time you use a \ in a Where clause you must preface it with a second slash. If you have back-to-back \\’s like we do here, you must preface each slash with a second slash, yielding this crazy-looking construct: \\\\. But, rules are rules, right?

After binding to the printer (and our collection will consist of only one printer, because DeviceIDs must be unique) we call the PrintTestPage method, capturing the return value in a variable named errReturn. If errReturn is 0 that means the test page printed successfully; if errReturn is anything but 0 then something went wrong and the test page could not be printed. Either way we echo the appropriate message to the screen.

Like we said, pretty easy. What makes this especially nice is : 1) because this is WMI, we can print test pages from remote machines just as easily as we can from the local machine; and, 2) the return value will tell us whether or not the print test succeeded. No need to walk all the way to the printer only to find out that, for some reason, the print test failed.

Of course, that’s all well and good if you’re running Windows XP. But what if you aren’t, what if you’re still running Windows 2000? Well, in that case the news isn’t quite so good; we don’t know of any “pure” scripting way to print a test page, at least not any way built into the operating system. However, you can print a test page from the command prompt using this command:

rundll32 printui.dll,PrintUIEntry /k /n "\\atl-ps-01\color-printer"

If you wanted to, you could wrap this command up in a VBScript and execute the test page from there. For more information about running command line tools from within a script take a gander at this classic Tales from the Script column (it’s actually the first Tales ever written).


How Can I Print Text Files to a Printer Other than the Default Printer?

Hey, Scripting Guy! How can I print text files to a printer other than the default printer?

-- JD

Hey, JD. Boy, talk about pride going before the fall. When we first set out to answer this question we already “knew” how to solve the problem. We were well aware that the Shell object provides a way for you to print text files to the default printer; we were also well aware that this was the only printer to which you could print text files. But, being the Scripting Guys and all, we figured we had a clever workaround for this problem: we’d tell everyone to use Microsoft Word to print out the text files. And then we’d just sit back and listen to everyone tell us how smart we were.

Oops. It was only after we started writing up our answer that we actually bothered to check and see if Microsoft Word actually did allow you to choose an alternate printer when printing documents from a script. As it turns out, it doesn’t: Word requires you to print to the default printer as well. As painful as it is to admit this, the Scripting Guys were wrong.

Note. OK, true: we’re wrong lots of times. But that doesn’t make it any less painful.

As you might expect, this then became a matter of pride: we had to find a way to print a text file to a different printer. Unfortunately, we couldn’t find such a way. Therefore, we opted to go with Plan B: if we can print only to the default printer, well, then we’ll just change the default printer, print the file, and then change back. Here’s a solution that will work on Windows XP and Windows Server 2003:

strComputer = "."

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

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Default = TRUE")

For Each objPrinter in colPrinters
    strOldDefault = objPrinter.Name
    strOldDefault = Replace(strOldDefault, "\", "\\")
Next

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer2'")

For Each objPrinter in colPrinters
    objPrinter.SetDefaultPrinter()
Next

Wscript.Sleep 2000

TargetFolder = "C:\Logs"
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(TargetFolder)
Set colItems = objFolder.Items
For Each objItem in colItems
    objItem.InvokeVerbEx("Print")
Next

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = '" & strOldDefault & "'")

For Each objPrinter in colPrinters
    objPrinter.SetDefaultPrinter()
Next

Note. Why only Windows XP and Windows Server 2003? Well, in prior versions of Windows it’s not real easy to identify – and change – the default printer; that’s because the WMI class Win32_Printer has neither a Default property nor a SetDefaultPrinter() method. Therefore, we decided to go with the XP/2003 solution for today; if there’s enough interest in doing a Windows 2000 version, well, we’ll see what we can come up with.

Admittedly, this script might seem a tiny bit complicated; bear with us, though, and you’ll see what we’re doing and why. We begin by connecting to the WMI service on the local computer and then retrieving the Name of the default printer. That’s what we do here:

strComputer = "."

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

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Default = TRUE")

For Each objPrinter in colPrinters
    strOldDefault = objPrinter.Name
    strOldDefault = Replace(strOldDefault, "\", "\\")
Next

The first part of this code snippet should be self-explanatory; the only semi-tricky part lies in our WQL query. We want information only about the default printer so we construct a query that returns data for printers where the Default property is equal to True. Because there can be only one default printer on a computer, this query should return just one printer.

The second part of the snippet – the For Each loop – might take a little bit more explaining. In the For Each loop we retrieve the Name of the default printer (remember, that’s the only printer in the collection) and store it in a variable named strOldDefault. So far so good. We then have this line of code:

strOldDefault = Replace(strOldDefault, "\", "\\")

Why? Well, if the default printer happens to be a network printer it’s going to have a name like this: \\atl-ps-01\printer1. That’s fine, except that later on in the script we need to use that printer name in a WQL Where clause. Because the \ is a reserved character, we have to “escape” each \ character by doubling it; in other words, our printer – for purposes the Where clause – must have the name \\\\atl-ps-01\\printer1. We use the VBScript Replace function to replace any \ in the printer name with a pair of \’s.

Got that? Good. Next we have this block of code:

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer2'")

For Each objPrinter in colPrinters
    objPrinter.SetDefaultPrinter()
Next

Wscript.Sleep 2000

Here we’re requesting a new collection, this one consisting of all printers with the name \\\\atl-ps-01\\printer2 (note the doubled-up \’s); as you might have guessed, this is the printer we want to print to. We retrieve this collection (which, again, will have only one member), then use the SetDefaultPrinter() method to make \\atl-ps-01\printer2 the default printer. We then pause the script for two seconds, just to make sure the change is made.

Now that we have a new default printer we can go ahead and print our text file. We’ve taken this code directly from a previous Hey, Scripting Guy! column, the one on printing text files. We won’t talk about the code here; see the column on printing text files if you need more information:

TargetFolder = "C:\Logs"
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(TargetFolder)
Set colItems = objFolder.Items
For Each objItem in colItems
    objItem.InvokeVerbEx("Print")
Next

So what have we done so far? Well, we’ve determined the name of the default printer (for this discussion we’ll assume it’s \\atl-ps-01\printer1) and stored it in a variable named strOldDefault. We’ve changed the default printer to \\atl-ps-01\printer2 and printed our text files. Now all we have to do is restore the old default printer, something we do with this block of code:

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = '" & strOldDefault & "'")

For Each objPrinter in colPrinters
    objPrinter.SetDefaultPrinter()
Next

As you can see, here we connect to the old default printer; that is, we request a collection consisting of all the printers that have a Name property equal to the value stored in the variable strOldDefault. As soon as we get that collection we then use the SetDefaultPrinter() method to reset \\atl-ps-01\printer1 as the default printer. The net result: we printed our text files to printer2, yet printer1 remains the default printer. It might not be a particularly clever solution, but it works.

Besides, we’ve learned our lesson about trying to be too clever. We promise never to try and do anything smart again (which is one promise we can probably keep).


How Can I Purge a Print Queue on a Print Server?

Hey, Scripting Guy! How can I purge a print queue on a print server? I need to do this on both Windows 2000 and Windows Server 2003.

-- KD

Hey, KD. And hey: no fair! Lots of times we take the easy way out in this column: we provide an answer that works only on Windows XP or Windows Server 2003, and then say, “If there’s enough interest we’ll see if we can find a way to do this on Windows 2000 as well.” If there’s not enough interest, well, that just means we didn’t have to work very hard on that particular column.

But you’re cheating: you specifically mentioned both Windows 2000 and Windows Server 2003. That makes it a bit harder for us to get away with the old “If there’s enough interest” ploy. Pretty clever, KD. Pretty clever.

But one thing about the Scripting Guys: we always respond to a challenge. (We don’t always respond well to a challenge, but we do respond.) You say you need to purge a print queue on Windows 2000. Okey-doke; here’s an ADSI script that does just that:

Set objPrinter = GetObject("WinNT://atl-ps-01/ArtDepartmentPrinter")
objPrinter.Purge

No, we’re not taking shortcuts here: that is the entire script. In the first line, we bind to the printer ArtDepartmentPrinter on the print server atl-ps-01. And in the second line we call the Purge method to delete all the print jobs in the queue. Just that easy, just that quick.

In other words, one down (Windows 2000) and one to go (Windows Server 2003). Now, technically, we don’t have to show you another script; after all, the preceding script also works on Windows Server 2003. But just to prove that we aren’t completely lazy, here’s a second script, one that uses WMI but runs only on Windows Server 2003 and Windows XP. Let’s look at and explain the code, then talk about why you might want to use this script to manage print queues:

strComputer = "atl-ps-01"
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colInstalledPrinters = objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = 'ArtDepartmentPrinter'")

For Each objPrinter in colInstalledPrinters
    objPrinter.CancelAllJobs()
Next

In this script we start off by connecting to the WMI service on the remote computer atl-ps-01. We then use this line of code to return a collection consisting of all the printers named ArtDepartmentPrinter (a collection that will have, at most, a single item, seeing as how printer names must be unique on a print server):

Set colInstalledPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Name = 'ArtDepartmentPrinter'")

From there we create a For Each loop to loop through the collection, calling the CancelAllJobs() method to purge all the print jobs for each print queue in the collection.

That’s pretty straightforward, but the first script we showed you is not only shorter but it runs on Windows 2000 as well. So why would ever you want to use the WMI version? Well, if all you’ll ever need to do is purge one specific print queue then we can’t really think of a good reason; go ahead and use the first script. The value of the second script can be found in WMI’s ability to manage printers and print queues, capabilities (found only in Windows XP and Windows Server 2003) that far exceed ADSI’s ability to manage printers and print queues.

For example, suppose you want to purge only print queues containing x number of print jobs or only print queues on your color printers. Those tasks are far easier to do using WMI than using ADSI. And what if you want to install (or uninstall) printers, printer drivers, or printer ports? You can do that with WMI; you can’t do that with ADSI. Thus WMI scripting offers a lot of long-term advantages, even if, in the short-term, and for a very specific task, ADSI might seem better.

And that’s why we recommend WMI for doing most system administration tasks, leaving ADSI for chores involving users and groups and stuff like that. You can take that basic WMI script and modify it to carry out all sorts of tasks; that isn’t necessarily true with the ADSI version.


How Can I Receive Notification Any Time Something is Printed to a Particular Printer?

Hey, Scripting Guy! How can I receive notification any time something is printed to a particular printer?

-- IL

Hey, IL. You know, one of the Scripting Guys once had a summer job in an organization where the only decent printer in the entire building was kept in the administrative assistant’s office. We can attest, from painful experience, that it didn’t take much effort to receive a … notification … any time you printed something to that printer.

Of course, that kind of notification involves a good deal of yelling and screaming. Fortunately, there’s a more peaceful and civilized way to receive notification any time something is printed to a particular printer. In fact, all you need is a WMI event notification script similar to this:

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

Set colPrintJobs = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceCreationEvent " _
        & "Within 10 Where TargetInstance ISA 'Win32_PrintJob'")

Do While True
    Set objPrintJob = colPrintJobs.NextEvent
    If InStr(objPrintJob.TargetInstance.Name, "atl-color-printer-1") Then
        Wscript.Echo objPrintJob.TargetInstance.Document, Now
    End If
Loop

This quieter, gentler form of notification starts off by connecting to the WMI service on the local computer. Note that you easily modify this script to connect to the WMI service on a remote computer: just assign the variable strComputer the name of that computer. Note, too that you do not have to connect to the print server; this script works on any computer that has a connection to the printer being monitored (in this case, a printer named atl-color-printer).

After making the connection to the WMI service we then run this line of code:

Set colPrintJobs = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceCreationEvent " _
        & "Within 10 Where TargetInstance ISA 'Win32_PrintJob'")

This is the actual event notification query: we’re asking WMI to inform us any time a new instance of the Win32_PrintJob class is created. Why? Well, each time someone prints a new document a new instance of the Win32_PrintJob class is created; by receiving notification each time a new Win32_PrintJob instance is created we’ll know that someone is printing a document.

Note. What does the Within 10 mean? That simply means that we’re going to check every 10 seconds to see if there are any new print jobs; if there aren’t, then the script will go into temporary hibernation and re-emerge 10 seconds later in order to make another check. If you aren’t sure what terminology like Within 10, TargetInstance, and ISA means, then you might want to check out the Scripting Guys Scripting Week 2 webcast on WMI events.

We should clarify one thing here: if all we have is the event notification query then our script will be informed any time a document is printed; however, we won’t be informed. If we want to know what’s going on (and, obviously, we do), we have to add in code that will send us a notification of some type. For this example script we’re going to use a very simple form of notification: we’ll just echo back the name of the document being printed and the time that printing began. Here’s what that code looks like:

Do While True
    Set objPrintJob = colPrintJobs.NextEvent
    If InStr(objPrintJob.TargetInstance.Name, "atl-color-printer-1") Then
        Wscript.Echo objPrintJob.TargetInstance.Document, Now
    End If
Loop

What we do here is set up a Do Loop that will run forever (Do While True basically means that the loop should continue to run as long as True is equal to True). Why do we do that? Well, we’re assuming you want to constantly monitor the printer and thus receive a notification each and every time a document is printed. The easiest way to do that is to put the notification code inside an endless loop.

Next we have this line of code:

Set objPrintJob = colPrintJobs.NextEvent

When the script encounters this line of code it stops dead in its tracks and waits for the NextEvent to occur. The event we are awaiting, of course, is the creation of a new instance of the Win32_PrintJob class. If no documents are ever printed (and thus no new instances of this class created) the script will simply sit on this line forever, dutifully waiting for the next event to take place.

So what happens when this next event does occur and someone does print a document? Well, the first thing we do is check to see if the document is being printed on the target printer:

If InStr(objPrintJob.TargetInstance.Name, "atl-color-printer-1") Then

We use the InStr function here because the print job Name (at least on network printers) is likely to look like this: \\\\atl-ps-01\\atl-color-printer-1, 226 (a big, long string that represents the network path to the printer plus the job ID –226 – assigned to the individual print job). All we’re doing is checking to see if the string atl-color-printer-1 appears anywhere within the print job Name. If it does, then that means the document is being printed to the printer atl-color-printer-1.

Note. If you only have one printer connection on the computer where the script is running, then you can skip this If-Then statement. After all, in a case like that there’s only one possible printer that the document could be printed to.

If the InStr function returns true (meaning the string atl-color-printer-1 was found), then we echo back the name of the document and the time that printing began:

Wscript.Echo objPrintJob.TargetInstance.Document, Now

And that’s it: we then loop around and wait for the next event. If we ever decide to stop monitoring all we need to do is terminate the process (either Cscript.exe or Wscript.exe) that the script is running in.

And, yes, instead of echoing back the fact that a document was being printed we could have written the script so that it plays a .WAV file that yells at you. But you’re on your own for that one. You know what they say: been there, done that. Our ears are still ringing, even after all these years.


How Can I Remove All the Black-and-White Printers on a Computer?

Hey, Scripting Guy! On a Windows XP computer, how can I remove all the black-and-white printers, leaving only color printers?

-- JL

Hey, JL. You know, in one classic episode of the Simpsons, Bart, in the throes of a sugar high, joins the Junior Campers. The next morning, realizing what he has done, he vows to weasel out of his commitment. His mother tries to stop him, but Homer interrupts: “Learning how to weasel out of things is important,” he says. “It’s what separates us from the animals … except the weasel, of course.”

As you might have guessed, the Scripting Guys are about to try and separate ourselves from the animals. Not that we’re going to totally weasel out of answering the question; heaven forbid! It’s just that we’re going to stick very close to the actual wording of the question, and only explain how to do this on Windows XP and Windows Server 2003.

Why are the Scripting Guys – of all people – suddenly sticklers for protocol? Well, as it turns out, removing just the black-and-white printers on a Windows XP or Windows 2003 computer is pretty easy. That’s not the case on, say, Windows 2000. On that version of Windows you can delete printers on the local computer, but you can’t (at least not easily) delete printers on remote computers. Trying to do things remotely that you can’t really do remotely always leads to far too many problems for us to address in this column. Therefore, we’ll just answer the question at hand – how do I do this on Windows XP? – and pretend we never even thought about how (or if) this might work on Windows 2000.

Note. OK, OK. Let us know if you need to perform this task on Windows 2000; if there’s enough interest we’ll reconsider our weasel-like behavior.

With that in mind, here’s the answer to your question, JL:

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * from Win32_PrinterConfiguration Where Color = 1")

For Each objPrinter in colPrinters
    strName = objPrinter.Name
    strName = Replace(strName, "\", "\\")

    Set colBWPrinters = objWMIService.ExecQuery _
        ("Select * from Win32_Printer Where Name = '" & strName & "'")
    For Each objBWPrinter in colBWPrinters
        objBWSPrinter.Delete_
    Next
Next

As you can see, after all that buildup there really isn’t all that much to the script. We start off by binding to the WMI service on the local computer, although – as hinted at above – we could just as easily perform this task on a remote computer. We then use this line of code to connect to the Win32_PrinterConfiguration class and retrieve a collection of all the printers on the computer that do not print color:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * from Win32_PrinterConfiguration Where Color = 1")

That might sound a bit backwards, but, remember, we want to keep all the color printers. The easiest way to do that is to grab a collection of all the black-and-white printers and then delete each printer in the collection. Do that, and, when you’re done, all you’ll have left are color printers.

We’re willing to bet your average weasel would never have thought of a solution like that.

Hey, we said your average weasel.

As you probably already figured out, if the value of the Color property is 1 then the printer is a black-and-white printer. If the value is 2, then you have a color printer. Those, by the way, are the only possible values for the Color property

After we get back our collection we then set up a For Each loop to walk through all the items in the collection. For the first item (that is, our first black-and-white printer) we use this line of code to retrieve the printer name and store it in a variable named strName:

strName = objPrinter.Name

That brings us to this line of code:

strName = Replace(strName, "\", "\\")

What we’re doing here is replacing any \ characters in the printer name with \\. Why the heck do we do that? Well, if we have a network printer it’s going to have a name like this: \\atl-ps-01\finance_printer. That’s fine, but we need to use the printer name in a WQL query. Because the \ is a reserved character in WQL we need to “escape it” (double it up). In other words, in our query the printer name needs to look like this: \\\\atl-ps-01\\finance_printer. That’s what we use the Replace function for: it replaces (escapes) each instance of the \ with \\.

Got that? Good. Now, as it turns out, the Win32_PrinterConfiguration class can tell us whether or not a printer can print in color. What it can’t do, however, is delete a printer. To actually get rid of the black-and-white printers we need to connect to the Win32_Printer class, which is what we do here:

Set colBWPrinters = objWMIService.ExecQuery _
        ("Select * from Win32_Printer Where Name = '" & strName & "'")

Notice that we use a Where clause here, indicating that we only want to retrieve those printers where the Name is equal to the variable strName. (And, remember, we just doubled up all the \ characters in strName.) Because printer names must be unique on a computer this ensures that we will get back a collection consisting of just one printer.

And what do we do with that collection? That’s easy. We set up a For Each loop to walk through it, something we have to do even though there’s only one item in the collection. Inside that For Each loop we call the Delete_ method to delete the printer:

objBWSPrinter.Delete_

Note. Incidentally, this is why our script won’t work on Windows 2000: on versions of Windows prior to Windows XP, the Win32_Printer class does not support the Delete_ method.

That single line of code deletes our first black-and-white printer. We then loop around to our original For Each loop and repeat the process with the second printer in the collection. This continues until all the black-and-white printers have been deleted.

And that’s the kind of thing that separates the Scripting Guys from the animals … except the Australian scripting badger, of course. But other than that ….


How Can I Remove All the Local Printers from a Computer?

Hey, Scripting Guy! How can I remove all the local printers from a computer?

-- JW

Hey, JW. You did say that you wanted to do this only on computers running Windows XP or Windows Server 2003, didn’t you? You didn’t? Oh. Well, we’re going to pretend that you did, because this is a very easy task to carry out on Windows XP and Windows Server 2003. That’s because on Windows XP and Windows Server 2003 the WMI class Win32_Printer not only can easily distinguish between local printers and network printers, but it also supports the Delete_ method. As you can probably guess, that method enables you to delete printer connections. In other words, WMI lets you pick out and remove local printers. Which, all in all, sounds exactly like the answer to your question.

That’s the good news. The bad news is that, on earlier versions of Windows, it’s more complicated to pick out local printers; even worse, the Win32_Printer class doesn’t support the Delete_ method. That means it’s far more challenging to identify and remove local printers. Because of that we’re going to focus on removing local printers from Windows XP and Windows Server 2003 computers and hope that takes care of your problem. If not, we’ll have to tackle printer removal on Windows 2000 computers at a later date.

For Windows XP and Windows Server 2003 users you can delete all the local printers using a script as simple as this:

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

Set colInstalledPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Network = FALSE")

For Each objPrinter in colInstalledPrinters
    objPrinter.Delete_
Next

This script begins by connecting to the WMI service on the local computer (although it can be easily modified to remove printers from a remote computer instead). We then use this line of code to return a collection of all the local printers; as far as WMI is concerned, a printer is a local printer if the Network property is set to False:

Set colInstalledPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Network = FALSE")

Once we have a collection of local printers we simply set up a For Each loop to walk through that collection, calling the Delete_ method (note the underscore on the end) for each printer in the collection:

For Each objPrinter in colInstalledPrinters
    objPrinter.Delete_
Next

There you have it: that will delete all the local printers, while leaving the network printer connections alone. Now aren’t you glad that you asked about removing local printers specifically from Windows XP and Windows Server 2003 computers without giving any thought whatsoever to earlier versions of the operating system?

OK, fine: we’ll see what we can do about addressing this issue on Windows 2000 computers. We won’t make any promises, but we’ll look into it.


How Can I Retrieve Information About the Printer Driver Used by a Printer?

Hey, Scripting Guy! How can I retrieve information about the printer driver used by a specific printer?

-- RW

Hey, RW. Did you say something? Oh, right: printer drivers. You’ll have to excuse us today: the Scripting Guys (well, one of them anyway) are in mourning. That’s because the Colt League All-Star team he helped coached finished third in the Northwest Regional Tournament. What’s wrong with finishing third in the Northwest region? Well, nothing…except that the team that finished first gets to go to California and play in the Zone tournament, with the winner there going on to the Colt League World series. Everyone else – including the team that finished third – gets to go home.

Anyway, thanks for asking: given time, we’ll recover and get back to our usual rollicking selves. Try us again in a month or two. Or maybe next year sometime. Perhaps by 2008 we’ll be ready to answer questions again. Until then….

Um, never mind: our manager just informed us that we’ve made a remarkable recovery and are ready to answer questions now. (Obviously he doesn’t understand how important Colt League baseball is.) So we’ll answer your question. And then we’ll go back into mourning.

Because this is a printing-related question we have the usual caveat: we can show you a solution that works great on Windows XP and Windows Server 2003, but won’t work on Windows 2000 or any other version of Windows. On those other platforms you can use a script like this one to return the name of the print driver used by a specific printer, but that’s pretty much all the information you can get:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID='\\\\atl-ps-1\\printer1' ")

For Each objItem in colItems
    Wscript.Echo objItem.Name
    Wscript.Echo objItem.DriverName
Next

Better than nothing, we suppose, but the name alone doesn’t exactly qualify as “details” about the printer driver. If you want detailed information like that, you need to use a script like this one, a script that runs only on Windows XP or Windows Server 2003:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Associators Of {Win32_Printer.DeviceID='\\atl-ps-01\printer1'} " _
        & "WHERE AssocClass = Win32_DriverForDevice Role=Antecedent")

For Each objItem in colItems
    Wscript.Echo objItem.Name
    Wscript.Echo objItem.DriverPath
Next

What we’re doing here is taking advantage of two new classes that were added to Windows XP and Windows Server 2003. Although you don’t see it explicitly mentioned in the script, we’re using the new Win32_PrinterDriver class to retrieve information about the printer driver; the properties Name and DriverPath (both echoed in our For Each loop) are properties of the Win32_PrinterDriver class.

The second class – which is explicitly spelled-out in the script – is the Win32_DriverForDevice class. This is an association class which, as the name implies, makes an association between two classes; in this case, Win32_DriverForDevice makes an association between a printer in the Win32_Printer class and a printer driver in the Win32_PrinterDriver class. To make this association we need to use an Associators Of query like this one:

Set colItems = objWMIService.ExecQuery _
    ("Associators Of {Win32_Printer.DeviceID='\\atl-ps-01\printer1'} " _
        & "WHERE AssocClass = Win32_DriverForDevice Role=Antecedent")

In plain English (not exactly a Scripting Guys specialty!) this query says, “For all the printers with the DeviceID \\atl-ps-01\printer1 use the Win32_DriverForDevice class to retrieve all the printer drivers associated with that printer.”

As usual, the query returns a collection, in this case a collection of all the printer drivers associated with the printer \\atl-ps-01\printer1. In our For Each loop we iterate through the collection and echo back the Name and DriverPath properties. Problem solved.

That much you could probably figure out for yourself; the query’s a bit odd-looking, but fairly intuitive. Except maybe for one thing: what does Role=Antecedent mean? Well, in the wonderful world of WMI an antecedent object is the independent object in a relationship; in other words, you can have all kinds of print drivers, none of which are dependent on a particular printer. However, a printer can have only one print driver, which means the printer plays a Dependent role in the relationship. (Yes, it does sound like an episode of Desperate Housewives, doesn’t it?) That means with a few tweaks we can flip this script inside-out and have it query the Win32_PrinterDriver class to return the names of all the printers that use a specified driver:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Associators Of {Win32_PrinterDriver.Name='Xerox Document Centre 255 PS,3,Windows NT x86'} " _
        & "WHERE AssocClass = Win32_DriverForDevice Role=Dependent")

For Each objItem in colItems
    Wscript.Echo objItem.DeviceID
Next

Give it a try and see what happens.

Now, if you’ll excuse us, we have…work…to do….

(In case you’re wondering, there’s a reason why there aren’t any books titled The Third-Best and the Third-Brightest: third-place isn’t anywhere near as much fun as it might sound!)


How Can I Set the Default Printer Based on the IP Address of the Computer?

Hey, Scripting Guy! How can I set the default printer based on the IP address of the computer?

-- TT

Hey, TT. You’ll have to forgive us for being a little distracted this morning. Last night the Scripting Guy who writes this column went to the gym with his 16-year-old son. For the first time ever, the Scripting Son was able to bench press more than the Scripting Dad. Sure, the Scripting Dad can still beat his son in wrestling, but as the Scripting Son pointed out, “That’s just because you know all those moves. If I keep getting bigger and stronger, those moves won’t help you: I’ll just pick you up and crush you.”

And he will, too.

What that means, of course, is that the days of the Scripting Dad wrestling the Scripting Son - or at least wrestling him and winning - are numbered. Therefore, the Scripting Dad is beginning to look for new things to wrestle, things like, well, wrestling with the problem of changing the default printer based on the IP address of the computer.

Note. OK, so maybe that doesn’t make for the most exciting wrestling match ever held. But right now the Scripting Dad isn’t looking for a challenge, he’s looking for something he can beat.

As you noted in your email, TT, you have several different subnets, and you’d like to set the default printer based on the second octet of those subnets. For example, if a computer is on subnet 192.1.x.x the user would be assigned one default printer; if the computer is on subnet 192.2.x.x the user would be assigned a different default printer.

Sounds hard, doesn’t it? In truth though, it’s pretty easy, at least for those dads who are stronger than their sons.

Fortunately, it wasn’t too hard for the rest of us, either:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled = True")

For Each objItem in colItems
    strIPAddress = objItem.IPAddress(0)
    arrIPAddress = Split(strIPAddress, ".")

    If arrIPAddress(0) = "192" Then

    Select Case arrIPAddress(1)
        Case 1
            Set colInstalledPrinters =  objWMIService.ExecQuery _
                ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer1'")
            For Each objPrinter in colInstalledPrinters
                objPrinter.SetDefaultPrinter()
                Exit For
            Next
        Case 2
            Set colInstalledPrinters =  objWMIService.ExecQuery _
                ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer2'")
            For Each objPrinter in colInstalledPrinters
                objPrinter.SetDefaultPrinter()
                Exit For
            Next
        End Select
    End If
Next

A few caveats here. First, we’re assuming that all the requisite printers have been preinstalled on the computers; if that’s not the case, you’ll have to modify the code to check for the existence of a particular printer and, if it can’t be found, install it. Second, we’ve used WMI to configure the default printer. The good news? That gives us a script that can run locally (perhaps as a logon script) or that we can run remotely. The bad news? That means the script shown above works only on computers running Windows XP or Windows Server 2003.

Note. Is there a workaround for those of you still using Windows 2000? You bet there is: you can use Windows Script Host 5.6 and its SetDefaultPrinter method to achieve the same effect. The only drawback to that approach is that the script must run locally; of course, if you are running this as a logon script then that’s a given anyway. For more information, see the Microsoft Windows 2000 Scripting Guide.

Tell you what: if you can’t figure out how to get this to work on Windows 2000 let us know, and we’ll see what we can do to help you out.

Now, where were we? Oh, yeah: the script. As you can see, we start out in pretty humdrum fashion; we simply use these two lines of code to connect to the WMI service on the local computer:

strComputer = "."

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

After we make the connection, we then issue this query, which returns a collection of the IP-enabled network adapters found on the computer:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_NetworkAdapterConfiguration Where IPEnabled = True")

Incidentally, the Where clause - Where IPEnabled = True - is important: it helps us filter out all the “network adapters” that don’t have (or can’t get) IP addresses. That might sound silly, but WMI considers all sorts of things (like virtual machine drivers and packet schedulers) to be network adapters. It’s a good idea to get in the habit of using IPEnabled = True to help zero in on your real network adapters. (The script will actually work just fine without this Where clause, but ….)

Once we have the collection we then set up a For Each loop to walk through all the network adapters in that collection. What we do inside that loop is actually pretty easy, but it does require a bit of explanation.

To begin with, we use this line of code to get the first IP address assigned to the first network adapter in the collection:

strIPAddress = objItem.IPAddress(0)

Granted, a single network adapter can potentially have more than one IP address. We’re assuming that if you’re mapping printers as part of a logon script then you’re probably dealing with client workstations; if that’s the case, then you don’t need to worry about “multi-homed” adapters. We’re taking a calculated risk that most (if not all) of your network cards will have just one IP address, so we take a shortcut here and grab just that first IP address.

So let’s say we now have the IP address 192.2.168.1. We’re interested in only the second octet; in this case 2. How do we isolate that value? Well, to begin with, we use the Split function to split the IP address into an array:

arrIPAddress = Split(strIPAddress, ".")

By splitting the IP address on the period, we end up with an array consisting of (in this example) the following items:

· 192

· 2

· 168

· 1

Our next step is to verify that the first octet (or item 0 in our array) is equal to 192:

If arrIPAddress(0) = "192" Then

Depending on your network setup this might not be a concern; that is, maybe all your IP addresses start with 192. We tossed this in just to be on the safe side.

We then use a Select Case statement that examines the value of the second octet (item 1 in our array):

Select Case arrIPAddress(1)

We then have a series of Case statements that check for the possible values of this second octet. For example, because our second octet is 2, this Case statement applies to us:

Case 2
    Set colInstalledPrinters =  objWMIService.ExecQuery _
       ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer2'")
        For Each objPrinter in colInstalledPrinters
            objPrinter.SetDefaultPrinter()
            Exit For
        Next

As you can see, what we’re doing here is using a WMI query to select all the printers that have the Name \\atl-ps-01\printer2; because printer names must be unique, that returns a collection consisting of, at most, one printer. And, no, we didn’t fall asleep with our finger on the \ key when we typed the printer name like this:

\\\\atl-ps-01\\printer2

As you probably know, the \ is a reserved character in WMI; that means that any time we use the \ in a Where clause we have to “escape” the character by prefacing it with another \. In other words, we have to double up each \; that means that the two \\’s found at the beginning of the string end up being four \\\\’s.

Crazy, huh?

Next we set up a For Each loop to walk through all the items in that collection; inside that loop we then use the SetDefaultPrinter method to make \\atl-ps-01\printer2 the default printer:

objPrinter.SetDefaultPrinter()

After that we exit the For Each loop. Why? Well, we’ve already assigned a default printer; there’s probably no reason to assign that default printer all over again (assuming that there’s another network adapter with an IP address starting with 192). Of course, if there is a reason to assign the default printer all over again then you can simply remove the Exit For statement.

You’ll need to set up similar Case statements (or some kind of function) to account for all the other possible values of that second octet. To do that, simply specify the octet value as part of the case statement (e.g., Case 3) and then include code that assigns the relevant printer for that subnet. All in all, it’s easy, way easier than the bench press. (Which, when you come to think of it, is kind of silly: since when does being able to lift heavy weights have anything to do with how strong you are?)

Hope that helps, TT. As for us, we have to go home and challenge the Scripting Son to a wrestling match. We need to try to get in as many victories over him as we can; after all, we have a sinking feeling that as soon as he beats us one time we’ll never be able to beat him again.

Oh: and here’s a version of this script that works on Windows 2000. Now that we can no longer count on beating people up we figured we’d better start being nice instead:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled = True")

Set objNetwork = CreateObject("Wscript.Network")

For Each objItem in colItems
    strIPAddress = objItem.IPAddress(0)
    arrIPAddress = Split(strIPAddress, ".")

    If arrIPAddress(0) = "192" Then

    Select Case arrIPAddress(1)
        Case 1
            Set colInstalledPrinters =  objWMIService.ExecQuery _
                ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer1'")
            For Each objPrinter in colInstalledPrinters
                objNetwork.SetDefaultPrinter objPrinter.Name
                Exit For
            Next
        Case 2
            Set colInstalledPrinters =  objWMIService.ExecQuery _
                ("Select * from Win32_Printer Where Name = '\\\\atl-ps-01\\printer2'")
            For Each objPrinter in colInstalledPrinters
                objNetwork.SetDefaultPrinter objPrinter.Name
                Exit For
            Next
        End Select
    End If
Next


How Can I Share All the Local Printers on a Computer?

Hey, Scripting Guy! How can I write a script that will automatically share all the local printers on a computer?

-- CD

Hey, CD. You know, when we redid the Hey, Scripting Guy! archive we created a new category devoted exclusively to printing. When we started to fill out the archive pages, however, we discovered – much to our surprise – that we hadn’t really answered that many printing-related questions. We thought we’d been answering a bunch of printing questions, but obviously we’d simply been hiding those questions in the closet and hoping that they’d go away.

Why have we been so reluctant to answer printing questions? After all, isn’t it really easy to manage printers in Windows XP or Windows Server 2003?

As a matter of fact, it is; the enhancements made to the WMI printing classes represent one of the real strengths of scripting in Windows XP and Windows 2003. The problem is that the vast majority of these enhancements represent capabilities not found in Windows 2000; to be honest, print management in Windows 2000 isn’t anything to write home about. (Or to write daily scripting columns about.) The solutions to most printing questions are XP/2003-specific: they simply don’t apply to Windows 2000. Because we prefer to offer generic solutions that work on most Windows platforms, we kind of hurry past printing questions and hope that no one notices.

But what the heck; we’ll go ahead and answer this one. Just keep in mind that, if you’re running Windows 2000, today’s column doesn’t apply to you. This is XP/2003 only. And, yes, we know that’s not fair. But that’s just the way it is.

Here’s a script that will share out all the local printers on a computer:

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

Set colInstalledPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Network = FALSE")

i = 1

For Each objPrinter in colInstalledPrinters
    objPrinter.Shared = TRUE
    objPrinter.ShareName = "Printer" & i
    objPrinter.Put_
    i = i + 1
Next

The script begins by connecting to the WMI service, then issues this query to return a collection of all the local printers; that is a collection of all the printers that are connected to a local printer port:

Set colInstalledPrinters =  objWMIService.ExecQuery _
    ("Select * from Win32_Printer Where Network = FALSE")

After issuing the query we set the value of a counter variable named i to 1; we’ll use this variable later on to give each of the local printers a unique share name. We then set up a For Each loop to loop through our collection of local printers.

And what do we do inside that loop? Well, for each printer we start off by setting the value of the Shared property to True; this will cause the printer to be shared. The Shared property, by the way, is a new addition to the Win32_Printer class; this property does not appear in the version of WMI found in Windows NT or Windows 2000, which explains why this script will not work on those platforms.

Because each shared printer requires a unique share name, we use this line of code to set the value of the ShareName property to the word Printer plus the value of our counter variable:

objPrinter.ShareName = "Printer" & i

The first time through the loop i is equal to 1, hence the ShareName will be set to Printer1. We then call the Put_ method to actually write the changes to the printer. Don’t leave out this line of code; if you do, your changes won’t be saved, and the printer will not be shared. We then increment the value of i by 1, and loop around again. That means the next printer in the collection will have the share name Printer2, the one after that will have the share name Printer3, and so on.

Like we said, this script works great, provided you’re running Windows XP or Windows Server 2003. If you’re running some other version of Windows – oh, look we’re out of time for today. Gotta go; bye!


How Can I Tell Which Users are Connected to a Print Queue?

Hey, Scripting Guy! How can I tell which users are connected to a print queue?

-- RE

Hey, RE. You know, if your printers are anything like the ones the Scripting Guys are used to working with then it’s easy to tell which users are connected to a print queue: all you have to do is walk into the printer room and see who’s in there frantically looking in every nook and cranny for the document that the printer insists it printed. At Microsoft the paperless office is very close to a reality. Not because we consciously set out to save paper, mind you, but because you typically have a better chance of hitting the jackpot in Las Vegas than you do of getting a document to actually print.

If you have printers that are a tad bit more reliable than that, however, you might find it easier to use a script to determine which users are currently printing to a particular printer. There’s a bit of a trick to doing this, and you have to use a slightly different approach to carry out this task on any version of Windows prior to Windows XP. But we’ll explain all that in a moment. For now, let’s take a look at a script that works on Windows XP and Windows Server 2003:

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

Set colPrintJobs =  objWMIService.ExecQuery _
    ("Select * From Win32_PrintJob Where Name Like '%FinancePrinter%'")

For Each objPrintJob in colPrintJobs
    Wscript.Echo objPrintJob.Owner
Next

As you can see, there’s not much to the script. It begins by connecting to the WMI service on the local computer (although the script could just as easily run against a remote machine). Here’s where it gets a little tricky.

As it turns out, the WMI class Win32_PrintJob can return the logon name of all the users currently printing to all the printers connected to a computer. Interestingly enough, however, Win32_PrintJob does not have a class that pinpoints which print queue a user is connected to. At best, you can identify the printer using the Name property. Values for this property are composed of two items: the name of the printer and a print job ID. For example, the print jobs currently in the queue for the printer FinancePrinter might look like this:

FinancePrinter, 1
FinancePrinter, 2
FinancePrinter, 3

Is that a problem? Yes, it is; after all, it prevents us from using a query like this to return all the print jobs in the FinancePrinter print queue:

Set colPrintJobs =  objWMIService.ExecQuery _
    ("Select * From Win32_PrintJob Where Name = 'FinancePrinter'")

Why can’t we use this query? Well, because there won’t be any print jobs with the Name FinancePrinter; instead, all the print jobs will be named things like FinancePrinter, 1. How … delightful ….

But that’s all right; as usual, we can work around WMI’s idiosyncrasies. On Windows XP and Windows Server 2003 we can use the LIKE operator and a wildcard query, one that looks like this:

Set colPrintJobs =  objWMIService.ExecQuery _
    ("Select * From Win32_PrintJob Where Name Like '%FinancePrinter%'")

Note the construction of our Where clause: Where Name Like ‘%FinancePrinter%’. The percent signs are our wildcard characters: they signify any character or set of characters, much the same way the asterisk does when used with the dir command. The command dir *.* means, “Show me all the files and folders whose names start with any character or set of characters, have a period in it, then end with any character or set of characters.” Our WQL query can be read in similar fashion: “Bring back all the print jobs where the Name property starts with any character or set of characters, has the term FinancePrinter in it, then ends with any character or set of characters.” (And, yes, “any character or set of characters” also means no character at all.) In other words, bring back any print job where the Name includes the string FinancePrinter.

At that point all we have to do is set up a For Each loop and walk through the collection of print jobs, echoing back the value of the Owner property for each one. The Owner property is the logon name of the connected user; for example, kmyer or packerman. That’s the best you can do with Win32_PrintJob; if you need, say, the user’s first and last name, you’ll have to do something else. (One suggestion: take the logon name [also known as the sAMAccountName] and use it in an Active Directory search that can return the user’s first and last name. For more information, see the Tales from the Script column Dude, Where’s My Printer?)

Now that’s all well and good on Windows XP and Windows Server 2003; unfortunately, this script won’t do much for anyone running an earlier version of Windows. That’s because the LIKE operator isn’t available on earlier version of Windows. That doesn’t mean you’re out of luck, it just means we have to do things the old-fashioned way. For example, instead of writing a query that returns only the print jobs being printed to FinancePrinter we need to write a query that returns all the print jobs, regardless of printer:

Set colPrintJobs =  objWMIService.ExecQuery _
    ("Select * From Win32_PrintJob")

Once we have the entire collection of print jobs we can then use the InStr function to determine whether or not the Name property includes the string FinancePrinter:

If Instr(objPrintJob.Name, "FinancePrinter") Then

It’s a little more cumbersome, but it’ll work. Here’s a complete script that returns the list of users currently connected to a Windows 2000 print queue:

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

Set colPrintJobs =  objWMIService.ExecQuery _
    ("Select * From Win32_PrintJob")

For Each objPrintJob in colPrintJobs
    If Instr(objPrintJob.Name, "FinancePrinter") Then
        Wscript.Echo objPrintJob.Owner
    End If
Next

Unfortunately, we don’t have a script that can tell you where printed documents actually go when the printer says they were printed yet the printed pages are nowhere to be found. But we’re working on it.


How Can I Use a Script to Rename a Printer?

Hey, Scripting Guy! How can I use a script to rename a printer?

-- SB

Hey, SB. Well, if your printer is connected to a Windows 2000 or Windows NT 4.0 print server we have bad news for you: you can’t rename a printer using a script, at least not using the technologies included in the operating system. (There might, or might not, be third-party tools that enable you to do this on those older versions of Windows.) If your printer is connected to a computer running Windows XP or Windows Server 2003, however, the news is much better.

That’s because the Win32_Printer class – which was extensively overhauled for Windows XP and Windows Server 2003 – includes a new method named RenamePrinter, a method which definitely lives up to its name. You want to rename a printer using a script? No problem:

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

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'HP'")

For Each objPrinter in colPrinters
    objPrinter.RenamePrinter("ArtDepartmentPrinter")
Next

The script begins by connecting to the WMI service on the local computer. We then use this line of code to retrieve a collection of all the printers named HP that are installed on that computer:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'HP'")

Because printer names must be unique, the returned collection will consist of, at most, a single item. We set up a For Each loop to loop through the collection, and then use this line of code to rename the printer:

objPrinter.RenamePrinter("ArtDepartmentPrinter")

All we do here is call the RenamePrinter method, passing as the sole parameter the new name to be given to the printer. That’s it; because we’re calling a method rather than changing the value of a read/write property we don’t even need to use the Put_ method to save the changes.

As long as you’re changing the name of the printer you might want to change the share name as well. This can be done by changing the printer name, then connecting to the “new” printer and modifying the ShareName property. Note that you can change the share name even if the printer isn’t currently being shared.

Here’s a revised script that changes the printer name and then changes the share name:

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

Set colPrinters =  objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'HP'")

For Each objPrinter in colPrinters
    objPrinter.RenamePrinter("ArtDepartmentPrinter")
Next

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'ArtDepartmentPrinter' ")

For Each objPrinter in colPrinters
    objPrinter.ShareName = "ArtDepartmentPrinter"
    objPrinter.Put_
Next

As you can see, after we change the printer name we then use this line of code to re-connect to the printer, this time using the new name, ArtDepartmentPrinter:

Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where DeviceID = 'ArtDepartmentPrinter' ")

We set up a For Each loop to loop through the collection of printers (again, there will be only one item in that collection). Inside that loop we change the value of the ShareName property and then call the Put_ method to save those changes. (As we implied earlier, any time you change the value of a read/write property you need to call the Put_ method in order to save those changes.) That gives us a printer with a new name, a new share name, and, no doubt, a whole new outlook on life.


Is There Any Way to Determine the Default Printer On a Computer?

Hey, Scripting Guy! Is there any way to determine the default printer on a computer?

-- JW

Hey, JW. If you’re running Windows XP or Windows Server 2003, determining the default printer is easy. That’s because the WMI class Win32_Printer found on these versions of Windows includes a property named Default. As the name implies, this property tells you which printer is the default printer: if the value is TRUE the device is the default printer, if it’s FALSE, well, then it’s not. To find out the default printer on a computer all you have to do is query for the collection of all printers where the Default property is TRUE (because you can have only one default printer, the collection will have only one item):

strComputer = "."

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer Where Default = TRUE")

For Each objPrinter in colPrinters
    Wscript.Echo objPrinter.ShareName
Next

And to think some people still say there’s no reason to upgrade to Windows XP or Windows 2003!

Of course, some of you are a bit worried right now. “They said this was easy if you’re running Windows XP or Windows 2003. They didn’t say anything about Windows 2000. I’m running Windows 2000; does this mean that either I can’t do this at all, or that it’s going to be really hard?”

Hey, relax: you can use WMI to determine the default printer on Windows 2000. And it’s not really hard, it’s just a little tricky. That’s because on Windows 2000 (and Windows NT 4.0 and Windows 98) the Win32_Printer class doesn’t have the Default property. Thus you can’t simply query for printers with a Default value of TRUE.

But that’s OK. The Win32_Printer class found on these older versions of Windows does include a bitmask property named Attributes. We can’t even begin to explain bitmasks today (for more information, see this section of the Microsoft Windows 2000 Scripting Guide), but suffice to say that a single bitmask is similar to a control panel with a bank of switches. Each switch represents, in this case, the property of a printer. If the switch is on, then that property is TRUE. If the switch is off, that property is FALSE. In the case of Win32_Printer, the bit (switch) equal to 4 represents the Default property. If this bit is on, the printer is the default printer; if it’s off, it’s not.

That’s a long way of saying this: to determine the default printer on a Windows 2000 computer, we simply need to grab the collection of all the computers, and then inspect the Attributes property on each one. When we find a printer where the bit equal to 4 is on, then that’s our default printer.

Here’s the script that does just that:

Const DEFAULT = 4

strComputer = "."

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colPrinters = objWMIService.ExecQuery _
    ("Select * From Win32_Printer")

For Each objPrinter in colPrinters
    If objPrinter.Attributes And 4 Then
        Wscript.Echo objPrinter.ShareName
    End If
Next

If you aren’t familiar with bitmasks, then this script might not make too much sense to you. But that’s OK; go ahead and use it now, and then read up on bitmasks when you get the chance.


How Can I Purge All the Print Jobs on a Windows 2000 Print Server?

Hey, Scripting Guy! How can I purge all the print jobs on a Windows 2000 print server?

-- KC

Hey, KC. This is one of those questions we hate, not because we don’t have an answer for it – we do – but because we have to temporarily abandon one of the core tenets of the Scripting Guy Way of Life. That tenet – just a second while we blow the dust off it– is this: ADSI should be used for managing Active Directory and other directory services (including local computer accounts); WMI should be used for everything else.

Ok, so maybe it’s not the 10 Commandments or the Golden Rule. But hey, we’re scripting guys, not philosophy guys.

Although ours is a simple way of life, most of the time this tenet works just fine. For example, suppose you’re using Windows XP or Windows Server 2003 as a print server. Want to delete all the print jobs running on that computer? All you do is use a WMI script, just like we told you to:

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

Set colInstalledPrinters = objWMIService.ExecQuery _
    ("Select * from Win32_Printer")

For Each objPrinter in colInstalledPrinters
    objPrinter.CancelAllJobs()
Next

But what if you’re using a Windows 2000 computer as a print server? Well, in that case we have a problem: the CancelAllJobs() method – used to delete print jobs from a printer on Windows XP and Windows Server 2003 – doesn’t exist on Windows 2000. Not only that, but there’s no comparable method to be found anywhere in the WMI repository. The cold, hard fact of the matter: you can’t use WMI to delete all the print jobs on a Windows 2000 computer.

Now, people of good moral fiber would simply stop at this point; after all, they would never compromise their principles just to delete a bunch of print jobs on a print server. We Scripting Guys have no such qualms, however; therefore, here’s an ADSI script that can delete all the print jobs on a Windows 2000 computer:

Set objComputer = GetObject("WinNT://atl-ps-01")
objComputer.Filter = Array("printQueue")

For Each objPrinter in objComputer
    objPrinter.Purge
Next

As you can see, we begin by using the WinNT provider to connect to the print server atl-ps-01. (By the way, WinNT is case-sensitive; your script will fail if you try using winnt or WINNT or any other variation.) This returns a collection of all the “things” on atl-ps-01, or at least all the things ADSI knows about: user accounts, groups, services, and – hey! – printers. Because we only care about the printers we apply a Filter that limits the returned data to printQueue objects; as you might expect, printQueue is the ADSI designation for a printer object. The filter requires just one line of code:

objComputer.Filter = Array("printQueue")

Incidentally, the object we’re filtering on – printQueue – must be configured as an array. This code looks like it should work, but it won’t:

objComputer.Filter = "printQueue"

All that’s left now is to loop through the collection of printers and – for each printer found – delete all the print jobs, something we can do using the Purge command. Deleting all the print jobs on all the printers takes just three lines of code:

For Each objPrinter In objComputer
    objPrinter.Purge
Next

Not too bad, huh? If you’d like more information about managing print servers on Windows 2000 you might check out the Printing chapter in the Microsoft Windows 2000 Scripting Guide. And if you’d like more information about breaking the rules, just keep reading this column; we’re bound to break a few more before we’re through.


How Can I Start Up and Shut Down a Virtual Server Session?

Hey, Scripting Guy! How can I start up and shut down a Virtual Server session?

-- DW

Hey, DW. One of the Scripting Guys once had a neighbor who used to say, over and over again, “You know, I think I should have a garage sale. I really should have a garage sale.” This went on for a couple of years, until the guy finally did have a garage sale. And nobody came. Towards the end of the day someone finally dropped by, and by then the neighbor was so happy to have a customer that he pretty much gave everything away. A few months later he moved (although we don’t know whether that’s because the garage sale was such a disappointment or because he had one of the Scripting Guys as a neighbor).

We can empathize with this ex-neighbor. A year or so ago the Scripting Guys posted a bunch of Virtual Server scripts in the Script Repository, assuming all the while that we’d soon be inundated with questions about scripting Virtual Server. And then nobody came. We know people downloaded the scripts, but nobody ever asked us any questions about scripting Virtual Server. We couldn’t help but feel a little disappointed.

Ah, but then we got your question, and we’re so happy to have a Virtual Server scripting customer that we’re going to pretty much give these scripts away. You want to use a script to start a virtual machine? Well then why didn’t you say so:

Set objVS =
CreateObject("VirtualServer.Application")
Set objVM = objVS.FindVirtualMachine("Windows 2000 Server")
objVM.Startup()

Yes, just three lines of code. We begin by creating an instance of the VirtualServer.Application object. We then call the FindVirtualMachine method, passing as a single parameter the name of the machine we want to start (Windows 2000 Server). After that we simply call the Startup() method, and the virtual machine will begin loading. It’s that simple.

It’s equally simple to shut down a virtual machine:

Set objVS = CreateObject("VirtualServer.Application")
Set objVM = objVS.FindVirtualMachine("Windows 2000 Server")
objVM.Turnoff()

Yes, it is identical to the startup script, except for one thing: this time we call the Turnoff() method in order to shut the machine down.

If you find these scripts useful, you might want to take some time and browse through the Virtual Server scripts in the Script Repository. There’s a lot to choose from, and our prices (free) are tough to beat. The Scripting Guys will not be undersold!


How Can I Determine the Version of DirectX Installed on a Computer?

Hey, Scripting Guy! How can I determine the version of DirectX installed on a computer?

-- ML

Hey, ML. How can you determine the version of DirectX installed on a computer? That’s an important question, and we’ll get to that in a moment. Before we do that, however, there’s an even more important issue we need to address: quicksand. Until very recently the Scripting Guy who writes this column wasn’t aware of this, but, as it turns out, quicksand won’t actually suck you all the way under, the way it does in the movies. That’s because the human body is less dense than the quicksand; at worst you might sink in to your waist or so. (And yes, that means that there actually is something more dense than the Scripting Guys.) Granted, you’ll find it hard to get out should you sink in that far, and while you’re stuck there you might die of thirst or be eaten by a lion or something. But you won’t actually drown.

Note. We appreciate that, but no thanks are necessary; we’re just doing our job. After all, if the Scripting Guys don’t tell you the truth about quicksand, well, who will?

Ironically, it proved easier to get bogged down in DirectX than it does to get bogged down in quicksand. We knew that the DirectX version number was stored in the registry, and we even had a vague idea where this information could be found in the registry (HKEY_LOCAL_MACHINE\Software\Microsoft\DirectX as it turns out). What we weren’t ready for was the actual value stored in the registry; for example, on our test computer that value happens to be 4.09.00.0904. That’s fine, except that this is a fairly new computer and we were pretty sure it had DirectX 9.something installed. Suddenly we began to get this sinking feeling, if you know what we mean.

But all’s well that ends well: to determine the version of DirectX all we need to do is retrieve the registry value and then convert that to the “real” version of DirectX. Coincidentally enough, that’s exactly what this script does:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\DirectX"
strValueName = "Version"

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Select Case strValue
    Case "4.02.0095"
        strVersion = "1.0"
    Case "4.03.00.1096"
        strVersion = "2.0"
    Case "4.04.0068"
        strVersion = "3.0"
    Case "4.04.0069"
        strVersion = "3.0"
    Case "4.05.00.0155"
        strVersion = "5.0"
    Case "4.05.01.1721"
        strVersion = "5.0"
    Case "4.05.01.1998"
        strVersion = "5.0"
    Case "4.06.02.0436"
        strVersion = "6.0"
    Case "4.07.00.0700"
        strVersion = "7.0"
    Case "4.07.00.0716"
        strVersion = "7.0a"
    Case "4.08.00.0400"
        strVersion = "8.0"
    Case "4.08.01.0881"
        strVersion = "8.1"
    Case "4.08.01.0810"
        strVersion = "8.1"
    Case "4.09.0000.0900"
        strVersion = "9.0"
    Case "4.09.00.0900"
        strVersion = "9.0"
    Case "4.09.0000.0901"
        strVersion = "9.0a"
    Case "4.09.00.0901"
        strVersion = "9.0a"
    Case "4.09.0000.0902"
        strVersion = "9.0b"
    Case "4.09.00.0902"
        strVersion = "9.0b"
    Case "4.09.00.0904"
        strVersion = "9.0c"
    Case "4.09.0000.0904"
        strVersion = "9.0c"
End Select

Wscript.Echo strVersion

Incidentally, don’t be deterred by the size of this script; most of the code involves converting the possible registry values. As you’re about to see, the script itself is extremely simple.

How simple? Well, we start out by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; we’ll use this constant to tell the script which registry hive to connect to. We then use this code to bind to the WMI service on the local computer:

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

And yes, like most WMI scripts we could just as easily bind to the WMI service on a remote computer. How? That’s easy; simply assign the name of the remote computer to the variable strComputer. For example, this block of code connects us to the WMI service on the remote computer atl-fs-01:

strComputer = "atl-fs-01"

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Note. When making your connection, be sure and use the WMI namespace root\default. Typically when you write a WMI script you connect to the root\cimv2 namespace. Not this time, however. Instead you need to connect to root\default, and for one simple reason: that’s where the StdRegProv class resides.

After making the connection we assign values to a pair of variables. The variable strKeyPath is assigned the path within the HKEY_LOCAL_MACHINE registry hive; for this script, that happens to be Software/Microsoft/DirectX. Meanwhile, the variable strValueName gets assigned the name of the actual registry value within the DirectX key we want to work with. For this script, that happens to be the Version value.

Once these values are assigned we then call the GetStringValue method in order to read the DirectX version from the registry:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Nothing too fancy there: we simply call GetStringValue, passing – in order – the constant HKEY_LOCAL_MACHINE, the “in” parameters strKeyPath and strValueName, and the “out” parameter strValue. What’s all this in-and-out stuff? Well, an in parameter is a value you supply to a method; by contrast, an out parameter is a value that a method returns to you. In other words, we give GetStringValue a variable name – strValue – and, in return, GetStringValue will assign to that variable the value read from the registry. For example, after we execute the GetStringValue method on our test machine, strValue will be equal to 4.09.00.0904.

Of course, we don’t really want strValue to be equal to 4.09.00.0904; instead, we want it to be equal to the “real” version of DirectX (9.0c). To convert the 4.09.00.0904 to a version number we’re all more familiar with we simply set up a Select Case statement that examines the value read from the registry and then assigns the more-commonly-used version number to a variable named strVersion. For example, here’s the code that converts 4.09.00.0904:

Case "4.09.00.0904"
    strVersion = "9.0c"

All that’s left now is to echo back the value of strVersion. See? We told you it was pretty simple.

You know that’s an interesting question: were all those old Tarzan movies and all those old episodes of Gilligan’s Island lying to us when they showed the bad guy getting trapped in quicksand and slowly sinking beneath its inexorable grip? (One interesting property of quicksand is that it only seems to work on bad guys; good guys can always figure a way to get out of it. Also, while quicksand can suck a man down in no time – and, again, it’s always a man, never a woman – it invariably leaves his hat floating on the surface. Go figure.)

To be honest, we don’t know whether they were lying or not. However, considering the fact that almost everything the Scripting Guy who writes this column knows about science and geography he learned from Gilligan’s Island, well, let’s hope they were just taking artistic license. (Which is fine with us. After all, where would the Scripting Guys be without, uh, “artistic license?”)


How Can I Tell Which Version of QuickTime is Installed on a Computer?

Hey, Scripting Guy! How can I tell whether or not QuickTime is installed on a computer, and, if so, which version is installed?

-- IU

Hey, IU. You know, for some reason this column suddenly seems to have become a daily dancing column rather than a daily scripting column. After all, just yesterday we recapped the Scripting Son’s … dancing … exploits at his cousin’s wedding. And now, today, the Scripting Guy who writes this column was getting a cup of coffee when he noticed an ad for tango lessons. Learn how to tango, said the ad, and you’ll be able to go into “ … any major city and dance – no partner required!”

We have to admit that, at first, this sounded like a pretty good deal, and we were all ready to sign up. But then we realized something: the Scripting Guys apparently don’t even need tango lessons. Go into any major city and not find someone who’d be willing to dance with you? That’s not much of a problem for the Scripting Guys; we don’t really need lessons on how not to get people to do anything with us.

On top of that, we’re a bit skeptical about this whole tango thing. Throughout his entire life the Scripting Guy who writes this column has been told that it takes two to tango. And now they’re telling him that no partner is required, that apparently it only takes one to tango. We’ll believe it when we see it.

Although we hope we never see any of the Scripting Guys doing the tango.

No, not even Peter. In fact, especially not Peter.

On the other hand, what we would like to see is the same thing IU would like to see: a script that can tell you whether or not QuickTime is installed on a computer. You know, a script similar to this one:

strComputer = "."

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

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Product Where Name = 'QuickTime'")

If colItems.Count = 0 Then
    Wscript.Echo "QuickTime is not installed on this computer."
Else
    For Each objItem in colItems
        Wscript.Echo "QuickTime version: " & objItem.Version
    Next
End If

As you can see, determining whether or not QuickTime is installed on a computer is even easier than doing the tango. (Or so we assume, having never actually done the tango.) As you can see, we start out by connecting to the WMI service on the local computer. However, like most WMI scripts, we can also use this one to determine whether or not QuickTime has been installed on a remote machine. How do we do that? Simple; we just assign the name of the remote computer (e.g., atl-ws-01) to the variable strComputer:

strComputer = "atl-ws-01"

After making the connection we then use this line of code to retrieve a list of all the installed software that has a Name with the value QuickTime:

Set colItems = objWMIService.ExecQuery _
    ("Select * From Win32_Product Where Name = 'QuickTime'")

Like we said, this returns a collection consisting of every application installed on the computer that has the name QuickTime. Now, suppose we don’t actually have QuickTime installed. That’s fine; in that case we simply get back a collection consisting of 0 items. In turn, that means we can identify whether or not QuickTime is installed simply by checking the number of items in the returned collection, something we do by examining the value of the collection’s Count property:

If colItems.Count = 0 Then

If Count is equal to 0 that means that QuickTime is not installed; therefore we echo back a message to that effect:

Wscript.Echo "QuickTime is not installed on this computer."

Logically enough, that also means that, if the Count is greater than 0, then QuickTime must be installed. In that case we set up a For Each loop to loop through all the items in the collection and, for each item, echo back the value of the Version property:

For Each objItem in colItems
    Wscript.Echo "QuickTime version: " & objItem.Version
Next

That’s going to be a value similar to this:

7.1.5.120

And there you have it: with that you’ll get back the version of QuickTime installed on the computer, and everyone will be happy.

Well, except maybe those people running Windows Server 2003. By default the Win32_Product class isn’t installed on Windows 2003; unless you’ve gone ahead and added this class (which can be done through Add or Remove Programs) this script won’t do you any good. But everyone else will be happy.

Well, except maybe those people running Windows Vista. As it turns out, the Win32_Product class is installed by default on Windows Vista; it’s even been enhanced with some additional properties. However, there are some known problems with Win32_Product class on Windows Vista: sometimes it doesn’t work at all (returning an error) and sometimes it works, but a bit more slowly than you might expect (or like). For example, that little script that tells us whether or not QuickTime is installed might take a minute (or even more) to complete.

If it’s any consolation, we agree with you.

So is there another way to determine whether or not QuickTime is installed and, if so, which version is installed? As a matter of fact there are a couple of workarounds that we know of. For example, if you’re performing this check as part of a logon script you can use this little script instead:

On Error Resume Next

Set objQuickTime = CreateObject("QuickTimeCheckObject.QuickTimeCheck.1")

If Err <> 0 Then
    Wscript.Echo "QuickTime is not installed on this computer."
Else
    Wscript.Echo "QuickTime version: " & Hex(objQuickTime.QuickTimeVersion)
End If

Here we’re trying to create an instance of the QuickTime.QuickTimeCheck.1 object. If the object cannot be created that means that QuickTime is not installed, and we echo back a message to that effect. If the object can be created we then grab the value of the Version property, convert that to a hexadecimal number, and then echo back the result:

Wscript.Echo "QuickTime version: " & Hex(objQuickTime.QuickTimeVersion)

Why do we need to convert the value to a hexadecimal number? Beats us; we just have to. That’s because the Version comes back as a value like 118849536; converting that to a hex value results in a version number of 7158000. Is that good? Yes; on our test machine we happen to be running QuickTime 7.1.5.

Note. Why does this come back, in effect, as 7.1.5.8000 and the version reported by Win32_Product come back as 7.1.5.120? We don’t know. But 7.1.5 is the official version number reported through the QuickTime UI so we aren’t going to worry about the discrepancy.

Unfortunately, though, we have a problem with this script, too: the QuickTime.QuickTimeCheck.1 object can’t be created remotely. (That’s why we included the stipulation about performing this check as part of a logon script.) If you need to perform this task remotely you can, instead, read the value from the registry:

HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Apple Computer, Inc.\QuickTime"
ValueName = "Version"

objReg.GetDWORDValue HKEY_LOCAL_MACHINE, strKeyPath, ValueName, dwValue

If IsNull(dwValue) Then
    Wscript.Echo "QuickTime is not installed."
Else
    Wscript.Echo "QuickTime version: " & Hex(dwValue)
End If

This script should work on any version of Windows. So then why didn’t we just show you this particular script in the first place? Well, in general, we’re hesitant about using scripts that read directly from the registry; after all, there’s no guarantee that the registry path will remain the same when the next version of QuickTime is released. For that matter, we can’t even guarantee that this script will work with previous versions of QuickTime. That’s why, whenever possible, it’s better to let a WMI class such as Win32_Product do all the work rather than try and retrieve these values from the registry. (Editor’s Note: Another reason? On the Scripting Editor’s computer, the UI and Win32_Product report a version of 7.0.3; the check of the QuickTime.QuickTimeCheck.1 object returns 7038000; the registry returns 7028000.) But, like they say, desperate times call for desperate measures. And, as in this case, sort of, kind of desperate times call for sort of, kind of desperate measures.

If that sounds like we’re trying to dance around the problem, well, to a certain extent we are. But at least that gives you an alternative approach just in case you run into problems with the Win32_Product class. And we promise that this is the only dancing you’ll ever see from the Scripting Guys, even when they attend TechEd 2007 this coming June. For one thing, the Scripting Guy who writes this column doesn’t dance, period. For another, after her near-tragic incident in San Diego, we’ve been discouraging Scripting Guy Jean Ross from doing things that are just too difficult for her. “Do your walking first,” we tell her, “and then chew your gum.” With any luck she’ll listen to us this time. (Editor’s Note: Could it be that the Scripting Guy who writes this column is looking out for the well-being of a fellow Scripting Guy? Don’t believe it; he’s really just concerned that Scripting Guy Jean Ross will embarrass him. Which is entirely possible – even likely.)


How Can I Print Text Files Using a Script?

Hey, Scripting Guy! I have a bunch of text files in a folder. Is there a way to print those files using a script? I know I can use Microsoft Word to print the files from a script, but I don’t have Microsoft Word installed on that computer.

-- MA

Hey, MA. Don’t tell anyone from Microsoft we said this, but that’s OK; you don’t need Microsoft Word just to print a few text files. Instead, you can use the Shell object – which is built right into the operating system – to print text files. And what’s cool about this is the fact that you can print these files even though the Shell object doesn’t have a Print method. Strange but true!

You can do this is because the Shell object has a method called InvokeVerbEx that lets you perform the tasks that show up when you right-click a file in Windows Explorer. Right-click one of your text files, and look at the context menu that pops up. You should see things like Open, Print, Edit, Cut, Copy, Delete, etc. The Shell object lets you perform all those tasks programmatically. In fact, here’s a script that prints all the files in the folder C:\Logs:

TargetFolder = "C:\Logs"
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(TargetFolder)
Set colItems = objFolder.Items
For Each objItem in colItems
    objItem.InvokeVerbEx("Print")
Next

All we’re doing here is connecting to the C:\Logs folder and then grabbing a collection of all the items found in that folder. We store the set of files in the variable colItems, using this line of code:

Set colItems = objFolder.Items

Note that we’ve made it easy on ourselves in this example by making sure that C:\Logs contains only log files, and that we want to print each of those files. If C:\Logs contains other files, files that we don’t want to print, you’ll have include code that distinguishes between the files you want to print and the files you don’t want to print.

After stashing the set of files in colItems, we use a For Each loop to loop through all the items in the collection. For each item in the collection (in other words, for each file in the folder) we use the InvokeVerbEx method, and we tell InvokeVerbEx that we want to print each file:

objItem.InvokeVerbEx("Print")

That’s it. Run this script and all the files will dutifully print, one right after the other.

Now what if we wanted to do something else, something like, say, edit each file? No problem; just replace the Print parameter with the appropriate parameter name from the context menu:

TargetFolder = "C:\Logs"
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(TargetFolder)
Set colItems = objFolder.Items
For Each objItem in colItems
    objItem.InvokeVerbEx("Edit")
Next

One thing to keep in mind is that when you use InvokeVerbEx your script replicates the same actions you see in Windows Explorer. For example, what happens in Windows Explorer if you right-click a file and choose Delete? That’s right: instead of the file being automatically deleted, a dialog box appears asking if you are sure you want to send the file to the Recycle Bin. That’s important, because the same dialog box will appear if you pass Delete to InvokeVerbEx. This script won’t automatically delete the files in C:\Logs, but instead pops up a confirmation dialog box for each file in the folder:

TargetFolder = "C:\Logs"
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(TargetFolder)
Set colItems = objFolder.Items
For Each objItem in colItems
    objItem.InvokeVerbEx("Delete")
Next

Keep in mind, too, that you might not be able to use all the options that appear in the Context menu. For more information on determining which options are available to you and which ones are not, check out this section of the Microsoft Windows 2000 Scripting Guide.


Can I Read a Text File From the Bottom Up?

Hey, Scripting Guy! I have a log file in which new data is appended to the bottom of the file; that means the most recent entries are at the end of the file. I’d like to be able to read the file starting with the last line and then ending with the first line, but I can’t figure out how to do that.

-- MB, Milwaukee, WI

Hey, MB. The FileSystemObject is extremely useful, but it also has its limitations; one of the major limitations is the fact that it can only read a file from top to bottom. Unlike, say, the Tail utility, you can’t ask the FileSystemObject to read a file from the bottom up. (Well, we suppose you can ask it, but it won’t do it for you.)

But that’s OK; as always seems to be the case with scripting, you can find a way to work around limitations like this. In this case, what we do is go ahead and read the file from top to bottom, beginning with line 1 and ending with line whatever. However, rather than immediately echoing these lines to the screen, we’ll store them in an array, with each line in the file representing one element in the array.

Why do we do that? Well, now we have an array that looks like this, replicating the way information is stored in the text file:

violet
indigo
blue
green
yellow
orange
red

Admittedly, all we seem to have done so far is re-invent the wheel. However, there’s one important difference between a text file and an array: it’s actually pretty easy to read an array from the bottom to the top. We’re going to tell our script to begin with the last item in the array (which we can determine using the Ubound function) and work backwards towards the first item in the array (Lbound).

In our sample array, we have 7 items in the array; the last item (Ubound) is the word red and the first item (Lbound) is the word violet. The first item in the array is given an index number of 0; thus violet has an index number of 0 and red has an index number of 6. Our script is going to start with item 6 and work backwards to item 0. How do we work backwards like that? We add the parameter Step -1, which essentially says, “Look at item 6 and do something with it. Then subtract 1 from the index number, which leaves 5. Look at item 5, and do something with it. Repeat this process until you’ve looked at all the items in the array.”

Here’s what the code looks like:

Dim arrFileLines()
i = 0
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\FSO\ScriptLog.txt", 1)
Do Until objFile.AtEndOfStream
     Redim Preserve arrFileLines(i)
     arrFileLines(i) = objFile.ReadLine
     i = i + 1
Loop
objFile.Close
For l = Ubound(arrFileLines) to LBound(arrFileLines) Step -1
    Wscript.Echo arrFileLines(l)
Next

Slightly confusing if you haven’t worked much with arrays, but once you get the hang of it you’ll see how simple it really is.

Oh, and here’s what our bottom-to-top output looks like:

red
orange
yellow
green
blue
indigo
violet

For more information about working with arrays, see this portion of the Microsoft Windows 2000 Scripting Guide.


Can I Read an Entire Text File Rather Than Reading It Line-by-Line?

Hey, Scripting Guy! Right now I use a script to open a text file containing computer names. The script reads the first line of the file and connects to that computer, then reads the second line of the file and connects to that computer. Is there a way to store those names in a variable or something, so that I don’t have to keep reading the text file?

-- KS

Hey, KS. We’re assuming that your script currently looks something like this one. In this sample script, we open a text file (servers.txt), read the first line (we’re also assuming that there is one computer name per line in the file), and then echo the name of the computer. (In your script, of course, you probably connect to that remote computer at this point.) We then read in the second line of the text file, echo the name of that computer, loop around, read in the third line of the text file, and continue in this vein until we’ve finished reading each line in the file:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\scripts\servers.txt", ForReading)

Do Until objTextFile.AtEndOfStream
    strComputer = objTextFile.ReadLine
    Wscript.Echo strComputer
Loop

objTextFile.Close

Now, there’s nothing wrong with reading a text file this way; it works just fine. However, we can understand why you might want to read the text file all at once and then work with the list in memory. After all, suppose the text file is located on a remote computer. Not only will you have to continually travel across the network just to read a single line out of a text file, but if that remote computer goes down, then your script goes down as well.

So can you read the text file all at once, store that information in a variable, and then work with the list of computers from memory? Of course you can:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\scripts\servers.txt", ForReading)

strText = objTextFile.ReadAll
objTextFile.Close

arrComputers = Split(strText, vbCrLf)

For Each strComputer in arrComputers
    Wscript.Echo strComputer
Next

Here’s how the script works. Like your current script, we create a constant (ForReading) and assign it the value 1; this is required when using the FileSystemObject to read a text file. We then create an instance of the FileSystemObject, and use the OpenTextFile method to open the file C:\Scripts\Servers.txt. But you already knew that, didn’t you?

Here’s the part you’re really interested in. Instead of reading the file line-by-line, we use the ReadAll method to read the entire text file in one fell swoop, storing the contents of that file in the variable strText. The variable strText now becomes an exact replica of the text file. Suppose your text file looks like this:

atl-ws-01
atl-ws-02
atl-ws-03
atl-ws-04

Now guess what will be displayed on screen if we echo the value of strText? That’s right:

atl-ws-01
atl-ws-02
atl-ws-03
atl-ws-04

Thus far we’ve succeeded in reading in the entire text file and storing it in a variable. However, that doesn’t really do us much good; after all, if we tried connecting to a computer using strText, we’d be connecting to the entire text file rather than a single computer, an operation doomed to fail. What we need to do now is somehow separate the individual computer names, so that we can then connect to the computers one at a time.

That’s what this line of code is for:

arrComputers = Split(strText, vbCrLf)

The Split command takes data (in this case, the variable strText) and creates an array out of it. How does it know where one computer name ends and the next begins? In our example, the “delimiter” that separates one computer from another is the carriage return linefeed; in our text file, we type one computer name, press ENTER, then type the next computer name. The VBScript constant vbCrLf used with the Split function represents the ENTER key. Suppose strText looked like this:

atl-ws-01,atl-ws-02,atl-ws-03,atl-ws-04

In that case, our delimiter would be the comma, and the Split command would look like this:

arrComputers = Split(strText, ",")

In other words, pass Split the variable name and the delimiter, and it does the rest.

After we have our computer names safely stashed on an array, we can then iterate each item in that array using a For Each loop:

For Each strComputer in arrComputers
    Wscript.Echo strComputer
Next

Or for a somewhat more realistic example, here’s a sample script that uses WMI to connect to each computer in the text file and retrieve the name of the installed operating system:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\scripts\servers.txt", ForReading)

strText = objTextFile.ReadAll
objTextFile.Close

arrComputers = Split(strText, vbCrLf)

For Each strComputer in arrComputers

    Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_OperatingSystem")
    For Each objItem in ColItems
        Wscript.Echo strComputer & ": " & objItem.Caption
    Next

Next


How Can I Compare the Contents of Two Text Files?

Hey, Scripting Guy! I have two separate text files. One file contains a list of MAC addresses taken from a network scan; the other contains a list of MAC addresses for our currently-managed devices. How can I compare these two files, and output a list of addresses that have no corresponding device to a third text file?

-- SR

Hey, SR. You know, last night the Scripting Guy who writes this column was watching TV when – hey, get out of here! Shoo! Shoo! Go on, get out!

Sorry; the goats wandered through the office again. We really need to put a door on this building, don’t we?

Anyway, last night the Scripting Guy who writes this column was watching TV when a segment about the NBA draft came on. As it turns out, in the NBA draft lottery the Portland Trailblazers drew the first pick in the draft and the Seattle Supersonics drew the second pick. No big deal, right?

Wrong, at least according to commentator Stephen A. Smith. Smith was absolutely outraged by this turn of events, labeling it a black day for the NBA. “You’ve got the two most exciting prospects to come around in years headed for the NBA,” he fumed. “And now both of them are going to end up in the Pacific Northwest, where no one will ever see them!”

Well, needless to say, the Scripting Guy who writes this column was outraged by Stephen A. Smith. What, we don’t have civilization up here in the Pacific Northwest? To tell you the truth, his first thought was to pick up the phone, call Stephen A. Smith, and give him what-for. And he would have, too … assuming we actually had telephones in the Pacific Northwest.

Note. In case you’re wondering, Stephen A. Smith hails from Philadelphia. The fact of the matter is that we hicks out here in the Pacific Northwest can’t hold a candle to someone from Philadelphia.

Mainly because we don’t actually have candles in the Pacific Northwest. But someday .…

At any rate, on behalf of the entire Pacific Northwest, the Scripting Guys would like to apologize to everyone who still cares about the NBA. (Yes, both of you.) And we’d like to make it up to everyone. Granted, those of us in the Pacific Northwest might not have fancy, modern-day conveniences like wall-to-wall carpeting and frozen yogurt, but we do have scripts that can compare two text files and report back any differences:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile1 = objFSO.OpenTextFile("C:\Scripts\Current.txt", ForReading)

strCurrentDevices = objFile1.ReadAll
objFile1.Close

Set objFile2 = objFSO.OpenTextFile("C:\Scripts\Addresses.txt", ForReading)

Do Until objFile2.AtEndOfStream
    strAddress = objFile2.ReadLine
    If InStr(strCurrentDevices, strAddress) = 0 Then
        strNotCurrent = strNotCurrent & strAddress & vbCrLf
    End If
Loop

objFile2.Close

Wscript.Echo "Addresses without current devices: " & vbCrLf & strNotCurrent

Set objFile3 = objFSO.CreateTextFile("C:\Scripts\Differences.txt")

objFile3.WriteLine strNotCurrent
objFile3.Close

Note. We apologize for using a stylus to inscribe today’s column into a stone tablet. Unfortunately that’s the best we can do out in the God-forsaken wasteland we call the Pacific Northwest. We hope this doesn’t inconvenience anyone. And it shouldn’t, provided your firewall will allow stone tablets to pass through.

All right, let’s see if we can figure out how this script works. (Needless to say, we have to do that right away, before the sun goes down.) To begin with, we have a text file named C:\Scripts\Current.txt, which lists the MAC addresses for all our currently-managed devices. That file looks something like this:

00:14:A5:4D:AC:01
00:14:A5:4D:AC:03
00:14:A5:4D:AC:04

We also have a second text file (C:\Scripts\Addresses.txt), which lists all the MAC addresses acquired during a network scan:

00:14:A5:4D:AC:01
00:14:A5:4D:AC:02
00:14:A5:4D:AC:03
00:14:A5:4D:AC:04
00:14:A5:4D:AC:05

What we want to do is compare the two files and see if there are devices on the network (identified using the MAC address) that do not appear in our list of currently-managed devices. In other words, we want to generate a list of unmanaged/unknown devices. So, what the heck: let’s go ahead and generate such a list.

Hey, drop that! Drop it, I say!

Sorry; raccoons tried to walk off with our text files again. Dirty, low-down, no-good varmints ….

In the script itself we start out by defining a constant named ForReading; we’ll need this constant to open and read from our two text files. We next create an instance of the Scripting.FileSystemObject, then use this line of code to open the file C:\Scripts\Current.txt for reading:

Set objFile1 = objFSO.OpenTextFile("C:\Scripts\Current.txt", ForReading)

From there we use the ReadAll method to read in the entire file, storing the contents in a variable named strCurrentDevices. And then, with no further need for Current.txt, we use the Close method to close the file.

We now have a list of the MAC addresses for all our managed devices. Our next step is to get the list of all MAC addresses found on the network and then check each of those addresses, one-by-one, to see if they show up in the list of managed devices.

To that end, the first thing we do is open the file C:\Scripts\Addresses.txt, also for reading:

Set objFile2 = objFSO.OpenTextFile("C:\Scripts\Addresses.txt", ForReading)

Because each MAC address is listed on a separate line in the file, we set up a Do Until loop that will run until we reach the end of the file. (Or, to be a little more technical, until the AtEndOfStream property is True.) Inside that loop, we use the ReadLine method to read the first line in the file (that is, the first MAC address) and store that value in a variable named strAddress:

strAddress = objFile2.ReadLine

Using our sample file, that means strAddress is equal to this:

00:14:A5:4D:AC:01

That brings us to this line of code:

If InStr(strCurrentDevices, strAddress) = 0 Then

What we’re doing here is using the InStr function to determine whether or not the first MAC address (00:14:A5:4D:AC:01) can be found anywhere in the list of managed devices. As you can see, InStr takes two parameters: the string we want to search (strCurrentDevices) and the string we’re searching for (strAddress). InStr returns the character position at which the search string is found; if the search string can’t be found then InStr returns 0.

In other words, if InStr returns 0 that means that the address is not in the list of managed devices. Therefore, we add that address to a running tally of unmanaged devices:

strNotCurrent = strNotCurrent & strAddress & vbCrLf

That bit of code is fairly straightforward: all we’re doing is assigning a value to a variable named strNotCurrent. And what value are we assigning this variable? We’re assigning it the existing value of strNotCurrent plus the MAC address we just failed to find (strAddress) plus a carriage return-linefeed (vbCrLf). And then we loop around and repeat this process with the next MAC address.

When we’ve finished checking all the MAC addresses we close the file, then use this line of code to echo back the results:

Wscript.Echo "Addresses without current devices: " & vbCrLf & strNotCurrent

Based on our sample text files, that’s going to result in the following information appearing onscreen:

Addresses without current devices:
00:14:A5:4D:AC:02
00:14:A5:4D:AC:05

And then, because SR wanted to save the results to a third file, we then execute this final block of code:

Set objFile3 = objFSO.CreateTextFile("C:\Scripts\Differences.txt")

objFile3.WriteLine strNotCurrent
objFile3.Close

What we’re doing here is first creating a new text file, one named C:\Scripts\Differences.txt. Once the file has been created we then call the WriteLine method to write the value of strNotCurrent to the new file. After that we close the file and call it good.

Of course, you could just as easily go the other direction and search for managed devices that don’t appear in the list of addresses. To accomplish that feat all you have to do is first read in the list of addresses and then search that list for each managed device. (Exactly the opposite of the task we just finished.) That code would look something like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile1 = objFSO.OpenTextFile("C:\Scripts\Addresses.txt", ForReading)

strAddresses = objFile1.ReadAll
objFile1.Close

Set objFile2 = objFSO.OpenTextFile("C:\Scripts\Current.txt", ForReading)

Do Until objFile2.AtEndOfStream
    strCurrent = objFile2.ReadLine
    If InStr(strAddresses, strCurrent) = 0 Then
        strNoAddress = strNoAddress & strCurrent & vbCrLf
    End If
Loop

objFile2.Close

Wscript.Echo "Devices with no address: " & vbCrLf & strNoAddress

Set objFile3 = objFSO.CreateTextFile("C:\Scripts\Differences.txt")

objFile3.WriteLine strNoAddress
objFile3.Close

Needless to say, you could also combine these two scripts and end up looking for items that appear in File A but not in File B as well as items that appear in File B but not in File A. But we’ll let you take care of that yourself.

We hope that answers your question, SR. In the meantime, we Scripting Guys are getting ready to watch Stephen A. Smith’s TV show and – oh, wait a minute. Sorry, we forgot: Stephen A. Smith’s TV show was cancelled. What a shame, huh?


How Can I Count the Number of Characters in a Text File?

Hey, Scripting Guy! How can I count the number of characters in a text file?

-- LD

Hey, LD. We know a burning question for many of you (in addition to the question of how you can count the number of characters in a text file) is this: what kind of digital video recorder does the Scripting Guy who writes that column own? Well, here’s your answer: the Scripting Guy who writes this column doesn’t own a digital video recorder. For the most part, that’s because he’s a cheapskate; however, it’s also true that he’s not the least bit interested in any technology that enables you to skip commercials.

That’s right: the Scripting Guy who writes this column actually watches TV commercials. Last night, for example, he saw a new one about a kindly zookeeper who has a terrible cold. When the zookeeper leaves for home he accidentally drops his debit card. An elephant picks up the card, goes out and make a few purchases (like chicken soup), then heads over to the zookeeper’s house and takes care of his ailing friend. Talk about heart-warming, huh?

You bet. Well, except for the fact that even an elephant can find your debit card and use it, no questions asked: no one ever asked the elephant for ID, nor was the elephant required to enter a PIN number. (That, of course, is the whole point of this new card: no longer will you have to waste two seconds of your life typing in a four-digit PIN number.) This was a cute commercial because it was a friendly elephant who found and used the zookeeper’s debit card.

But suppose a 27-year-old high school dropout who needed new wheels and tires for his ’67 Camaro found the zookeeper’s debit card? Or suppose a rogue elephant who needed new wheels and tires for his ’67 Camaro found the zookeeper’s debit card? In cases like that, well, we’re not sure that the ad would have been quite as heart-warming.

Note. The Scripting Guy who writes this column doesn’t work at a convenience store, but you can rest assured that, if he ever does, he will demand to see ID any time an elephant enters the store and tries to buy something using a debit card. You just never know with elephants.

Well, unless the elephant happens to be Horton. (“I meant what I said and I said what I meant: an elephant’s faithful, 100 percent.”)

And now for burning question number 2: how can we count the number of characters in a text file? Let’s assume that we have the following text file, which just happens to be a passage from Horton Hatches the Egg:

"It's strange! It's amazing! It's wonderful! New!
Don't shoot him! We'll catch him! That's just what we'll do!
Let's take him alive. Why he's terribly funny!
We'll sell him back home to a circus, for money!"

How are we going to count the characters in this text file? Like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strCharacters = objFile.ReadAll

strCharacters = Replace(strCharacters, vbCrLf, "")
Wscript.Echo Len(strCharacters)

objFile.Close

To tell you the truth, we don’t know of any direct method of determining the number of characters in a text file (for example, binding to the file and then looking at some sort of NumberOfCharacters property). But are we going to let that stop us? Of course not. Instead, we’ll simply read the file and count the number of characters ourselves.

Note. OK, that’s not entirely true. It seems like overkill to us, but you can open the text file in Microsoft Word and then use Word’s ComputeStatistics method to determine the number of characters in the file. See this Office Space article for details.

And just how are we going to count the number of characters in the file? Well, for starters, we define a constant named ForReading and set the value to 1; we’ll use this constant when we open our text file for reading. We then use these two lines of code to create an instance of the Scripting.FileSystemObject and, well, open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as the file is open we then use the ReadAll method to read the entire file and store the contents in a variable named strCharacters:

strCharacters = objFile.ReadAll

We’re practically done now. Why? Because once we have the contents of the file stashed away in a variable we can then use VBScript’s Len function to report back the number of characters in that variable. And that’s good, because the number of characters in the variable will equal the number of characters in the file.

So then why did we say that we were practically done? Well, if you were to manually count the characters in the file you’d probably come up with 204. However, our script is going to report back 210. Why? Because the script is going to include two characters (the carriage return and the linefeed characters) that occur at the end of lines 1, 2, and 3 in the file. You don’t see those characters and you probably don’t consider them to even be characters. But VBScript does.

Therefore, before we begin counting we use this line of code to get rid of any carriage return-linefeed characters:

strCharacters = Replace(strCharacters, vbCrLf, "")

All we’re doing here is using the Replace function to search through strCharacters and replace all the carriage return-linefeeds (represented by the VBScript constant vbCrLf) with, well, nothing (“”). Needless to say, we could use this same approach to get rid of anything else we didn’t want to count as characters. For example, suppose you don’t want blank spaces counted as characters. No problem; just add in this line of code, which removes all the blank spaces from strCharacters:

strCharacters = Replace(strCharacters, " ", "")

Don’t like periods, question marks, or, say, semicolons? Now you know what to do about those problem characters, too.

After deleting the carriage return-linefeeds (and any other unwanted characters) we finally get to use the Len function to report back the number of characters in the file:

Wscript.Echo Len(strCharacters)

All that’s left now is to close the file and then go back to watching TV.

Speaking of which, we’re sure many of you are thinking, “Gosh, Scripting Guys, you already answer our scripting questions for us; you don’t have to watch TV for us as well.” All we can say is this: don’t worry about it. Someone needs to tell you about all the latest commercials, and if that means the Scripting Guys have to spend their time watching TV, well, that’s a sacrifice we’re willing to make. Anything for our customers!


How Can I Count the Number of Lines in a Text File?

Hey, Scripting Guy! How can I count the number of lines in a text file?

-- MS

Hey, MS. It’s easy to tell that Microsoft’s scripting technologies weren’t written by people who do system administration for a living. Our scripting technologies are jam-packed with features and capabilities, yet we always seem to missing the things that people really want to do. Counting the number of lines in a text file is a good example of that. This seems to be something system administrators do on a regular basis; after all, this is the fourth or fifth time we’ve been asked this question just this month. And yet, there’s no straightforward way to count the number of lines in a text. Is there a CountLines method somewhere? Nope. How about a NumberOfLines property? Nope. Go figure.

Fortunately, while our scripting technologies aren’t always straightforward, they usually are flexible enough to provide a workaround. And this is no exception. While there might not be an obvious way to count the number of lines in a text file, you can still use a script to get at this information. For example, this script returns the number of lines found in the file C:\Scripts\Test.txt:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("C:\Scripts\Test.txt", ForReading)

objTextFile.ReadAll
Wscript.Echo "Number of lines: " & objTextFile.Line

So what’s the secret here? Well, we begin by using the FileSystemObject to open the file for reading. Next we simply read the entire text file, using the ReadAll method. When we use ReadAll, we read in every line of the text file. Because the FileSystem object can only read from the beginning of a file to the end of the file, that means that when ReadAll is finished we must be on the very last line of the file; it’s impossible for us to be anywhere else. Consequently, all we have to do is echo the value of the Line property, which reports the line number of the current line. Because we are on the last line, the Line property in this case also tells us the number of lines in the file. Simple.

Of course, you might be thinking, “Oh, sure, open up and read an entire file just to get the line count? How long will that take?” Surprisingly enough, not very long at all. We tested this script on a text file with just over 20,000 lines. On a regular old laptop computer (2.39 GhZ, 512 MB of RAM) the script took 1 second to complete. Your results might vary, but they probably won’t vary by much.


How Can I Count the Number of Times a Word Appears in a Log File?

Hey, Scripting Guy! How can I count the number of times the word Failure appears in a log file? There’s one catch, though: the log file simply writes its events one after another, creating one big, giant line of text.

-- FS

Hey, FS. According to the rest of your email, your log file looks something like this:

Failure 2/7/2006 8:25 AM Failure 2/7/2006 9:45 AM Success
2/7/2006 3:10 PM Failure 2/8/2006 9:15 AM Success 2/7/2006 3:01 PM

As you also noted, your first thought was to use the InStr function to see if the word Failure appears anywhere in each line of the log file; you could then keep a running tally of the number of times you found the word, a technique remarkably similar to the one we demonstrated in yesterday’s column. That was a good idea, but, as you discovered, there was one major flaw in the plan: technically your log file has only one line in it. Consequently, your script always reported that it found one instance of the word Failure, regardless of how many instances actually existed. “But I’m stuck,” you wrote, “because there’s no way to break this single line into multiple lines.”

Oh ye of little faith. Try this on for size:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.log", ForReading)
strContents = objFile.ReadAll
objFile.Close

i = 0

arrLines = Split(strContents, " ")

For Each strLine in arrLines
    If InStr(strLine, "Failure") Then
        i = i + 1
    End If
Next

Wscript.Echo "Number of failures: " & i

Now, admittedly, there are a couple other ways we could have solved this problem. We went with this approach because it piggybacked nicely on your original thought, and because we thought it would be fairly easy for everyone to understand. We mention this simply in case anyone is reading this and thinking, “Man, that’s not how I would have solved this.” That’s fine: this isn’t the answer. It’s just an answer.

OK, so what about the script itself? Well, we begin by defining a constant named ForReading; we’ll use this constant later on when we open our log file. We then create an instance of the Scripting.FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Test.log. With the file open, we use the ReadAll method to read the entire contents of the file into a variable named strContents, and then we close Test.log.

Got that? Next we assign the value 0 to a counter variable named i; we’ll use i to keep a running tally of each instance of the word Failure that we run into. And then we have this line of code:

arrLines = Split(strContents, " ")

Remember how you said that you were stuck because your log file was all one big long line? Well, basically what we’re doing here is dividing your log file (or at least the version of it stored in the variable strContents) into a whole bunch of little lines. In your log file individual words are separated by a blank space. In this line of code, we use the Split function to “split” the value of strContents into an array; by splitting on the blank space (that is, by creating a new item in the array each time we encounter a blank space) we end up with an array that starts out like this:

Failure
2/7/2006
8:25
AM
Failure
2/7/2006
9:45
AM
Success

Sure, it looks funny, but now we can set up a For Each loop to run through each item in the array; more important, we can also use the InStr method to see if the word Failure can be found in any of those lines. If it can, we then increment the value of our counter variable i. All of that takes place within this block of code:

For Each strLine in arrLines
    If InStr(strLine, "Failure") Then
        i = i + 1
    End If
Next

After we run through our For Each loop all we have to do is echo back our failures and we’re done.

Well, check that: all we have to do is echo back the number of failures found in the log file. Your script would likely time out long before it could finish echoing back all of our failures. (Although we still think Scripting with Celebrities would be far more entertaining than watching someone dance or ice skate with celebrities.)


How Can I Count the Number of Words in a Text File?

Hey, Scripting Guy! How can I count the number of words in a text file?

-- LA

Hey, LA. You know, this is one of those questions where the Scripting Guys outsmarted themselves. (Not that outsmarting the Scripting Guys is particularly hard to do, mind you.) For one thing, we’re writing this column on a Friday, and we always look for an easy way out on Fridays. For another, we were just involved in a discussion on word counts the other day, so the subject was already on our minds. This question sounded easy and we’d already been thinking about word counts: add the two together and you have the perfect column for a Friday.

Or so we thought.

The first hint of trouble occurred right off the bat, when we sat down to figure out the answer to your question. After all, there are several different ways we could approach this problem. For example, it’s easy to calculate word counts using Microsoft Word, so our first thought was, “Let’s just use Microsoft Word.” But that seemed like overkill, and we didn’t want to imply that you couldn’t count the number of words in a text file unless you went out and bought Microsoft Office. (Although if the Office team would give us a commission we’d reconsider that position.) We then thought, “You know, this is probably the perfect scenario for using regular expressions.” But then we got a headache just thinking about regular expressions and so we abandoned that idea, too.

We then came up with this simple and elegant solution:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

strText = objFile.ReadAll
objFile.Close

arrWords = Split(strText, " ")
Wscript.Echo Ubound(arrWords) + 1

Simple and elegant indeed: all we did here was open the text file C:\Scripts\Test.txt and store the entire text file into a variable named strText. We then used the Split function to split the array on blank spaces (figuring that the only time you would have a blank space would be between words.) Having used the Split function to create an array named arrWords (an array in which each element represents a single word), all we had to do then was echo back the Ubound (upper bound) value of the array, plus 1. (Why plus 1? Because the Ubound value of an array is always the number of items in the array minus 1.)

That worked – sort of. As it turned out, though, the text file we used occasionally had extra blank spaces to align information:

Name                                        Date
Ken Myer                                    3/30/2006
Pilar Ackerman                              3/31/2006

That created a problem: each of those extra blank spaces was counted as being a word. Thus our final word count was a little bit higher than it should have been.

Back to the drawing board:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

strText = objFile.ReadAll
objFile.Close

arrWords = Split(strText, " ")

For Each strWord in arrWords
    If Len(strWord) > 0 Then
        i = i + 1
    End If
Next

Wscript.Echo i

As you can see, this time around we didn’t echo back the Ubound value. Instead, we set up a For Each loop to loop through all the items in the array. Inside that loop we used the Len function to determine the number of characters in each individual item. If the length of the item was 0, that meant we had encountered one of our excess blank spaces. In that case we simply skipped that item (because few words have 0 characters in them). If the length was greater than 0, then we incremented a counter variable by 1:

i = i + 1

After looping through the entire array we then echoed back the value of our counter variable:

Wscript.Echo i

This was much better, but the word count still seemed a little too high. After puzzling this over for a minute or two we realized why. Suppose our text file consisted of this sentence:

Two plus two = four

Most people would say that there are four words in this sentence; however, our script insisted that there were five words in the sentence:

Two
plus
two
=
four.

Why five words? Because the script was counting the equals sign (=) as a word. Likewise, we had other “extraneous” characters in the document: for example, this construction counted as three words all by itself:

. . .

Yuck.

We didn’t like that, and so we modified the script one final time, using a series of Replace functions to replace characters such as the equals sign and the period with blank spaces:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

strText = objFile.ReadAll

strText = Replace(strText, ",", " ")
strText = Replace(strText, ".", " ")
strText = Replace(strText, "!", " ")
strText = Replace(strText, "?", " ")
strText = Replace(strText, ">", " ")
strText = Replace(strText, "<", " ")
strText = Replace(strText, "&", " ")
strText = Replace(strText, "*", " ")
strText = Replace(strText, "=", " ")

strText = Replace(strText, vbCrLf, " ")

objFile.Close

arrWords = Split(strText, " ")

For Each strWord in arrWords
    If Len(strWord) > 0 Then
        i = i + 1
    End If
Next

Wscript.Echo i

This one we liked better. Like our previous scripts, we start this one off by defining a constant named ForReading; this constant tells the FileSystemObject that we want to read the text file (as opposed to writing or appending to it). Next we create an instance of the FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Test.txt. Once we get the FileSystemObject up and running we then use the ReadAll method to read the entire file into a variable named strText:

strText = objFile.ReadAll

Following that we execute a series of Replace functions to replace characters in the variable strText. (Note that we aren’t touching the actual file itself, just the copy of the file stored in memory.) For example, this line of code replaces all the commas in strText with a blank space:

strText = Replace(strText, ",", " ")

We’ll leave it up to you to decide which characters – if any – you want to replace. If you’re OK with the equals sign and the plus sign (+) being counted as individual words then you might not have to make any replacements at all.

Wait, check that: there’s one replacement that you will have to make. Suppose we have a text file that looks like this:

A
B
C
D
E

How many words in this text file? We would have said 5, too, but the script said there was just 1. Why? Well, we told the script to split the text on the blank space; however, this file doesn’t have any blank spaces, just carriage return-linefeeds at the end of each line. Therefore, our array has only one item in it. Ouch.

So how do we overcome that problem? That was actually pretty easy: we just replaced all the carriage return-linefeeds (vbCrLf) with blank spaces:

strText = Replace(strText, vbCrLf, " ")

Once we had blank spaces between each character (rather than carriage return-linefeeds between each character) the script correctly reported back 5 words for this sample text file.

Now where we were? Oh, yeah. After we close the file we then call then Split function to split strText into an array. We then use the For Each loop we already showed you to count the number of words in the array (and hence the number of words in the text file), skipping over excess blank spaces. We then echo back the value of our counter variable and we’re done.

At least to our satisfaction. Whether or not the word count is 100% accurate is somewhat subjective. For example suppose you have this line in the text file:

2+2=4

Do you have 5 words in this line (2, +, 2, =, and 4)? Maybe you have just three words: 2, 2, and 4. Or maybe you just have one word: 2+2=4. (Microsoft Word sees this as being a single word.) You’ll have to make those decisions on your own. As for us, we’ve decided that the next time we find an “easy” question to answer we’ll just skip that one and try something else!


How Can I Extract Information Located Between Two Known Values in a Text File?

Hey, Scripting Guy! In a text file, how can I extract the information that appears between the string HOSTNAME= and a blank space?

-- B

Hey, B. You’ll have to pardon us if we seem a little sluggish today; we’re still recovering from all the fun and excitement of Windows PowerShell Week. What if you missed out on Windows PowerShell Week? Well, then you really blew it, didn’t you? Now you’ll never get to learn about Windows PowerShell, you’ll lose your job, your dog will bite you, and your spouse will pack up and leave.

OK, so maybe there is at least one advantage to having missed Windows PowerShell Week, if you know what we mean.

Just a second, B .…

Sorry; apparently the Scripting Editor has discovered a typo in the first paragraph of today’s column. Instead of, “Well, then you really blew it, didn’t you?” that sentence should read like this: “That’s OK; after all, all the Windows PowerShell Week resources – including links to the on-demand webcasts, the question-and-answer logs, and other related goodies – are available on the Windows PowerShell Week home page.” So we guess you’re not out of luck after all.

And yes, that does mean that you have to keep your job and spouse, at least for the moment. On the other hand, your dog probably won’t bite you. And you can still have the opportunity to learn all about Windows PowerShell.

Of course, one thing that you won’t find on the Windows PowerShell Week home page is a script that can extract the text that appears between the string HOSTNAME= and a blank space in a line in a text file. But don’t fret. Fortunately, we just happened to have a script lying around that can extract this kind of information from a string variable. We’ll show you how this generic script works, then modify it slightly so that it can grab this same information from a text file.

Here’s the script:

strSearchString = "ABCDEFGHIJK, NDPSGW PORT=LPR
HOSTNAME=R2333_HP_1100 ABCDEFGHIJK"

intStart = InStr(strSearchString, "HOSTNAME=")
intStart = intStart + 9

strText = Mid(strSearchString,
intStart, 250)

For i = 1 to
Len(strText)
If Mid(strText, i, 1) = " "
Then
Exit For
Else
strData = strData &
Mid(strText, i, 1)
End If
Next

Wscript.Echo strData

As you noted in your email, B, your text file includes lines that look something like this:

ABCDEFGHIJK, NDPSGW PORT=LPR HOSTNAME=R2333_HP_1100 ABCDEFGHIJK

You need to take these lines of code and find the string HOSTNAME=. Once you find this string you then want to grab all the text that appears after the equals sign, at least until you encounter a blank space. In this example, that means pulling out the text R2333_HP_1100. Let’s see if we can figure out how to do that.

Note. Before we go much further we should probably point out that there are several different ways that we could tackle this chore; for example, we could have used regular expressions to locate the value in question. The approach we chose isn’t necessarily the most sophisticated way of solving the problem, but we thought it was the easiest and most straightforward method; in addition, it was a method we thought the typical system administration scripter would be comfortable with. As usual, we’re less concerned with elegance than we are with simply finding a way to get the job done.

As you can see, we start out by assigning the sample line of text to a variable named strSearchString. We then use the InStr function in the following line of code to determine the starting position for the string HOSTNAME=:

intStart = InStr(strSearchString, "HOSTNAME=")

In this case, the InStr function is going to return the value 30. Why? Because the string HOSTNAME= can be found at character position 30 in the string strSearchString (if you count the characters you’ll see for yourself). Of course, knowing the starting position for HOSTNAME= is useful, but it doesn’t quite solve our problem: we need to know where the string ends. Why? Because that’s where we begin collecting data. Fortunately, that’s an easy obstacle to overcome: HOSTNAME= has 9 characters in it, meaning that all we have to do is add 9 to the variable intStart and we’ll know exactly where the target data begins. That’s what we do with this line of code:

intStart = intStart + 9

Like we said, now we know exactly where the data starts: in this case, that’s character position 39 (30 + 9). (Admittedly, it might seem like the math is off by 1. But, again, count the number of characters and you’ll see that this is OK.) Among other things, that tells us that we can discard the first 38 characters in the string; there’s nothing in there of interest to us. With that in mind we use the following line of code to create a new string (stored in the variable strText) that starts at character 39 and encompasses the next 250 characters:

strText = Mid(strSearchString, intStart, 250)

Note. Why 250? Actually, that’s an arbitrary number: we just wanted to make sure that we grabbed all the remaining characters in the string. And yes, we could have calculated the length of the string and determined exactly how many characters to grab, but we’re assuming you won’t have any line lengths larger than 250 characters. If you do, then you’ll need to modify this line of code accordingly.

And don’t worry: if your string contains less than 250 characters this line of code won’t generate an error. Instead, VBScript will simply take as many characters as do exist and call it good.

Next we set up a For Next loop that begins at 1 (representing the first character in the variable strText) and continues until we reach the end of the string (which we determine using the Len function):

For i = 1 to Len(strText)

What are we going to do inside this loop? Well, for starters, we’re going to check the very first character and see if it happens to be a blank space (remember, when we find a blank space we’ve reached the end of our data). If character 1 is a blank space we call the Exit For command and exit the loop. If character 1 is not a blank space then we use the Mid function and this line of code to add the character to a variable named strData (in the parameters passed to Mid the variable i represents the letter in the string and the 1 tells Mid to grab only that one character):

strData = strData & Mid(strText, i, 1)

What does that mean? In this example, character 1 is the letter R. The letter R is not a blank space, so the value R gets added to strData. We then loop around and check the second character in the string: 2. Again, this is not a blank space, so we append this character to strData, making strData equal to R2. We then loop around and check the third character in the string, continuing until we either run out of characters or until we encounter a blank space.

After we exit the loop we then echo back the value of strData which, with any luck, should be this:

R2333_HP_1100

And there you have it.

Now, how would you go about doing this in a text file? Why, like this, of course:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

Do Until objFile.AtEndOfStream
    strData = ""
    strSearchString = objFile.ReadLine

    intStart = InStr(strSearchString, "HOSTNAME=")

    If intStart <> 0 Then
        intStart = intStart + 9
        strText = Mid(strSearchString, intStart, 250)

        For i = 1 to Len(strText)
            If Mid(strText, i, 1) = " " Then
                Exit For
            Else
                strData = strData & Mid(strText, i, 1)
            End If
        Next
    End If
    Wscript.Echo strData
Loop

We won’t bother explaining the whys and wherefores of reading text files; we have plenty of Hey, Scripting Guy! columns that can help you with that. But now that you have the basic idea about how to locate the target information you shouldn’t have too much trouble figuring out how the text file version of this script works.

We hope that helps, B. And, again, if any of you missed Windows PowerShell Week don’t worry: you can still revel in every minute of the week. Is it true that, during the day 5 webcast, Peter Costantini sang not one but two Windows PowerShell songs? Let’s put it this way: any time you hear a story about Peter, no matter how crazy, you should assume that it’s true.

But why not check out the day 5 webcast and see for yourself?


How Can I Extract the Text Between the Header and Footer in a Text File?

Hey, Scripting Guy! How can I extract the text between the header and footer in a text file?

-- LL

Hey, LL. Before we answer your question we should point out that, depending on when you’re reading this, you might be missing Day 1 of Windows PowerShell Week. Even as we speak the lovely and talented Jean Ross could be presenting an introduction to Windows PowerShell, Microsoft’s new command shell/scripting technology.

Well, OK: right now the talented Jean Ross could be presenting an introduction to Windows PowerShell, Microsoft’s new command shell/scripting technology.

Fine: right now Jean Ross could be presenting an introduction to Windows PowerShell, Microsoft’s new command shell/scripting technology.

Is that better?

But don’t fret if you somehow missed today’s presentation; all the webcasts will be available in archive form within the next 2 or 3 days. Besides, we still have four more webcasts to go; for example, tomorrow the lovely and talented Dean Tsaltas will explain the whys and wherefores of Windows PowerShell Cmdlets.

….

Sorry. We expected somebody to protest the fact that we referred to Dean as being both lovely and talented, but everyone seems fine with that description. Good news, Dean: the scripting world loves you!

Of course, some of you – namely you, LL – might not care about any of that; you just want to know how to extract the text between the header and footer of a text file. Well, then why didn’t you say so:

Const ForReading = 1

x = 0

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile =
objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
strLine
= objFile.ReadLine

If Left(strLine, 4) = "****" Then
x = x + 1
End If

If x = 2 Then
Exit Do
End If

If Left(strLine, 4) <> "****" And x = 1 Then
strText = strText & strLine & vbCrLf
End If
Loop

Wscript.Echo strText

A note of explanation before we begin. The text file that LL referred to uses a line of asterisks to mark the beginning and end of the file header. In other words, the text file looks something like this:

******************************************************************
This is line 1 of the header.
This is line 2 of the header.
******************************************************************

This is text that we don't care about.
This, too, is text we don’t care about.
As is this.

Our job is to pull out just the two lines wedged between the asterisks. You know, these two lines:

This is line 1 of the header.
This is line 2 of the header.

Will our script be able to pull off such a feat? Let’s find out.

As you can see, we start out by defining a constant named ForReading and setting the value to 1; we’ll need this constant when we open our text file. We then set the value of a mysterious variable named x to 0. What do we need this variable for? You’ll find out soon enough.

What do you mean that isn’t soon enough? We understand how exciting this is, but try to be patient. Remember: good things come to those who wait.

Our next step is to create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading. That's what we do with these two lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

After the file has been opened we set up a Do Until loop that runs until we reach the end of the file (or, to make it sound like we know what we’re talking about, until the AtEndOfStream property is True). The first thing we do inside that loop is use the ReadLine method to read the first line of the text file and then store that value in a variable named strLine:

strLine = objFile.ReadLine

Most likely the first line in the text file is a row of asterisks that marks the beginning of the header. However, we don’t know that for sure. Therefore, we use this line of code to verify that the first four characters in the line are, indeed, asterisks:

If Left(strLine, 4) = "****" Then

Note. We’re guessing that you don’t have any other lines in the text file that begin with four asterisks. If you do, then you’ll have to adjust the preceding line of code to take that into account (for example, checking to see if the first 10 or the first 20 or the first whatever characters are all asterisks).

Let’s assume that the first four characters of the line are all asterisks. (If they aren’t, well, no big deal; the script is designed to handle that.) If we have encountered a line of asterisks that means that we’ve hit the line that signifies the beginning of the header. To help us mark this momentous occasion we increment the value of x by 1. In other words, when x equals 1 we know that we've found the header.

That brings us to this block of code:

If x = 2 Then
    Exit Do
End If

What’s this for? Well, when we reach the line in the text file that marks the beginning of the header we increment the value of x by 1. And guess what? We do the very same thing when we reach the line in the text file that marks the end of the header. (Why? Because that line also begins with four asterisks.) That means when we reach the end of the header x will be equal to 2; logically then, if x is equal to 2 we must have reached the end of the header. And because we have reached the end of the header then there's no point in going on; therefore, we use the Exit Do statement to exit the Do Until loop.

Make sense? The variable x is equal to 0 until we hit the line that marks the beginning of the header. At that point x gets set to 1 and remains at 1 until we reach the line marking the end of the header. The value of x will then get upped to 2, which is a signal to the script that it’s time to exit the loop.

Of course, the first time through the loop x won’t be equal to 2; most likely it will be equal to 1. That brings us to this block of code:

If Left(strLine, 4) <> "****" And x = 1 Then
    strText = strText & strLine & vbCrLf
End If

Here we’re checking two conditions: that x is equal to 1 (which means we’re dealing with the header) and that the first four characters in the line are not equal to ****. If both of these conditions are true then we’re actually dealing with the header text, the very text we’re trying to extract. With that in mind we grab the current line (strLine) and append that value plus a carriage return-linefeed character to a variable named strText. We then loop around and repeat the process with the next line in the text file. When we’re all done, we echo back the value of strText.

Admittedly, it’s a tad bit complicated, but it works. In fact, here's what we get back when we run the script:

This is line 1 of the header.
This is line 2 of the header.

Like we hinted at, this script works even if the header doesn't appear right at the very beginning of the document. To see what we mean, try running the script against the following text file and see what happens:

This line we don't care about.

******************************************************************
This is line 1 of the header.
This is line 2 of the header.
******************************************************************

This is text that we don't care about.
This, too, is text we don’t care about.
As is this.

And now, if you’ll excuse us, we need to get back to the Windows PowerShell Week webcasts. Could this be the day that the lovely and talented Peter Costantini breaks into song? You'll find out soon enough.


How Can I Locate a Value in a Text File and Then Parse the Line of Text Where the Value Was Found?

Hey, Scripting Guy! How can I open a CSV file, locate a value in column 1, then store the value in column 2 in a variable?

-- THvdH

Hey, HvdH. We have to be honest with you, it’s a little difficult for the Scripting Guy who writes this column to stay focused on his work today. After all, tomorrow is one of the biggest and best of all holidays in the US: the annual Turducken Bowl.

Thanksgiving? Sure, tomorrow is also Thanksgiving in the US … we think. To tell you the truth, we feel a little sorry for Thanksgiving: it’s a shame it has to fall on the same day as the Turducken Bowl. But those are the breaks, right?

On the off-chance that there’s someone out there who isn’t familiar with the Turducken Bowl, the Bowl is an annual touch football game played between two teams: the Scripting Guy who writes this column and his Scripting Nephew (Jordan) versus the Scripting Son and yet another Scripting Nephew (Blake). To say that this year’s game is the most important event in the history of the universe would be an understatement. The Scripting Guy who writes this column (and his faithful sidekick, Jordan) won the first two Turducken Bowls; that was mainly because, way back then, the Scripting Guy who writes this column was bigger and faster than the Scripting Kids. In year 3, however, the Scripting Guy who writes this column somehow became the smallest and the slowest of all the players on the field. As a result, even though each game in years 3-through-5 were hard-fought, Dylan the Scripting Son and his faithful sidekick won the all 3 games (7 points per touchdown, first one to 100 wins).

Ah, but then came the 2005 Turducken Bowl. In that game, the Scripting Guy and Jordan stunned the crowd (and, interestingly enough, there actually was a crowd, consisting of neighborhood kids who came out to watch) by grabbing a 21-0 lead and then coasting in from there. That tied the series at 3 wins apiece, making this year’s game all the more important.

Note. So what do you get if you actually win the Turducken Bowl? Well, nothing really. In theory you get bragging rights for the year, but by the time we’ve finished running up and down the field everyone’s usually too tired and out-of-breath to do much bragging. But you know how it is: it doesn’t matter whether you get anything, it just matters whether or not you win. Especially when your son is playing for the other team.

At any rate, the Scripting Guy who writes this column is a bit preoccupied today, what with having to plan his strategy for tomorrow’s game. (of course, most years that strategy consists solely of him saying, “OK, Jordan, run out as far as you can and I’ll try to hit you with pass.”) However, in the spirit of the holidays, the Scripting Guy who writes this column has agreed to to take a brief timeout and show you how to write a script that can open a CSV file, locate a value in column 1, then store the value in column 2 in a variable.

You know, a script like this one:

Const ForReading = 1

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

Do Until objFile.AtEndOfStream
    strText = objFile.ReadLine
    If InStr(strText, "HE123-ASD-FRE-R4") Then
        arrText = Split(strText, ",")
        strVariable = arrText(1)
        Exit Do
    End If
Loop

objFile.Close

Wscript.Echo strVariable

Based on your email, HvdH, it sounds like your text file looks something like this, with a bunch of computer names followed by the IP address of that computer:

HE123-ASD-FRE-R01,192.168.1.5
HE123-ASD-FRE-R02,192.168.1.6
HE123-ASD-FRE-R03,192.168.1.33
HE123-ASD-FRE-R04,192.168.1.9
HE123-ASD-FRE-R05,192.168.1.44

Our job – or at least the job of our script – is to locate a specific computer (in this case, HE123-ASD-FRE-R04) and return the IP address: 192.168.1.9. Let’s see if our proposed solution will actually do that.

As you can see, we begin by defining a constant named ForReading and setting the value to 1; we’ll use this constant when we open our text file. After defining the constant we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt. That’s what we do here:

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

Our next step is to create a Do Until loop that will read through the file, line-by-line, until each and every line has been read. (How do we know when each and every line has been read? When the AtEndOfStream property is True.) Admittedly, checking the file line-by-line is sort of a brute force method; there are probably more elegant ways to approach this problem. So then why did we settle for the brute force method? Two reasons. First, we thought this approach was far easier than, say, trying to write a regular expression to ferret out the target value. Second, when it comes to reading text files the FileSystemObject is pretty dang fast: even if your file has thousands of computer names the FileSystemObject can process that file in a matter of seconds. Because of that there’s no appreciable performance penalty in taking the brute force approach over some other method. Given the choice between quick and easy and slightly-quicker but also-more-complicated, well, we’ll take quick and easy every time.

Inside the Do loop we use the ReadLine method to read the first line from the text file and store that value in a variable named strText. We then use the InStr function and the following line of code to determine whether or not the name of our target computer (HE123-ASD-FRE-R4) can be found in that line:

If InStr(strText, "HE123-ASD-FRE-R4") Then

If InStr returns a 0 that means the target computer name doesn’t appear anywhere in the line of text. Therefore, we simply loop around and repeat the process using the next line from the text file.

Ah, but suppose InStr returns something other than 0. (Technically, InStr returns the starting position of the target string if that string can be found). In that case, we execute the following block of code:

arrText = Split(strText, ",")
strVariable = arrText(1)

In the first line we use the Split function to split the line of text into an array named arrText. Because we’re using the comma as the delimiter that means we end up with an array consisting of these two items:

· HE123-ASD-FRE-R04

· 192.168.1.9

The item we’re interested in, of course, is item 2, the IP address. As you know, in VBScript each item in an array is automatically assigned an index number: the first item is given the index number 0, the second item is given the index number 1, and so on. Because we’re interested in the second item (which has an index number of 1) we can use this line of code to grab the value of that item and store it in a variable named strVariable:

strVariable = arrText(1)

At this point we’re pretty much done: we’ve located the target computer, grabbed the IP address, and stored that address in a variable named strVariable. Among other things, that means that we don’t need to read through the rest of file; we’ve already found what we were looking for. Therefore, we call the Exit Do statement to break out of the loop. Once we’re out of the loop we use the Close method to close the file, then echo back the value of strVariable:

192.168.1.9

And now we truly are done. And ready to get back to the real task at hand: tomorrow’s Turducken Bowl.

Note. In case you’re wondering, the name Turducken Bowl is derived from the turducken, a somewhat-mythical Thanksgiving dish in which you take a chicken, stuff it inside a duck, stuff the duck in side a turkey, and then cook the whole thing. To tell you the truth, we don’t know of anyone who has ever actually eaten a turducken, but the Scripting Kids liked the name enough to christen the annual football game the Turducken Bowl.

Incidentally, this is the last Hey, Scripting Guy! column until Monday, November 27th; that’s because we Microsoft employees get the next four days off in honor of the Turducken Bowl. And yes, that also means it will be awhile before the results of this year’s game are publicly available. In fact, depending on who wins, it might be a long while before the results of this year’s game are publicly available.

Note. Actually, the Scripting Guy who writes this column is confident of victory. This year, he’s going to tell Jordan to run out as far as he can, then the Scripting Guy will try to hit him with a pass. Dylan and Blake will never know what hit them.


How Can I Locate a Word in a Text File and Then Echo Back That Word and Any Remaining Text?

Hey, Scripting Guy! How can I search for a word in a text file and then, after I find it, echo back that word and any remaining text in the text file?

-- KA

Hey, KA. Before we begin we’d like to thank the hundreds and hundreds of people who have sent Scripting Guy Jean Ross get well wishes after her unfortunate mishap at Microsoft Management Summit 2007. Not that we’ve actually received any get well wishes, mind you. But we have no doubt that they are all on the way. Consider this a thank you in advance.

Incidentally, and while we’re on the subject, there is a rumor going around that Jean slipped in her hotel room, fell, and cracked her head open. That is definitely not what happened. Instead, Jean arrived at the conference at the very moment the San Diego Convention Center was surrounded by hundreds of ninja warriors, all clad entirely in black except for a small penguin logo just above the heart. While her fellow Microsoft employees cowered in fear, begging for their lives, Jean – oblivious to the danger and willing to sacrifice everything for the good of both Microsoft and all of humanity – leaped into the fray, single-handedly defeating the entire army.

Not that she emerged unscathed, mind you; in fact, at one point a ninja shot a flaming arrow that lodged itself deep into Jean’s skull. Of course, one flaming arrow in the skull isn’t going to stop Scripting Hero Jean Ross. Instead, she simply yanked the arrow from her head and continued the battle. It was only many hours later, after she knew that everyone was safe and that the Summit had been saved, that she consented to go to the emergency room and receive first aid. That’s what really happened to Scripting Guy Jean Ross.

Or so she says, anyway.

So why would a band of ninja warriors attack the Microsoft Management Summit? To be honest, we don’t know. However, we suspect they wanted to shut down the conference before Microsoft could make the week’s most important announcement: the fact that, using a script, it’s possible to locate a specific word in a text file and then echo back just that word and any subsequent text in the file. In fact, you can do so using this script:

Const ForReading = 1

Set obJFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

arrWords = Split(strContents, " ")
intStart = 0

For Each strWord in arrWords
    If InStr(strWord, "telescope") Then
        intStart = intStart + 1
    End If

    If intStart > 0 Then
        strFinalText = strFinalText & strWord & " "
    End If
Next

Wscript.Echo strFinalText

Yes, you can see why the ninjas wouldn’t want word of this to leak out, can’t you? And don’t worry: the Scripting Guys are not afraid to explain how this script works, either. However, before we do that we should note that, for this particular example, we’re assuming that we have a text file that just happens to consist of a quotation from Alice in Wonderland:

Curiouser and curiouser!' cried Alice (she was so much surprised, that
for the moment she quite forgot how to speak good English); 'now I'm
opening out like the largest telescope that ever was! Good-bye, feet!'
(for when she looked down at her feet, they seemed to be almost out of
sight, they were getting so far off).

Our task is to locate the word telescope in this file and, if and when we find the word, echo back the target word telescope and any subsequent text in the file. In other words, our script should echo back the following:

telescope that ever was! Good-bye, feet!'
(for when she looked down at her feet, they seemed to be almost out of
sight, they were getting so far off).

And guess what: that’s exactly what our script does do! Now all we have to do is try and figure out how it does that.

To begin with, we define a constant named ForReading and set the value to 1; we’ll use that constant when we go to open our text file. Speaking of which, we next use the following two lines of code to create an instance of the Scripting.FileSystem object and to open the file C:\Scripts\Test.txt:

Set obJFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as the file is open we use the ReadAll method to read the entire contents into a variable named strContents, and then just as quickly close the file. (Why? Because, with the contents safely tucked away in the variable strContents we no longer need the actual file itself.)

OK; now what do we do? Well, using a trick we employed during the 2007 Winter Scripting Games, we next use the VBScript Split function to split the value of strContents on the blank space:

arrWords = Split(strContents, " ")

What does that give us? That gives us an array (arrWords) consisting of all the individual words (plus any punctuation marks) in the text file, an array that starts out like this:

Curiouser
and
curiouser!'
cried

Etc., etc.

Once we have the array we then set the value of a counter variable named intStart to 0:

intStart = 0

You’re right: at this point in time this doesn’t make much sense, does it? But don’t fret; everything should become crystal-clear in just a moment. After assigning a value to intStart we next set up a For Each loop to loop through all the items (words) in the array arrWords. What’s the first thing we do inside the loop? The first thing we do inside that loop is check to see if the value of the current array item contains the string telescope:

If InStr(strWord, "telescope") Then

Let’s pretend that the very first word in our text file really is the target word telescope. What do we do then? Well, all we do is increment the value of intStart by 1:

intStart = intStart + 1

Why do we do that? To answer that question, take a look at the next block of code:

If intStart > 0 Then
    strFinalText = strFinalText & strWord & " "
End If

As you can see, if intStart is greater than 0 we’re going to assign a value to a variable named strFinalText. That value is going to consist of the current value of strFinalText plus the current array item (strWord) plus a blank space. (Why a blank space? Because that enables us to restore all the blank spaces that disappeared when we called the Split method.)

See what we’re doing here? The first word in our text (and thus the first item in the array arrWords) is the word Curiouser. Does the word Curiouser contain the string telescope? No, it doesn’t; therefore the value of intStart remains 0. In turn, that means we won’t assign a new value to strFinalText; after all, we do that only if intStart is greater than 0. What about the second word in the file: and? Nope, no match there either. And so, once again, we loop around and try the next word.

Of coruse, sooner or later we will hit the word telescope; at that point we’ll change the value of intStart to 1 and assign a new value to strFinalText. And guess what? From that moment on intStart will always be greater than 1. In turn, that means that all the remaining words in the file will be assigned to strFinalText; that’s how we manage to capture both the target word and any subsequent text in the file. When we’ve finished looping through the array we finish the script by reporting back the value of strFinalText, a value that should look just like this:

telescope that ever was! Good-bye, feet!'
(for when she looked down at her feet, they seemed to be almost out of
sight, they were getting so far off).

And that, ninja warriors or no ninja warriors, is the answer the world has been waiting for. Whew!

Now we know what you’re thinking. And, in all fairness, at first the other Scripting Guys were a little skeptical of this whole ninja thing, too. But then we realized something: Jean has been on the team for a little over 2 years now, and during that time none of the Scripting Guys has ever been attacked by ninja warriors, not even once!

Man, she is good, isn’t she?


How Can I Locate Strings That Consist of a Series of Numbers Followed by .ZIP?

Hey, Scripting Guy! I have a file which includes a number of file names. All of these names consist of a series of numbers followed by a .zip file extension; for example, 1234.zip, 5678.zip, etc. How can I write a script that locates all these file names and then saves just the names to a second file?

-- RS

Hey, RS. You know, numbers are very important. Take the number 19, for example. Last night the Scripting Guy who writes this column and the Scripting Son were playing 21, a one-on-one basketball game in which you start off by trying to make a field goal (worth 2 points). After you make a field goal you then step to the foul line and shoot free throws, with each free throw worth 1 point. Furthermore, you are allowed to continue shooting free throws until you miss. The game continues in this fashion until someone reaches 21 points.

Last night the Scripting Son got the ball first and quickly made a basket; he then made three straight free throws, putting him up 5-0. Unfortunately – for him anyway – he missed his next free throw; the Scripting Dad grabbed the rebound, made a field goal, and then ran the table, makinge 19 consecutive free throws to win the game. (Best of all, he made them despite the Scripting Son throwing the ball back hard to him, throwing it back soft to him, throwing it over his head, throwing it at his feet, and doing all those time-honored strategies designed to upset the shooter’s rhythm.)

But we have to let you in on a secret: the Scripting Dad cheated. After all, he didn’t bother to mention that, when he was in fourth grade, he was the school free throw shooting champion at Eastgate Elementary School in Kennewick, WA. The poor Scripting Son had no idea who he was up against.

Note. Want to know a truly sad story? In fourth grade the Scripting Guy who writes this column really was the free throw shooting champion at Eastgate Elementary School. Winning the school championship qualified him for the district championship and could have, in theory, led him all the way to the national championship. Except, of course, the school forgot to tell him about the district championship. He thus never even got a chance to compete for the national championship. Life has pretty much been downhill ever since.

So will any of that help you locate your target file names? Probably not. But this will:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll()
objFile.Close

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Global = True
objRegEx.Pattern = "\d{1,}.zip"

Set colMatches = objRegEx.Execute(strContents)

If colMatches.Count > 0 Then
   Set objFile = objFSO.CreateTextFile("C:\Scripts\Zipfiles.txt")
   objFile.Write strList
   For Each objMatch in colMatches
       objFile.WriteLine objMatch.Value
   Next
   objFile.Close
End If

Before we go into the nitty-gritty details we should point out that we’re assuming you have a text file similar to this:

Here is our first file: 1234.zip.
Another file is 5678.zip.
123456789.zip is the third file.
This file -- 987654321.zip -- is file number 4.

As you can see, there are four file names scattered throughout the contents of this file:

1234.zip
5678.zip
123456789.zip
987654321.zip

If all goes well our script will open the file C:\Scripts\Test.txt, locate all the file names, and then write those file names to a second text file (C:\Scripts\Zipfiles.txt). Granted, if we knew all the file names in advance this would be easy: we could just use a series of InStr commands to see if any of those names could be found in Test.txt. Unfortunately, though, we don’t know the names of the files and we don’t know how many files might be listed in Test.txt; we don’t even know how many characters are in each file name. (For example, 1234.zip has 4 characters in the file name itself, while 123456789.zip has 9 characters in the file name.) Sounds hopeless, doesn’t it?

Well, maybe for some people. But not for a Scripting Guy who can make 19 consecutive free throws to defeat his son.

So how does our script manage to overcome such a hopeless situation? Well, we start out simple enough, defining a constant named ForReading and setting the value to 1; we’ll use this constant when we open and read the text file C:\Scripts\Test.txt. After defining the constant we create an instance of the Scripting.FileSystemObject and open the file Test.txt. With the file open we can then use the ReadAll() method to read the entire contents of the file into a variable named strContents:

strContents = objFile.ReadAll()

At that point we have no further need for Test.txt so we use the Close method to close the file.

Got all that? All we’ve done so far is open the file Test.txt and copy the contents to the variable strContents. What we’ll do now is search for those target file names using the value strContents rather than the actual text file itself. (Why? Because the FileSystemObject doesn’t really provide a way for us to search a text file; we need to make a copy of that file in memory and do our searches on that copy.)

That brings us to our secret ingredient for the day: regular expressions. We’ve already noted that the InStr function – commonly used for locating string values within a text file – is of little use to us here. But that’s OK; regular expressions, while admittedly a bit cryptic at times, are far more powerful than InStr, as well as far more adaptable to situations where all you have is a general idea of what you’re looking for (a string of numbers followed by .zip). InStr, by contrast, works best when you know exactly what you’re looking (i.e., a file named 1234.zip).

Note. Yes, we know: regular expressions might be new to a lot of you. Buy we Scripting Guys think of everything: we already have a webcast that explains the fundamentals of regular expressions.

Before we conduct the search itself we need to do a little preparation. First, we create an instance of the VBScript.RegExp object. Second, we execute these two lines of code:

objRegEx.Global = True
objRegEx.Pattern = "\d{1,}.zip"

In line 1 we set the Global property of the regular expressions object to True; that tells the script that we want to find every instance of the target string. (Had we set this to False the script would have stopped after finding the first file name in the file.) In line 2 we then set the Pattern property to the target string. Believe it or not, this is what we’re searching for:

\d{1,}.zip

Like we said, regular expressions can look a little cryptic at times. With that in mind let’s break this pattern down to its constituent parts:

· \d. The \d indicates that we only want to match digits (0-9). Letters, blank spaces, punctuation marks – we aren’t interested in any of those things. Just numbers.

· {1,}. This odd-looking construction tells the script how many consecutive numbers qualify as a match. The 1 simply says that the target string must have at least 1 number in it; the comma followed by nothing means that there is no limit to the total number of digits in the target string. In others words, a 1-digit number is a match; so is a 4-digit number, and a 10-digit number, and a 7,585-digit number. This is perhaps easier explained by posing a different scenario: what if our target string had to consist of a number with at least 3 digits but with no more than 7 digits? In that case we’d use this syntax: {3,7}. Make sense?

· .zip. Finding a series of consecutive numbers is great, but those numbers then have to be followed by.zip. That’s the reason for adding .zip to the pattern.

In other words, we’re asking the script to search for a number or consecutive set of numbers (it doesn’t matter how many numbers) immediately followed by a .zip. You know, strings such as this:

1234.zip
5678.zip
123456789.zip
987654321.zip

After defining the Pattern we then call the Execute method and actually search the value of strContents:

Set colMatches = objRegEx.Execute(strContents)

Any time you call the Execute method all the instances of the target string that are discovered are stored in the Matches collection (in our script, we use the object reference colMatches to refer to that collection). To determine which file names (if any) can be found in strContents we simply need to set up a For Each loop to loop through all the items in the collection:

If colMatches.Count > 0 Then
   Set objFile = objFSO.CreateTextFile("C:\Scripts\Zipfiles.txt")
   For Each objMatch in colMatches
       objFile.WriteLine objMatch.Value
   Next
   objFile.Close
End If

Oh, right: first we check to see if the value of the Count property is greater than 0. If it is, that means at least one instance of the target string was found. In that case, we then go ahead and use the CreateTextFile method to create a new text file, C:\Scripts\Zipfiles.txt.

With Zipfiles.txt created and ready for business we next set up our For Each loop. Inside that loop we simply use the WriteLine method to write the Value property to Zipfiles.txt. As you can probably guess, the Value property will correspond to the value of the matching string: if we found the string 1234.zip then the Value for that match will be, well, 1234.zip.

After we’ve looped through the entire collection we call the Close method to close Zipfiles.txt. And guess what we’ll see the next time we open Zipfiles.txt:

1234.zip
5678.zip
123456789.zip
987654321.zip

Cool, huh?

OK, maybe not as cool as hitting 19 consecutive free throws in order to defeat your overly-competitive son. (And no, we don’t have any idea where he gets that from.) But it’ll do for now.


How Can I Number the Lines in a Text File?

Hey, Scripting Guy! How can I number the lines in a text file?

-- AS

Hey, AS. You know, after being on vacation for three weeks the Scripting Guy who writes this column has a lot of catching up to do. In turn, that means that you have a lot of catching up to do. For example, did you know that the Seattle area experienced a huge power outage shortly before Christmas? Well, it’s true; in fact, the Scripting Guy who writes this column went six days before power was restored to his house. Did desperation set in for the Scripting Family? Let’s put it this way: they were just about to draw lots to determine which family member would be killed and eaten when the power miraculously came back on.

OK, to be honest it wasn’t quite that bad. For example, the Scripting family room stayed at a (relatively) balmy 50 degrees Fahrenheit, which meant that at least one portion of the house was (relatively) inhabitable. What about the rest of the house? Well, after a couple of days the Scripting Guy who writes this column noticed that the ice in the freezer was starting to melt. Therefore, he dumped all the ice into the sink. Two days later, the ice was still there; it had barely melted at all. In other words, it was warmer in the freezer than it was in the rest of the house.

Which is usually not a very good sign.

Of course, one advantage to not having any electricity was the fact that it gave the Scripting Guy who writes this column lots of time to just sit and think. And what did he think about? Why, how to write a script that would number all the lines in a text file, of course.

OK, that’s not really true, either; instead, he spent most of his time thinking about the best way, if worse came to worse, to barbecue the Scripting Son. (Editor’s Note: Is anyone else getting a little grossed out at this point?) The Scripting Guy who writes this column just said that he thought about writing a script that would number all the lines in a text file because it made a nice lead-in to this, a script which actually does number all the lines in a text file:

Const ForReading = 1
Const ForWriting = 2

i = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strContents = strContents & i & "   " & strLine & vbCrLf
    i = i + 1
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strContents
objFile.Close

Let’s see if we can figure out how this baby works. We begin by defining a pair of constants – ForReading and ForWriting – that we’ll use when working with the text file. We then set the value of a counter variable named i to 1; we’ll use this variable to keep track of line numbers.

Got all that? Good. Now we’re ready to start. To begin with, we use these two lines of code to create an instance of the Scripting.FileSystemObject and then open the file C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

You might have noticed that we opened the file for reading (hence the use of the constant ForReading). As most of you know by now, that’s due to a quirk in the FileSystemObject: you can open a file for reading or you can open a file for writing, but you can’t do both operations (reading and writing) at the same time. Because of that we need to read the file into memory, add the line numbers to this “virtual” copy of the file, then reopen the file for writing and replace the existing lines with the newly-numbered lines.

To that end we first set up a Do Until loop that runs until we reach the end of the file (that is, until the file’s AtEndOfStream property is True). Inside the loop we use the ReadLine method to read the first line of the file, storing that value in a variable named strLine. That brings us to this line of code:

strContents = strContents & i & "   " & strLine & vbCrLf

This is the code we use to construct a new file in memory, a file in which each line is numbered. As you can see, what we’re doing here is assigning a value to a variable named strContents. What value are we assigning to the variable? We’re assigning it the following:

· The current value of strContents (which, the first time through the loop, is nothing).

· The value of the counter variable i (which, the first through the loop, is 1).

· Three blank spaces.

· The value of the variable strLine (which, the first time through the loop, is equal to the first line in the text file).

· A carriage return-linefeed (represented by the VBScript constant vbCrLf).

For example, suppose our text file looks like this:

This is line 1.
This is line 2.
This is line 3.
This is line 4.

When we run through the loop the first time, our “equation” will look like this:

[nothing]
+                            1
+         [three blank spaces]
+ [a carriage return-linefeed]

In turn, that means strContents will be equal to this:

1   This line 1.

We then increment the value of i by 1, loop around and repeat the process with the second line of the text file. At the end of this second loop strContents will be equal to this:

1   This line 1.
2   This line 2.

And so on. When we’re all done strContents will be equal to this:

1   This line 1.
2   This line 2.
3   This line 3.
4   This line 4.

Which, in fortuitous fashion, just happens to be what we want our text file to look like.

In a perfect world we’d now simply replace the old contents of Test.txt with these new and improved contents. As the Scripting Guy who writes this column can attest, however, this isn’t always a perfect world. That’s as true of the FileSystemObject as it is the power grid. We want to simply replace the contents of Test.txt just like we want to be able to go to bed at night without having to wear long underwear, sweatpants, a sweatshirt, and a knitted ski cap. But, as the Rolling Stones warned us years ago, you can’t always get what you want.

Therefore, we need to take a somewhat more circuitous path. (Hey, how about that? We used both fortuitous and circuitous in the same column. We’ll try to fit in gratuitous if at all possible.) For starters, we use the Close method to close our text file:

objFile.Close

We then turn right around and reopen the file, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

As soon as the file is reopened we can then call the Write method and write the value of strContents to Test.txt. After we call the Close method to close the file Test.txt will look like this:

1   This line 1.
2   This line 2.
3   This line 3.
4   This line 4.

And there you have it.

And, to be honest, there truly was one nice thing about the blackout: it gave the Scripting Guy who writes this column an opportunity to prove that he was every bit as rugged and resourceful as people who lived in the days before electricity and modern conveniences. Of course, that’s assuming that those people just hopped in the car and drove to a nearby restaurant any time they got hungry. (Most areas around Seattle had power restored within two days.) And that they were able to while away those long, cold winter nights by attending a college basketball game; the University of Washington lost power only briefly, if at all. And that, instead of huddling together during the day in order to keep warm, they went Christmas shopping at the mall.

But that seems reasonable enough to assume.


How Can I Open a Text File as Unicode?

Hey, Scripting Guy! I have some text files that include Unicode characters. When I try to open those files using a script all I get back is gibberish. How can I open a text file as Unicode?

-- FA

Hey, FA. You know, the truly great magicians don’t concoct elaborate tricks that rely on trap doors, secret panels, and huge puffs of smoke to hide whatever they’re doing. Instead, magicians known that the best tricks are the simple tricks, the ones that rely on something obvious and straightforward, yet something that most people overlook. The Scripting Guys tend to work the same way. When we answer a question we do exhaustive research and then brainstorm for hours, trying to come up with a script that no one but a Scripting Guy could possibly think of. We’re kidding, right? Right. Instead, the Scripting Guys usually rely on something obvious and straightforward, yet something that most people overlook.

It’s not as flashy, but it’s a heck of a lot easier.

Take your question, for example. Let’s say you have a very simple Unicode file, one that consists of the following characters:

abc ėěĩ def

What happens when you use a script to open this file and then echo back the contents? Well, you get back something like this:

Interesting. If you remove all the ASCII characters from the file – leaving just ėěĩ – you get this when you echo back the contents:

That’s even cooler looking. Man, we could do this all day!

But, as our manager just pointed out, what we really could (should) be doing all day is finishing this column. Fair enough; with that in mind here’s a script that will open the Unicode file and correctly echo back the contents:

Const ForReading = 1
Const TriStateTrue = -1

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt",
ForReading,False,TriStateTrue)

strText = objFile.ReadAll
objFile.Close

Wscript.Echo strText

We start out by defining a pair of constants: ForReading and TriStateTrue. ForReading is used to open our text file for reading; TriStateTrue is used for – well, we don’t want to give away the exciting ending just yet. You’ll find out in a moment what TriStateTrue (with a value of -1) is used for.

After creating an instance of the FileSystemObject we then call the OpenTextFile method to open the file C:\Scripts\Test.txt. What do you mean “boring?” Take a look at the parameters we pass to OpenTextFile:

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading,False,TriStateTrue)

The first two parameters probably don’t faze you much: they’re simply the full path to the file we want to open and the constant ForReading. And you’re right: this is standard operating procedure when it comes to reading text files. But what about those other two parameters, one False, the other the constant TriStateTrue?

This is where the Scripting Guys perform their magic. The optional third parameter is a Boolean parameter that, if True, creates the specified text file if the file cannot be found. Because we’re only interested in opening an existing file, we set this parameter to False (which is also the default value).

That brings us to parameter 4, our old pal TriStateTrue. If the fourth parameter passed to the OpenTextFile method is equal to -1 then the file will be opened as a Unicode file. It’s that easy. Leave the fourth parameter off and the file will be opened as ASCII; set the fourth parameter to -1 and – presto-changeo! – your file will be opened as Unicode.

Thank you, thank you; you’re too kind. How about a hand for our lovely assistant, Marjorie?

What’s that? Of course it works. Take a look at the message box we generate:

For our next trick, we will now make one of the Scripting Guys disappear and reappear at his son’s high school baseball game. (Did we mention that the Scripting Kid is batting over .400 three-fourths of the way through the season? Never mind: we don’t want bore you with a Scripting Dad bragging about his son. – Editor’s Note: Only the other Scripting Guys get that pleasure.) See you tomorrow.


How Can I Parse a Comma-Separated Values File?

Hey, Scripting Guy! I have a text file which consists of individual fields separated by commas. I need to look at the second field in each line and, if it includes the word Everyone, I then need to take the sixth field in the line and write that value to a second text file. How can I do that?

-- RP

Hey, RP. You know, a lot of you are probably sitting there thinking, “Gee, I wonder what life in the Seattle area is really like?” Well, we’ll tell you. This morning the Scripting Guy who writes this column was driving to work when he heard sirens; like most people, he promptly pulled over to the side of the road as both an ambulance and fire truck hurtled into view. As the vehicles got closer, a lady who was stopped at a red light suddenly put the pedal to the metal and zoomed across the intersection, forcing the ambulance to screech to a halt. A close call, but, fortunately, the ambulance and the fire truck were able to keep going.

Now, Seattle being Seattle, where do you suppose this lady was going, and in such a hurry that she not only ran a red light, but nearly collided with an ambulance and a fire truck in the process? You’re absolutely right: after running a red light and nearly smashing into an ambulance, the lady pulled into the drive-through lane at Starbucks.

But, then again, you can’t blame her; after all, what are the odds of finding a Starbucks in Seattle? Here’s a little advice for you from the Scripting Guys: any time you see a Starbucks pull in immediately. If you don’t, well, good grief, in that case you might have to drive three or four blocks before you find another one.

Geographic note. In an eight-block stretch right by the Scripting Home, you’ll find two Starbucks, an independent coffee shop, a coffee cart outside a grocery store, and two of those little drive-through coffee huts set up in parking lots. Oh, and a McDonald’s, which sells lattes and iced coffees as well as regular coffee.

Not to mention 7-11, which also sells coffee.

Incidentally, we don’t know what the emergency was or where the ambulance was headed. However, we assume it must have been something pretty serious; otherwise the ambulance driver probably would have pulled into Starbucks as well.

Of course, if you’re like most people, you’re probably tired of hearing about Seattleites and their obsession with coffee. Needles to say, the average person would much rather hear about a script that can parse a comma-separated values file, extract information from selected lines in that file, and then save that extracted information to a new file. Well, you’re in luck. When the lady ran the red light and pulled into Starbucks, the Scripting Guy who writes this column took advantage of the confusion to pull into the drive-through lane of the local scripting store. And, lo and behold, guess what he found there? A script that can parse a comma-separated values file, extract information from selected lines in that file, and then save that extracted information to a new file!

Note. Yes, he was lucky to find one of those. Usually those things sell out in no time.

Here’s the script:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    arrFields = Split(strLine, ",")

    If InStr(arrFields(1), "Everyone") Then
        strContents = strContents & arrFields(5) & vbCrlf
    End If
Loop

objFile.Close

Set objFile = objFSO.CreateTextFile("C:\Scripts\Everyone.txt")
objFile.Write strContents

objFile.Close

Before we launch into an explanation of how the script works, let’s take a look at the text file in question (C:\Scripts\Test.txt):

MBPGPS1PRN-001\,Administrators,LOCALGROUP,S-1-5-32-544,PRINTSHARE,\\MBPGPS1PRN-001\mbbetsb01,OWNER,,
MBPGPS1PRN-001,Everyone,GLOBALGROUP,S-1-1-0,PRINTSHARE,\\MBPGPS1PRN-001\mbbetsb01,(+PrR),,
MBPGPS1PRN-001,Everyone,GLOBALGROUP,S-1-1-0,PRINTSHARE,\\MBPGPS1PRN-002\mbbetsb01,(+PrR),,
MBPGPS1PRN-001,Administrators,GLOBALGROUP,S-1-1-0,PRINTSHARE,\\MBPGPS1PRN-001\mbbetsb01,(+PrR),,
MBPGPS2PRN-001,Everyone,GLOBALGROUP,S-1-1-0,PRINTSHARE,\\MBPGPS2PRN-001\mbbet02,(+PrR),,

As RP noted, each line in this file is made up of several different fields. We need to look at the second item in each line (e.g., Administrators) and determine whether or not that value contains the word Everyone. (No, it doesn’t, at least not in line 1.) If the second item does contain the word Everyone (which is the case for line 2 in the file), then we need to extract the sixth parameter (a printer path like \\MBPGPS1PRN-001\mbbetsb01) and write that information to a second text file.

Got that? Good. Now let’s talk about the script and how it performs its magic.

As you can see, we start out by defining a constant named ForReading and assigning it the value 1; we’ll use this constant when we go to open the file Test.txt. Coincidentally enough, we next create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open Test.txt for reading. That’s what these two lines of code are for:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Before we can read and process the file line-by-line we have to set up a Do Until loop that runs until the file’s AtEndOfStream property is True. (Which is just a hoity-toity way of saying, “Keep reading until you reach the end of the file.”) Inside the loop, the first thing we do is use the ReadLine method to read the initial line in the file and then store that value in a variable named strLine:

strLine = objFile.ReadLine

That brings us to this line of code:

arrFields = Split(strLine, ",")

Here we’re using the Split function to turn the string value strLine into an array; to be a little more specific, we’re asking the Split function to create a new item for the array arrFields each time Split encounters a comma. Why would we do that? We do that because this gives us an array with the following items:

· MBPGPS1PRN-001\

· Administrators

· LOCALGROUP

· S-1-5-32-544

· PRINTSHARE

· \\MBPGPS1PRN-001\mbbetsb01

· OWNER

Is that worth having? You bet it is; after all, each item in the array corresponds quite nicely with the individual fields in our line of text. As we noted earlier, we need to know if the second field in this line of text has the word Everyone in it. Well, now we have a very easy way to check for that:

If InStr(arrFields(1), "Everyone") Then

Here we’re using the InStr function to determine whether or not the word Everyone can be found in the second field of our line of text. Or, more correctly, we’re checking to see if Everyone can be found in the second item in our array. And don’t worry, we really are checking the second item in the array, despite the syntax arrFields(1). In VBScript, arrays are “0-index.” That means that the first item in the array is assigned the index number 0, while the second item in the array (the item we’re interested in) is assigned the index number 1. Hence the value 1 in arrFields(1). If we wanted to look at the third item in the array, then we’d use the syntax arrFields(2).

And so on.

If the target word is not found in arrFields(1) then we simply loop around and repeat the process with the next line in the text file. If the target word is found, then we execute this line of code:

strContents = strContents & arrFields(5) & vbCrlf

All we’re doing here is assigning a value to a variable named strContents, a value consisting of the existing value of the variable plus the value of the sixth item in the array plus a carriage return-linefeed character (vbCrLf). And yes, we specified 5 as the index number for a very good reason: the sixth item in an array will always have an index number of 5.

After we’ve read and processed each line in the text file we call the Close method to close the file Test.txt. We then use this line of code to create a new text file named C:\Scripts\Everyone.txt:

Set objFile = objFSO.CreateTextFile("C:\Scripts\Everyone.txt")

As you might have guessed, this is where we plan on writing the data we’ve extracted from Test.txt, something we do by calling the Write method, passing the method the variable strContents:

objFile.Write strContents

If we then close the file Everyone.txt and examine the contents, we should see something that look like this:

\\MBPGPS1PRN-001\mbbetsb01
\\MBPGPS1PRN-002\mbbetsb01
\\MBPGPS2PRN-001\mbbet02

In other words, these are the printers that the Everyone group has access to. (Well, assuming that’s what the file Test.txt is all about.)

That should do it, RP. Or at least we hope it does; as much as we’d like to stick around and chat about parsing text files, well, we need to get going. Time for a coffee break, you know.

Note. Actually, in Seattle people don’t take coffee breaks; that is, we don’t steal a few moments away from work in order to drink a little coffee. Instead, we take work breaks, stealing a few moments away from drinking coffee in order to do a little work.


How Can I Query a Text File and Retrieve Records That Occurred on a Specific Date?

Hey, Scripting Guy! We have tons of text files in which each line in the file represents a record or an event of some kind. How can I use a script to retrieve records or events that occurred on a specified date?

-- TW

Hey, TW. You know, in your email you sort of apologized for using text files, noting that “I know, I know, we should probably use a database rather than text files.” Take it from us, TW: you have nothing to apologize for. If these text files do what you need them to do, then you should keep using them. Admittedly, there are times when a database might be the most appropriate tool; then again, based on the number of questions we get regarding text files it’s obvious that lots of people still find the old-fashioned log file to be extremely useful. As the saying goes, if it ain’t broke, don’t fix it.

The truth is, people often-times worry about things they shouldn’t worry about. We get lots of letters from system administrators saying, “I have a batch file that works great, but it is just a batch file. Should I convert this to a script?” Well, you can if you want to, but if it works (and works great) why bother? If you’re like the Scripting Guys, you have enough to do fixing things that really do need to be fixed; there’s no sense going around fixing things that don’t need to be fixed.

Good point: not only is that a long, philosophical discussion, but it doesn’t mention baseball even once. Wait a minute: this is supposed to be the Hey, Scripting Guy! column; somehow we’ve wandered into Dr. Phil territory!

On the other hand, Dr. Phil never (well, hardly ever) dispenses code like this:

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

strPathtoTextFile = "C:\Scripts"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=" & strPathtoTextFile & ";" & _
        "Extended Properties=""text;HDR=YES;FMT=Delimited"""

strHireDate = #3/1/2006#
strFile = "Employees.txt"

objRecordset.Open "Select * FROM " & strFile & " where HireDate = #" & strHireDate & "#", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("LastName")
    Wscript.Echo objRecordset.Fields.Item("FirstName")
    Wscript.Echo objRecordset.Fields.Item("Department")
    Wscript.Echo objRecordset.Fields.Item("HireDate")
    objRecordset.MoveNext
Loop

Before we start we should point out that TW has a series of comma-separated values (CSV) files, where the first line in each file is a header row indicating the fields in the file. For example, TW has a file similar to this (note that, in order to make the example a bit easier to follow, we’ve removed several fields):

LastName,FirstName,Department,HireDate
Myer,Ken,Finance,3/1/2006
Ackerman,Pilar,Finance,3/1/2006
Smith,Joe,Resarch,4/1/2006

Is it important that your text files be formatted like this? It’s not just important, it’s crucial. We’re going to use database techniques to retrieve information from the file, and to do that the file needs to be delimited in some way (in this case, using the comma as the delimiter). Ideally, your files should include a header row as well. As long as your text file looks like this you’re in business.

Note. What if your text file doesn’t look like this? Well, in some cases you might still be able to use database techniques to query the file. For more information, take a look at our MSDN column Much ADO About Text Files.

We should note two things here. First, we aren’t going to explain each line of code in-depth; that’s because we’ve already done that in the MSDN column. Second, don’t be scared away by the cryptic nature of some of the code found in the script. As you’re about to see, much of that code is boilerplate that you neither have to worry about nor modify. It’s just code that needs to be there.

The script itself starts out by defining three constants: adOpenStatic, adLockOptimistic, and adCmdText. These constants are used to configure our “database” (text file) query. After defining the constants we create instances of the ADODB.Connection and ADODB.Recordset objects. That brings us to this block of code:

strPathtoTextFile = "C:\Scripts"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=" & strPathtoTextFile & ";" & _
        "Extended Properties=""text;HDR=YES;FMT=Delimited"""

As you can see, we’re doing two things here. First, we assign a path to the variable strPathToTextFile. Note that this path represents the folder in which the text file can be found. It’s not the path to the actual text file itself; that would be akin to C:\Scripts\Employees.txt. It’s just the path to the folder.

Second, we call the Open method to make an ADO (ActiveX Data Objects) connection to the folder C:\Scripts. This is a perfect example of the boilerplate nature of this script: assuming your text file is properly formatted and assuming you’ve assigned the folder path to strPathToTextFile, then you can leave this code exactly as-is; you don’t need to change a thing.

If only everything in life was that easy, huh?

After we make a connection we then assign values to a pair of variables:

strHireDate = #3/1/2006#
strFile = "Employees.txt"

In this script we want to retrieve information about all the employees who were hired on March 1, 2006; consequently, we assign that date to the variable strHireDate. Why did we enclose the date in #’s? That’s easy: to ensure that VBScript treats the value as a date, and not as, say, an arithmetic problem (i.e., 3 divided by 1 divided by 2006). Meanwhile, we assign the name of the text file (Employees.txt) to a variable named strFile.

What does all that mean? It means we’re ready to issue our query:

objRecordset.Open "Select * FROM " & strFile & " where HireDate = #" & strHireDate & "#", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

The query is actually pretty straightforward. One of the fields in our text file is named HireDate. How do we know that? Because that’s one of the field names listed in the file’s header row:

LastName,FirstName,Department,HireDate

When we issue our query we simply request all the records (that is, all the lines in the text file) where the HireDate field is equal to #3/1/2006#. Notice that we use the variable strFile (which contains the name of the text file) at the point in the query where we would typically insert a database table name:

"Select * FROM " & strFile & "

And that’s basically that. Through the magic of ADO you’ll get back a recordset consisting of all the records (all the lines in the text file) where the hire date is equal to 3/1/2006. At that point you simply walk through the recordset and echo back the values:

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("LastName")
    Wscript.Echo objRecordset.Fields.Item("FirstName")
    Wscript.Echo objRecordset.Fields.Item("Department")
    Wscript.Echo objRecordset.Fields.Item("HireDate")
    objRecordset.MoveNext
Loop

Not only do you get the same results as you would if your data was stored in a database, but you use pretty much the same scripting code as well. If this works for you, TW, then we recommend that you hang on to your text files. For obvious reasons, the Scripting Guys would never suggest that you should get rid of someone – er, something just because it isn’t cool. (Not that the Scripting Guys aren’t cool, mind you, but, still ….)


How Can I Read a Text File on a Remote Computer?

Hey, Scripting Guy! How can I read a text file on a remote computer?

-- BM

Hey, BM. We have a confession to make: we’re using you and your question for our own nefarious purposes. Earlier this week we answered a question about reading the last line in a group of text files; in our answer we promised we’d show people how to use the FileSystemObject to perform that same feat on a remote computer. That was fine, except that – in order to maintain the spirit of this column – we couldn’t do that unless we had a question about working with text files on a remote computer. So we chose yours.

So, yes, we’re using you. But on the bright side, you’ll get your question answered. In fact, let’s start with your question first.

As you know, the FileSystemObject (the object used for reading and writing text files) is designed to work locally; in fact, pretty much anything and everything you read about the FileSystemObject will take great pains to point out that you can’t use the object against remote machines. As it turns out, though, that’s not entirely true: that’s because the FileSystemObject is capable of using UNC paths. Suppose the file you want to read is located on a file share (\\atl-fs-01\public\myfile.txt). In that case, opening and reading the text file is as easy as this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objTextFile = objFSO.OpenTextFile _
    ("\\atl-fs-01\public\myfile.txt", ForReading)
strContents = objTextFile.ReadAll
objTextFile.Close

Wscript.Echo strContents

As you can see, we begin by defining a constant named ForReading and setting the value to 1. We then create the FileSystemObject reference and call the OpenTextFile method, passing two parameters: the UNC path to the file in question, and the constant ForReading. At this point we can do anything we want with the file: Just to give you a quick example of working with a text file we call the ReadAll method to read the entire contents of the file into a variable named strContents. We then close the file, echo the value of strContents, and we’re done.

Now, this works great as long as the file in question is in a shared folder. But what if the file is not in a shared folder? In that case, your only recourse is to use the Administrative shares (like C$). This script will read the file MyFile.txt even if the folder C:\Public has not been shared:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objTextFile = objFSO.OpenTextFile _
    ("\\atl-fs-01\C$\public\myfile.txt", ForReading)
strContents = objTextFile.ReadAll
objTextFile.Close

Wscript.Echo strContents

If you don’t use Administrative shares, well, then you’re probably out of luck. (Unless you do something really crazy, like use the WSHController object. But that’s a story for another time.)

And that brings us to our ulterior motive: how can we cycle through all the files in a remote folder and use the FileSystemObject to open and read each of those files? Well, here’s one way:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

strComputer = "atl-fs-01"

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

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

For Each objFile In colFileList
    strFilePath = "\\" & strComputer & "\C$\Logs\" & _
        objFile.FileName & "." & objFile.Extension
    Set objTextFile = objFSO.OpenTextFile(strFilePath, ForReading)
    strContents = objTextFile.ReadAll
    Wscript.Echo strContents
    objTextFile.Close
Next

What we’re doing here is connecting to the remote computer atl-fs-01 and retrieving a collection of all the files in the folder C:\Logs. The tricky part lies in constructing a path to each file; that’s because we need to use an Administrative share path similar to this:

\\atl-fs-01\C$\Logs\MyFile.log

To construct that path we use a little bit of WMI and a little bit of hard-coding:

strFilePath = "\\" & strComputer & "\C$\Logs\" & _
    objFile.FileName & "." & objFile.Extension

What we’re doing is this:

1. Starting with a pair of \’s: \\

2. Adding the name of the computer: \\atl-fs-01

3. Adding a \ and the Administrative share path C$\Logs\: \\atl-fs-01\C$\Logs\

4. Adding the WMI FileName property (which is just the name, minus the file extension): \\atl-fs-01\C$\Logs\MyFile

5. Adding the period between the file name and the file extension (because the period is not part of WMI’s Extension property): \\atl-fs-01\C$\Logs\MyFile.

6. Adding the WMI property Extension: \\atl-fs-01\C$\Logs\MyFile.log

It’s a tad bit complicated, but it builds the UNC path we need. Furthermore, each time through the loop it will substitute a new file name and new file extension (the computer name and the folder path never change). Thus we’ll eventually be able to open – and read – each file in the remote folder.

By the way, thanks for letting us take advantage of your question, BM. We owe you one!


How Can I Read Just a Single, Specified Line from a Text File?

Hey, Scripting Guy! How can I read only a specific line in a text file? For example, how can I write a script that reads only line 16 in a text file and then stores the value of that line in a variable?

-- DL

Hey, DL. You know we haven’t answered a question about text files in awhile, and that’s not something we should brag about; after all, it sometimes seems like every other question we get has to do with reading from and writing to text files. People always ask if we’ve ever met Bill Gates; the truth is we always try to avoid Bill Gates, because we’re afraid he’ll ask us questions about text files.

Of course, the reason we get so many questions about text files is the fact that the scripting technologies built into Windows don’t have very sophisticated text handling capabilities. Take this question, for example. You might think that WSH or VBScript or some scripting object would have a method in which you could say, “Hey, go read line 16 and only line 16 of this file.” No such luck. The FileSystemObject – which provides most of the text-handling capabilities available to scripters – can only start with line 1 of a file and then works its way down. There’s no way to quickly jump to a specified line.

Now, that doesn’t mean we can’t store just the value of line 16 in a variable; it just means we have to use a somewhat less-than-elegant approach. For the sake of brevity, let’s consider a simple text file with five lines:

1
2
3
This is line no. 4 in the text file.
5

We want to store the contents of line 4 – and just the contents of line 4 – in a variable. We can’t jump to line 4 directly, but we can achieve the desired result by using a script like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("mylogfile.log", ForReading)

For i = 1 to 3
    objTextFile.ReadLine
Next

strLine = objTextFile.ReadLine
Wscript.Echo strLine

objTextFile.Close

So what’s going on here? Well, we begin by creating an instance of the FileSystemObject and opening the file Mylogfile.log for reading. We then create a For Next loop that loops from 1 to 3. Inside that loop, we use the ReadLine method to read the current line in the text file.

Why? Well, remember, all we’re really interested in is line 4, but the FileSystemObject won’t let us go directly to line 4; instead, we have to get through lines 1, 2, and 3 first. That’s exactly what this block of code does. The first time through the loop, it calls the ReadLine method; this reads the first line in the text file (although we don’t do anything with the information we read), and then automatically drops us down to the next line in the file (line 2). The second time through, we read line 2, and drop down to line 3. The third time through – that’s right, we read line 3, and then drop down to line 4, the line we’re interested it.

At this point, we’re done with our loop. We call the ReadLine method again, only this time we store the contents of that line in the variable strLine. That’s what we do here:

strLine = objTextFile.ReadLine

For the purposes of our demonstration script, we then echo the value of strLine and close the text file. We weren’t able to use any nifty shortcuts, but we achieved our goal: we managed to get the value of line 4 into the variable strLine.

Like we said, not very elegant, but it works. And it works reasonably fast, too. For example, create a sample log file with 10,000 lines, and modify the script so it stores just the value of line 9999 in a variable. (How would you do that? Change the For Next loop so that it loops from 1 to 9998.) Then echo the value of line 9999. If you try this, you’ll see that – elegance aside – the whole process only takes a second or so. Beauty is in the eye of the beholder, and we find any script that takes only a second or two to complete to be very beautiful indeed.


How Can I Read Just the Last Line of a Text file?

Hey, Scripting Guy! How can I read just the last line of a text file?

-- BM

Hey, BM. You know, if you’ve ever wondered how Hey, Scripting Guy! differs from other daily columns (like, say, Dear Abby), well, here’s one way. Suppose someone writes Dear Abby and says this:

Dear Abby:
My life is all messed up and I need to do something about it. How can I turn my life around and be happy again?
Desperate in Dayton

Most likely Dear Abby would never give Desperate an answer like this:

Dear Desperate:
Well, you can’t. Sorry.

So how do you think the Scripting Guys will answer your question about reading just the last line of a text file? You got it:

Dear BM:
Well, you can’t. Sorry.

But wait, don’t go yet. It’s true that Dear Abby would never say, “You know, Desperate, I can’t help you become happy. But here’s a workaround that will make it look like you’re happy.” The Scripting Guys, however, have no such qualms. In other words, we can’t give you a script that reads just the last line of a text file. But here’s a script that will make it look like you read just the last line of a text file:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
Loop

objFile.Close

Wscript.Echo strLine

The problem we’ve run into here is that the FileSystemObject (the scripting object used to work with text files) knows only one direction: forward. It has to start at the beginning of a file, and it can only continue on towards the end of that file. You can’t specify an alternate starting location and you can’t read backwards (from the end to the beginning). In fact, you can’t even re-read a file: if you reach the end there’s no way to start all over except by closing and re-opening the file. That’s why workarounds are pretty much par for the course when it comes to text files.

The workaround here is that we actually do read the entire script from beginning to end. However, we keep track of only the very last line we’ve read. When we reach the end of the file, we’ll have a variable that contains the value of the last line read; that will also be the last line in the file. When we echo back the value of that variable it will look like we read only the last line (especially because, if nothing else, the FileSystemObject is pretty fast). We didn’t – we actually read the entire file – but no one will ever know. It’ll be our little secret.

As for the code itself, we begin by defining a constant named ForReading and setting the value to 1; we’ll use this constant to tell the FileSystemObject that we want to open a file for reading. We then create an instance of the Scripting.FileSystemObject, and use the OpenTextFile method to open the file C:\Scripts\Test.txt. That brings us to this block of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
Loop

All we’re doing here is reading the file, line by line, until we reach the end (that is, until we’re at the end of the file stream). Each time we read in a line we replace the value of the variable strLine with the text we just read. For example, suppose our text file consists of three lines:

A
B
C

Inside our loop we read in line 1, and thus assign the value A to strLine. We loop around and read the second line, meaning the value B gets assigned to strLine. We loop around again and assign the value C to strLine. Because we’ve reached the end of the file strLine retains the value C, which just happens to be the last line in the file. We then close the file and echo the value of strLine. As far as anyone knows, all we did was read – and report back – the value of the last line in the file.

Yes, very sneaky.

Admittedly, there’s one potential problem with this script. Suppose you have a file that has several blank lines tacked on to the end. Our script will dutifully report back nothing (a blank value) as the last line in the text file. That’s what it should do: after all, the last line in the file is blank. But suppose this is a log file of some type and, for whatever reason, the application that creates this log always throws a few blank lines onto the end of the file. In that case what you’re probably really interested in is the last non-blank line in the file. Here’s a modified version of the script that uses the Len function to check the length of each line that’s read in. If the length is equal to 0, that means it’s a blank line, and the value is not stored in the variable strLine:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strNextLine = objFile.ReadLine
    If Len(strNextLine) > 0 Then
        strLine = strNextLine
    End If
Loop

objFile.Close

Wscript.Echo strLine

Eat your heart out, Dear Abby!


How Can I Retrieve IP Addresses (and Only IP Addresses) From a Text File?

Hey, Scripting Guy! I have a script that uses Netsh.exe to return information from my DHCP servers; all that information is then written to a text file. That works great, except that I need to be able to parse the data and pull out any IP addresses included in that information. How can I write a script that extracts just the IP addresses from a text file?

-- MJ

Hey, MJ. You know, a lot of people are mystified as to why we Scripting Guys do a daily scripting column. “Doesn’t that become a big hassle to have to write a new column every day?” they ask. “Isn’t it hard to come up with new ideas and new things to write about? Wouldn’t it be easier to be a slacker like Scripting Guy Jean Ross, and just write one new column a month rather than one new column a day?”

Well, needless to say, the answer to all those questions is, “Yes.” Yes, it is a hassle; yes, it is hard to come up with new ideas and new things to write about; and yes, we would like to be like Scripting Guy Jean Ross. (Hey, who wouldn’t?) So then why do we continue to write a new Hey, Scripting Guy! each and every day?

Well, that’s partly due to our desire to best Scripting Guy Jean Ross at every turn. (Yo, Jean Ross, check the score: Hey, Scripting Guy! 781 columns, Sesame Script a measly 26 columns!) More importantly, however, publishing a new column each and every day gives us a chance to address your questions and concerns when it comes to system administration scripting.

Not to mention the fact that it also gives us a venue for shamelessly promoting everything the Scripting Guys do. You know, things like traveling to Barcelona, Spain for the 2007 TechEd IT Forum, which modestly bills itself as:

“Microsoft’s premier European conference designed to provide IT professionals with technical training, information and community resources to build, plan, deploy and manage the secure connected enterprise using Microsoft infrastructure products and technologies.”

They’ve got us convinced.

As you might expect, the Scripting Guys are looking forward to the trip: this marks the first time we’ve ever been let out of the country to attend a conference. (Of course, whether or not they’ll let us back into the country has yet to be determined.) Scripting Guys Jean Ross and Greg Stemp will be manning a booth in the Ask the Experts section of the convention center; Greg will be there each and every day, Jean will probably show you up once and call it good. (Editor’s Note: At least Jean will show up, which is more than Greg did at TechEd 2007 in Orlando .) Will we be available to chat and to answer questions? Of course we will. Will we be giving away hundreds of copies of Dr. Scripto’s Fun Book, as well as a whole bunch of Dr. Scripto bobblehead dolls? Of course we will. Will we let you throw darts at a big huge dartboard? Greg says yes we will; Jean says – well, never mind. We’re not allowed to print the things that Jean says any time Greg comes up with a new idea.

Anyway, if you haven’t signed up for the TechEd IT Forum there’s still plenty of time to do so. And if you won’t register for yourself, then do it for the Scripting Guys. After all, we have no desire to explain to our manager why he spent all that money to send us to Barcelona only to have no else show up.

Besides, the scripting world owes us one. After all, aren’t we the ones who showed you all how to write a script that can extract IP addresses from a text file?

Oh, we didn’t show you a script like that? Well, now we have:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strSearchString = objFile.ReadAll

objFile.Close

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"

Set colMatches = objRegEx.Execute(strSearchString)

If colMatches.Count > 0 Then
    For Each strMatch in colMatches
       Wscript.Echo strMatch.Value
    Next
End If

Before we launch into an explanation of how the script works we should give you a peek at MJ’s text file. That file, which uses data retrieved (and formatted) by Netsh.exe, looks a little something like this:

==========================================================================
Scope Address        - Subnet Mask         - State        -Scope Name           -Comment
 192.168.120.0        - 255.255.255.0      - Active       -New York, Class C    - New York, NY C
 Total No. of Scopes = 1

As you can see, we have two IP addresses in this file: 192.168.120.0 and 255.255.255.0.

Note. OK, technically, 255.255.255.0 is the subnet mask, you may or may not want your script to return this value. The script we just showed you returns the subnet mask. What if you don’t want the script to return the subnet mask? Relax; we’ll show you how to do that in just a minute or so.

But first things first. The script starts out by defining a constant named ForReading and setting the value to 1; we’ll use this constant when we open the text file. After defining the constant, we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as the file is open, we use the ReadAll method to read in the entire contents of that file and store that information in a variable named strSearchString:

strSearchString = objFile.ReadAll

And then we simply call the Close method to close the file. At that point, we’re done.

Well, OK, we’re done with the text file; when it comes to extracting IP addresses, however, well, there we still have a little bit of work to do. But that’s all right; the Scripting Guys have never shied away from a little hard work.

Note. Not that we’ve ever actually done any hard work, mind you. We’ve just never shied away from it.

As MJ noted in his email, these IP addresses could be just about anything; there’s no guarantee that they all start with, say, 192.168. Does that complicate the issue of locating these IP addresses? Believe it or not, it doesn’t. At least not if you use regular expressions in your script.

We’re not going to discuss the theory of regular expressions today; for that, you might check out the webcast String Theory for System Administrators: An Introduction to Regular Expressions, presented by the late, great Dean Tsaltas.

Note. OK, technically Dean isn’t really dead; he just moved to Canada. But that’s pretty much the same thing, right?

Note to our loyal Canadian readers. We’re just joking. The Scripting Guys have nothing against Canada; in fact, some of our best friends are Canadians.

Like who? Um, well … like the late, great Dean Tsaltas, that’s who.

At any rate, in order to use regular expressions within our script we first need to create an instance of the VBScript.RegExp object:

Set objRegEx = CreateObject("VBScript.RegExp")

As soon as we create the object we then set the Global property to True; that tells the regular expressions object that we want to search for all the IP addresses in our search string. (If we didn’t set this to True the script would stop after finding the first IP address. Needless to say, that would be a problem if the file actually contains multiple IP addresses.)

That brings us to this line of code:

objRegEx.Pattern = "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"

As it turns out, the Pattern property represents the value that we’re looking for. (In this case, an IP address.) We’ll explain what that cryptic-looking pattern means in just a second. Before we do that, however, let’s take a few minutes to meditate on the nature of IP addresses. What is an IP address? Well, in general, an IP address consists of:

· Anywhere from 1 to 3 digits (the values 0 through 9) followed by a period (e.g., 192.)

· Anywhere from 1 to 3 digits followed by a second period (e.g., 168.)

· Anywhere from 1 to 3 digits followed by one more period (e.g., 120.)

· Anywhere from 1 to 3 digits (e.g., 1)

Believe it or not, that’s an awfully good description of our search pattern as well. The construction \d{1,3} means “Find some digits; there must be at least one digit (1) but no more than three digits (3).” The construction \. Means “Find a dot.” If you follow the pattern all the way through, what search criteria do we end up with? You got it:

1 to 3 digits (the values 0 through 9) followed by a period, then 1 to 3 digits followed by a second period, then 1 to 3 digits followed by one more period, then 1 to 3 final digits.

Crazy, huh? But it all works.

After we define the search pattern our next step is to call the Execute method and perform a regular expressions search on the file contents; that’s what we do here:

Set colMatches = objRegEx.Execute(strSearchString)

If the Execute method finds any text matching the search pattern, that matching text will be stored in a collection named colMatches. That means we can determine whether or not any IP addresses appear in the file simply by checking to see if the collection’s Count property is greater than 0:

If colMatches.Count > 0 Then

If the Count property is greater than 0 then we set up a For Each loop to loop through all the items in that collection. Inside that loop we do nothing more complicated than echo back the match Value (that is, the text that met the search pattern):

Wscript.Echo strMatch.Value

What do we get back when we run this script against MJ’s sample dataset? This is what we get back:

192.168.120.0
255.255.255.0

Don’t want the subnet mask to be included? That’s fine; just add an If Then statement to your For Each loop and filter out any IP addresses that begin with 255. That’s something you can do by using the Left function to determine whether or not the first three characters in the value are equal to 255:

For Each strMatch in colMatches
    If Left(strMatch.Value, 3) <> "255" Then
       Wscript.Echo strMatch.Value
    End If
Next

Run this modified script and you’ll get back the following:

192.168.120.0

Bye-bye subnet mask.

And hello, Barcelona. If you’re planning on attending the IT Forum in Barcelona (November 12-16) drop us a line and let us know (and be sure to pop by the Ask the Experts section and say hi. Oh, and if you know of any good restaurants or any fun things to do in Barcelona we’d appreciate hearing about those as well. After all, even Scripting Guy Jean Ross has to eat.

Well, once a month, anyway.


How Can I Search a Text File for 9-Digit Numbers?

Hey, Scripting Guy! How can I search for user names in a text file? In my case, the user names all consist of 9 digits, although they can be any 9 digits.

-- GG

Hey, GG. Interesting question. You want to search for 9-digit numbers (like 123456789) in a text file; the only problem is that these could be any 9-digit numbers. And that definitely is a problem. If you were looking for a specific 9-digit number you could use VBScript’s InStr function:

intMatch = InStr(strText, "123456789")

If intMatch is greater than 0 that means the value 123456789 was found in the search string strText; if intMatch is 0, then the value couldn’t be found. Simple.

But that won’t work here, not unless you want to create separate InStr calls for each possible 9-digit value. (In case you’re wondering, that would be around 1 billion lines of code.) So if InStr can’t do the job for us, what can?

Did someone say regular expressions? Well, you should have, because that’s the answer. Regular expressions – which are actually built into VBScript – provide a very flexible and very powerful way to search through text data. Regular expressions are used quite a bit in the Unix world simply because so much of Unix administration is built around the use of text files. But as this question illustrates, regular expressions have their use in Windows as well. InStr can’t find any 9-digit number, regardless of the 9 digits; regular expressions can.

Note. Actually, regular expressions can do almost anything. (Hey, we said almost.) We can’t even begin to detail all the uses of regular expressions in one little column. If you’re interested in learning more about this very powerful technology, view the Scripting Guys webcast String Theory for System Administrators: An Introduction to Regular Expressions.

Let’s take a look at a script that can locate any 9-digit numbers in a search string. For this simple example we’ll search a hard-coded value. After we’ve explained how the script works we’ll then modify it to search a text file.

Here’s the script:

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Global = True
objRegEx.Pattern = "\d{9}"

strSearchString = "aaaaaaa123456789qqqqqqqqqqqq234567891aaaa12345678"

Set colMatches = objRegEx.Execute(strSearchString)

If colMatches.Count > 0 Then
   strMessage = "The following user accounts were found:" & vbCrlf
   For Each strMatch in colMatches
       strMessage = strMessage &  strMatch.Value & " (character position " &  _
           strMatch.FirstIndex & ")" & vbCrLf
   Next
End If

Wscript.Echo strMessage

If you run this script under Cscript you should get back information that looks like this; in other words, the script should have located (and reported the position of) the two 9-digit values in the search string:

The following user accounts were found:
123456789 (character position 7)
234567891 (character position 28)

So how did we do this? We used regular expressions, remember? Sheesh.

Oh, right: sorry. Actually, we began by creating an instance of VBScript’s Regular Expressions object (RegExp); that’s what this line of code does:

Set objRegEx = CreateObject("VBScript.RegExp")

We then set two properties of the Regular Expressions object. By setting the Global property to True we’re telling the script to go ahead and search through the entire string and locate all matches; by default, the Regular Expressions object locates only the first match and then stops searching.

The Pattern property represents the item we’re searching for. Note that, in this case at least, we really are specifying a pattern to search for. The \d means “Search for digits; ignore anything that isn’t a number.” The {9} means “Nine consecutive matches.” Taken together we’re saying, “Look for any 9 numbers in a row.” 123456789 will be a match; that’s because there are 9 numbers in a row. 12345678 9 won’t be a match; that’s because we have 8 numbers and then a blank space. Close, but no cigar.

After specifying the pattern we then assign our search string to a variable named strSearchString. That happens here:

strSearchString = "aaaaaaa123456789qqqqqqqqqqqq234567891aaaa12345678"

And now we’re ready to go. This line of code fires off the search and returns a collection of all the matches:

Set colMatches = objRegEx.Execute(strSearchString)

How do we know if our search actually found any 9-digit numbers? The easiest way is to simply check the Count property of the collection; if the count is greater than 0 then at least one match was found. If that’s the case we then loop through the collection and grab the Value and FirstIndex values for each match. (FirstIndex is simply the character position in the string where the match can be found.) We store this information in a variable named strMessage; at the end of the script we then echo the value of this variable. As we noted earlier, that should look like this:

The following user accounts were found:
123456789 (character position 7)
234567891 (character position 28)

Is that cool or what? Of course, the preceding example only searches a string hard-coded into the script. To search a text file you need to use the FileSystemObject to open the file, read the contents into the variable strSearchString, and then execute the Regular Expressions search. Here’s a modified script that searches the file C:\Scripts\Test.txt:

Const ForReading = 1

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Global = True
objRegEx.Pattern = "\d{9}"

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)
strSearchString = objFile.ReadAll
objFile.Close

Set colMatches = objRegEx.Execute(strSearchString)

If colMatches.Count > 0 Then
   strMessage = "The following user accounts were found:" & vbCrlf
   For Each strMatch in colMatches
       strMessage = strMessage &  strMatch.Value & " (character position " &  _
           strMatch.FirstIndex & ")" & vbCrLf
   Next
End If

Wscript.Echo strMessage

Like we said, is that cool or what?


How Can I Search a Text File for Strings Meeting a Specified Pattern?

Hey, Scripting Guy! How can I search a text file of product IDs and retrieve just those lines that meet a specified pattern?

-- WT

Hey, WT. Before we get to today’s question we were wondering if anyone else has seen the commercial for the car that features the all-new “heartbeat sensor?” The idea is that, before you open your car door, you check the sensor, which can detect the heartbeat of anyone who happens to be hiding in the car waiting to pounce on you.

As a general rule the Scripting Guys are opposed to people hiding in cars waiting to pounce on unsuspecting drivers. And we don’t doubt that this very thing has happened before: someone has opened their car door and been pounced upon. Nevertheless, we’d be curious to know how often this sort of thing happens. It makes sense to have smoke alarms in houses; houses do occasionally catch on fire. But do we really need heartbeat sensors in cars? We’re just not sure. To tell you the truth, no one ever hides in any of the Scripting Guys’ cars.

But, then again, that could simply be because no one wants to catch a Scripting Guy.

The Scripting Guy who writes this column finds this all very interesting, in part because there is plenty of research to indicate that even though our lives continue to get better and better people continue to get unhappier and more depressed. That could be due to the fact that money and material goods truly don’t buy happiness. Alternatively, it could be due to the fact that, just when things start looking up, someone invents a new menace to worry about, and provides a solution to a problem no one even knew existed.

Just wondering.

By contrast, the Scripting Guys only provide solutions to problems that do exist. (We’re also responsible for creating many of those problems in the first place. But that’s another story.) For example, some people need to be able to retrieve a list of specific products from a text file. How are they supposed to do that? Here’s how:

Const ForReading = 1

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Pattern = "^[1-9]...GRP"

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strSearchString = objFile.ReadLine
    Set colMatches = objRegEx.Execute(strSearchString)
    If colMatches.Count > 0 Then
        For Each strMatch in colMatches
            Wscript.Echo strSearchString
        Next
    End If
Loop

objFile.Close

Before we explain how the script works we should note that, based on WT’s description, we have a text file similar to this:

1XXXGRPABCEFG
2YYYGRPDEF
AZZZGRPDEF
RTRRABCGRPRTY
YTHJABCPBCOP

WT is looking only for those records (lines in the text file) that meet the following criteria:

· The first character is a number, 1 through 9. This character indicates a specific product type.

· The second, third, and fourth characters are – well, it doesn’t matter. We don’t care about these characters.

· The fifth, sixth, and seventh characters are GRP. These happen to indicate different product groups.

· The remaining characters are – well, again, it doesn’t matter.

Based on these criteria the first two lines in the text file are the only two lines we’re looking for: they both begin with a number and then have GRP in the fifth, sixth, and seventh character spots. Granted, Line 3 has GRP in the designated spot; however, line 3 doesn’t begin with a number. As for lines 4 and 5, well, the less said about them the better.

So how do we go about finding the desired records? For us the easiest way to do that was to use a regular expression; after all, we’re looking for a specific pattern (a number followed by three characters followed by GRP) and regular expressions are very adept at sniffing out patterns (as opposed to finding specific words and phrases). Will our regular expression work? Let’s find out.

The script starts out by defining a constant named ForReading and setting the value to 1; we’ll need this constant when we open our text file for reading. We then use these two lines of code to create an instance of the VBScript.RegExp object and to specify a Pattern for our search:

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Pattern = "^[1-9]...GRP"

Needless to say, the Pattern is the key to getting this script to work; because of that we should take a minute or two to explain the various components of this regular expression. To begin with, we have the ^ character. That simply tells the script that the pattern must be found at the beginning of the search text; that prevents a value like this from being incorrectly tagged as a match:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1XXXGRP

As you can see, the desired pattern is there, but it occurs at the very end of the string, not at the beginning. Therefore, it doesn’t count, at least not with this script.

Next we have this construction: [1-9]. This simply says that the next character in the pattern must be one of the digits 1 through 9 (the square brackets indicate a range of acceptable values). In other words, to be a match the string must start with one of the numbers 1 through 9.

Pretty simple so far, right? Right.

Next up we have three dots: …. What do the dots represent? In a regular expression a dot represents any character (except for the newline character). We use the three dots here simply to indicate that, to meet our pattern, the string must have three characters following the opening digit. What three characters? It doesn’t matter.

Finally, we have the product group code: GRP. After the opening digit we need to have three characters. And after those three characters we need to have the letters GRP, in that order. Any string that fits that complete pattern will be considered a match; any string that doesn’t fit that complete pattern won’t be considered a match.

Note. Does all that make sense? If not, you might want to take a look at String Theory for System Administrators, Scripting Guy Dean Tsaltas’ definitive explanation for how to use regular expressions in a script.

After defining our pattern we next use these two lines of code to create an instance of the Scripting.FileSystem object and to open the text file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

At this point, the game is afoot. Our next step is to set up a Do Until loop that runs until we’ve read each and every line in the text file (technically, until the AtEndOfStream property is True). Inside that loop we use the ReadLine method to read the first line in the text file and store it in a variable named strSearchString. That brings us to this line of code:

Set colMatches = objRegEx.Execute(strSearchString)

What we’re doing here is using the Execute method to determine whether or not our regular expression pattern can be found in the value of strSearchString. If it can, that information will be returned as a collection named colMatches. If it can’t well,, then colMatches will end up a collection consisting of 0 items.

With that in mind all we have to do now is check to see if the collection colMatches has anything in it. If it does, we echo back the value of strSearchString. (Why? Because that’s the value that meets our pattern.) If it doesn’t, we simply loop around and repeat the process with the next line in the text file. All of that takes place in this block of code:

If colMatches.Count > 0 Then
    For Each strMatch in colMatches
        Wscript.Echo strSearchString
    Next
End If

When we’re all done we close the file Test.txt and then sit back and admire the results:

1ABCGRPABCEFG
2DEFGRPDEF

Beautiful.

Incidentally, the Scripting Guys couldn’t resist: just a few minutes ago we all went out to test the new heartbeat sensor. Somewhat to our surprise it seemed to work pretty good, at least until the Scripting Editor hid in the car; with her in there the sensor failed to detect a heartbeat. Does that mean that, as many of us have long suspected, the Scripting Editor truly is heartless? We can’t say that for sure; she wouldn’t let us do an autopsy. However, we do know, for sure, that she at least has something in her head: emergency room staples. We don’t know the details either, but you can find out more by reading her daily dispatch from the Microsoft Management Summit.

Searching for a String Pattern Using Windows PowerShell

Another way to solve this problem, courtesy of Microsoft’s very own June Blender:

This task is exceptionally easy to do in Windows PowerShell because the PowerShell expression parser is designed to interpret regular expressions.

Here's the same solution in Windows PowerShell. You can enter this command at the PowerShell command line or save it as a script file (.ps1).

get-childitem file.txt | select-string -pattern ^[1-9]...GRP | foreach {$_.line}

The first command uses the Get-ChildItem cmdlet (similar to dir or ls) to find the text file. The pipeline operator (|) sends the output to the next command.

get-childitem file.txt

The second command uses the Select-String cmdlet to search for the regular expression in the File.txt file. The Pattern parameter (-pattern) specifies the regular expression. Because both VBScript and PowerShell use standard regular expressions, the syntax of the regular expression in this PowerShell command is identical to the one that you use in the VBScript script.

select-string -pattern ^[1-9]...GRP

The third command uses the ForEach-Object cmdlet (alias = foreach) to select the Line property of each match object from the output. Without it, the command displays the file name and the matched characters. With it, the command displays only the matched characters.

foreach {$_.line}

Both the VBScript and PowerShell solutions for this task really demonstrate the power of regular expressions. Although regular expression syntax isn't easy, it's so useful that it's worth taking the time to learn it.

In Windows PowerShell, start with the About_Regular_Expression topic: get-help about_regular_expression.


How Can I Search Every Text File in a Folder For Phone Numbers?

Hey, Scripting Guy! In yesterday’s column you showed us how to search for phone numbers in a text file; you also promised us that in today’s column you’d show us how to modify that script so that it can search through all the text files in a folder. Are you still going to do that?

-- SE

Hey, SE. Yes, we’re going to – wait a second. SE? Interesting; you have the exact same initials as the Scripting Editor, the same Scripting Editor who began nagging the Scripting Guy who writes this column the very second he said he was going to do a two-part Hey, Scripting Guy! this time around. “Two parts? Why are you doing two parts?” said our SE. “You always say you’re going to do a second part or a third part of something and then you never do it. What I think you should do is blah, blah, blah, blah ….”

Note. To be honest, she didn’t actually say “blah, blah, blah, blah.” Or at least we don’t think she did. By then we’d kind of lost interest.

At any rate, SE – whoever you are – yes, we are going to show you how you can search for phone numbers in all the text files found in a specified folder. Before we do that, however, we feel we should apologize to our readers. After all, we assume that none of you were able to sleep last night, tossing and turning and fretting and fuming, thinking to yourself, “How could they do this to me? How am I supposed to sleep, heck, how am I supposed to do anything until I find out how this story ends? How can I search for phone numbers in all the text files found in a specified folder?” You guys are right: that was a terrible thing for us to do, and we apologize. Never again will we do a multi-part Hey, Scripting Guy! column.

At least not until the next time we do a multi-part Hey, Scripting Guy! column.

And yes, we know: the suspense is killing you, isn’t it? Therefore, without any further ado, let’s take a look at a script that can open up all the files in the folder C:\Scripts and then search for (and echo back) any phone numbers found in those files:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")

strComputer = "."

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

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

For Each objFile In colFileList
    strName = objFile.FileName
    strName = Replace(strName, "_", " ")
    Wscript.Echo strName

    Set objFile = objFSO.OpenTextFile(objFile.Name, ForReading)
    strSearchString = objFile.ReadAll
    objFile.Close

    Set objRegEx = CreateObject("VBScript.RegExp")

    objRegEx.Global = True
    objRegEx.Pattern = "\d{3}-\d{3}-\d{4} \([a-zA-Z]*\)"

    Set colMatches = objRegEx.Execute(strSearchString)

    If colMatches.Count > 0 Then
       For Each strMatch in colMatches
           Wscript.Echo strMatch.Value
       Next
    End If

    Wscript.Echo
Next

You’re right: this is a fairly long script, at least for a Hey, Scripting Guy! column. But don’t fret; remember, we went over most of this in yesterday’s column. Therefore, we’re going to skip the part about using regular expressions to search for phone numbers and focus, instead, on getting that search to take place against all the files in a folder.

Our first step in that journey (after defining the constant ForReading and after creating an instance of the Scripting.FileSystemObject) is to connect to the WMI service on the local computer. And before you ask, no, you can’t use this script as-is to search text files on a remote computer; that’s because the FileSystemObject isn’t designed for working remotely. That doesn’t mean that it’s impossible to search files on a remote machine, it just means that you’ll have to make some additional modifications before you can do so. For more information, see our column How Can I Read a Text File on a Remote Computer?

Hmmm, you know we could turn this into a three-part column – well, never mind; we don’t like the look the Scripting Editor just gave us. Forget we even mentioned that.

After we make the connection to the WMI service we then use this line of code to retrieve a collection of all the files found in the folder C:\Scripts:

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

What’s that? You say that your text files aren’t in a folder named C:\Scripts? Uh-oh ….

Nah, just kidding: all you have to do is replace the path C:\Scripts with the path to the folder where your text files are located. For example, if your text files are in D:\Employees then you need to use this line of code:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='D:\Employees'} Where " _
        & "ResultClass = CIM_DataFile")

Admittedly, these Associators Of queries are nasty looking, to say the least. But their bark is far worse than their bite.

Once we get our collection back we then set up a For Each loop to loop through all the items in that collection (that is, through all the files in the folder C:\Scripts). The first thing we do inside that loop is execute this block of code:

strName = objFile.FileName
strName = Replace(strName, "_", " ")
Wscript.Echo strName

As we noted yesterday (typical Scripting Guys: always living in the past) we have a series of files with names like this:

Jonathan_Haas.txt
Ken_Myer.txt
Pilar_Ackerman.txt

As you can see, each file is named after the employee whose personal data is stored in that file. Does that matter? Yes, it does. Yesterday we only searched one file (Ken_Myer.txt); because that file path was hard-coded into our script it wasn’t too hard to figure out whose phone numbers we were retrieving. (No, sorry, Scripting Editor; the correct answer is this: Ken Myer’s.) Today, however, things have changed: today we’re retrieving a bunch of different phone numbers belonging to a bunch of different people. Needless to say, we need some way to keep track of which phone numbers belong to which employee.

And thanks to our file-naming convention we can do this simply by retrieving the name of the file and then making a tiny little tweak to the file name. In our first line of code we retrieve the value of the FileName property (that’s the name of the file, minus the file extension) and store it in a variable named strName:

strName = objFile.FileName

That’s going to make strName equal to this:

Jonathan_Haas

That’s OK, but we decided to go one step further and replace the underscore character with a blank space. Interestingly enough, we do that by using the VBScript Replace function, replacing the underscore (_) with a blank space (“ “):

strName = Replace(strName, "_", " ")

Guess what we see onscreen when we echo back the new value of strName? Um, no, sorry again, Scripting Editor; the correct answer is this:

Jonathan Haas

Note. This is a tough day today for the Scripting Editor, isn’t it? We think she’s just in shock because the Scripting Guy who writes this column said he was going to do something (write both parts of a two-part series) and then he actually went ahead and did what he said he was going to do.

Which means there really is a first time for everything.

After echoing back the employee name we’re now ready to search the first file (Jonathan_Haas.txt) for phone numbers. From here on most of the script is exactly the same as the code we showed you yesterday. One major exception: when we call the OpenTextFile method (which we need to do in order to open and read the text file) we don’t use a hard-coded file path like C:\Scripts\Ken_Myer.txt. Instead, we pass the method the value of the Name property (which just happens to be equivalent to the file path):

Set objFile = objFSO.OpenTextFile(objFile.Name, ForReading)

From there we go ahead and read the file, store the contents in a variable named strSearchString, then use a regular expression to search those contents for phone numbers. After echoing the back any phone numbers found in the file we then use the Wscript.Echo statetment to echo a blank line to the screen. Having finished with Jonathan Haas, we then loop around and repeat the process with the next file in the collection. When we’re all done our screen should look something like this:

Jonathan Haas
425-555-4241 (office)
425-555-9977 (home)

Ken Myer
425-555-1212 (office)
425-555-5656 (cell)
425-555-3434 (home)

Pilar Ackerman
425-555-2134 (office)
425-555-6341 (cell)
425-555-8900 (home)

And there you have it. Do you want to perform some task (like search for phone numbers) against all the files in a folder? No problem; just follow this pattern: Use WMI to retrieve a collection of all those files, then use the Name property to grab the path for that file (for example, if you need to pass that path to another object, like the FileSystemObject). Readers are always asking us how to perform reading/writing tasks against all the files in a folder; well, now you know!

At any rate, that concludes our two-part episode on searching text files for phone numbers. Like we said, we’ll try to steer clear of multi-part series’ from now on. Granted, that’s going to cause some problems for the Scripting Guys; for one thing, we’re not totally sure what we’re going to do with the first 67 columns for our intended 300-part series on declaring variables. But don’t worry about it; we’ll think of something.

Note. Were we really going to write a 300-part series on declaring variables? No, not really. Instead, we were just going to write the first 5 or 6 parts and then simply re-run them over and over again. We figured that, after reading 5 consecutive columns on declaring variables, there wouldn’t be any readers (or editors) left to notice that we were just running the same 5 columns day after day.

And you’re right: some days it’s kind of hard to tell anyway, isn’t it?


How Can I Search For Phone Numbers in a Text File?

Hey, Scripting Guy! I have a bunch of text files, one for each of our employees. Included in these text files are phone numbers: work phone, home phone, cell phone, etc. How can I write a script that searches for and extracts the phone numbers from these text files?

-- MN

Hey, MN. You know, many years ago the Scripting Guy who writes this column read The Hitchhiker’s Guide to the Galaxy. Now, being a Scripting Guy, he can’t help but admire anyone who writes a trilogy that actually contains five books. However, he still feels obligated to point out that the Hitchhiker’s Guide informed everyone that the answer to the ultimate question of life, the universe, and everything (a question that hasn’t even been asked yet) is 42. Sorry, but that’s simply not right. Instead, the correct answer is 1301.

Why 1301? Well, we’re assuming that the ultimate question of life, the universe, and everything is this: if I go to TechEd 2007 in Orlando, how in the world will I ever track down the Scripting Guys? The answer? By remembering the magic number: 42.

Oh, wait; sorry. The magic number is 1301, not 42. Go into the Partners Expo and look for booth 1301, the CMP Media booth. See the guy in a ratty old black hat with the word Japan on it and the woman with a bunch of staples in her head? Well, there you go: you just found the Scripting Guys. We’re interested in talking to as many people as we can (or, to be more accurate, as many people as we can before we can slip out the back door and head for Splash Mountain.) So if you find yourself in Orlando June 4-8 be sure to drop by and say hi, and to give us an idea of what we can do to serve you better.

Remember that number: 1301. The same year that Scripting Guy Peter Costantini was born.

Now that we know the answer to the ultimate question of life it’s time to turn our attention to the next ultimate question of life: how can I extract phone numbers from a text file? According to MN’s email she has a series of text files, each of them looking something like this:

Ken Myer
Accountant
Fabrikam, North American Division
Building 16, Room 129
Redmond, WA 98052
425-555-1212 (office), 425-555-5656 (cell)
Wife’s name: Sarah
Children: Robert (4), Teri (2)
425-555-3434 (home)

What MN needs to do is read through these files and extract the phone numbers, like so:

425-555-1212 (office)
425-555-5656 (cell)
425-555-3434 (home)

That doesn’t sound too hard, does it? Unfortunately though, there’s at least one complication: the text files are not necessarily consistent. For example, in Ken Myer’s text file his home phone appears on line 9. However, suppose Ken wasn’t married and suppose he didn’t have any children. In that case, his text file would look like this, and his home phone would appear on line 7:

Ken Myer
Accountant
Fabrikam, North American Division
Building 16, Room 129
Redmond, WA 98052
425-555-1212 (office), 425-555-5656 (cell)
425-555-3434 (home)

Likewise, while the phone numbers will always be in the same format: Area Code-Prefix-Suffix (phone type), the area codes can (and will) differ, as will the phone types. For example, some people don’t have cell phones. In other words, about all we can do is search for a particular pattern and then report back each instance of that pattern. But how in the world can we do that?

Well, here’s one way:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Ken_myer.txt",
ForReading)

strSearchString = objFile.ReadAll

objFile.Close

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\d{3}-\d{3}-\d{4} \([a-zA-Z]*\)"

Set colMatches = objRegEx.Execute(strSearchString)

If colMatches.Count > 0 Then
For Each strMatch in colMatches
Wscript.Echo strMatch.Value
Next
End If

Before we begin explaining how this script works, take a deep breath and relax: this is nowhere near as complicated as it might look. As you can see, we actually start off in very simple fashion, defining a constant named ForReading and setting the value to 1. (We’ll need this constant when we go to open the text file). Speaking of which, our next two steps are to create an instance of the Scripting.FileSystemObject object and then use the OpenTextFile method to open the file C:\Scripts\Ken_myer.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Ken_myer.txt", ForReading)

As soon as the file is open we use the ReadAll method to read in the entire file and store the contents in a variable named strSearchString. And once we have the contents stashed safely away in strSearchString we go ahead and close the text file.

Now comes the tricky part.

If we were searching for a particular value (e.g., the phone number 425-555-1212) we could simply use VBScript’s InStr function. However, we aren’t interested in any one phone number; as we noted before, we’re really look for a phone number pattern. Any time you’re searching for a pattern you need to use a regular expression. That’s also true for the Scripting Guys: any time we’re searching for a pattern we need to use a regular expression. Hence the next line of code, which creates an instance of the VBScript.RegExp object:

Set objRegEx = CreateObject("VBScript.RegExp")

Note. If you’re already lost you might want to take a look at String Theory for System Administrators, Scripting Guy Dean Tsaltas’ immortal introduction to regular expressions for regular people.

Once we have a regular expression object we need to configure two key properties of that object. First, we set the Global property to True; this tells the regular expression object that we want to search the entire target string (strSearchString) for all instances of the pattern. If Global is set to False then the regular expression object will find the first instance of the pattern and then stop. In this case, that’s no good: it would find the first phone number and then quit, without looking for any additional phone numbers for Ken Myer.

And then it’s time to define our search Pattern:

objRegEx.Pattern = "\d{3}-\d{3}-\d{4} \([a-zA-Z]*\)"

Maybe you better sit down; you look a little dizzy. That’s the problem with regular expressions: you take one look at the syntax and your first thought is to panic. Listen, don’t panic (as the Hitchhiker’s Guide repeatedly implored). Regular expression syntax isn’t very pretty, but it can be explained.

To begin with, our phone numbers always start with the area code; that means a phone number will always start with three numbers back-to-back-to-back. Because of that, our regular expression also starts with three numbers; that’s what the \d{3} is for. (The \d means digits, the {3} means there must be three consecutive digits.) After the area code we have a hyphen followed by three more digits (the prefix). And guess what? In our regular expression we also have a hyphen followed by three more digits (again using \d{3}). In the phone number the prefix is followed by another hyphen and four more digits. And in our regular expression – that’s right, we have the exact same thing: \d{4}. (Note that we use the syntax {4} because we’re now looking for four consecutive digits.)

That’s actually all the information we need to find phone numbers. However, each phone number of followed by a parenthetical statement that tells us the type of phone number (e.g., (cell)). We thought it might be nice to grab these parenthetical statements as well. Therefore, we decided to gussy up our pattern a little bit.

So how did we gussy up our pattern? Well, if you look closely you’ll see there’s a blank space immediately following the telephone suffix; that corresponds to the blank space in a phone number like 425-555-1212 (office). Following the blank space we use this construction to represent the opening parenthesis: \(. (Why the \ before the parenthesis? That’s because the open and close parentheses are reserved characters in regular expressions. The \ simply tells the script to look for the actual open parenthesis character; that is, look for the ( character.)

You might note that, at the end of the pattern, we use a similar construction to represent the close parenthesis: \).

OK, so open and close parentheses aren’t too hard. But what about the stuff that goes between the parentheses? At the very least that could be office, cell, or home; how do we search for something that could be almost anything?

To be honest, there are probably all sorts of ways to do that. (Regular expressions are nothing it not flexible.) Because everything within the parentheses is going to be a word (of some kind) we decided to go this route:

[a-zA-Z]*

We’ve actually got several things going on here. To begin with, we’re interested only in letters, either lowercase or uppercase. In a regular expression, you can search for a range of values by enclosing that range within square brackets and by using hyphens as needed. Inside our square brackets we actually have two items: a-z and A-Z. The a-z syntax simply says, “Find anything that begins with a lowercase letter (a-z)”; as you might expect, the A-Z syntax says “Find anything that begins with an uppercase letter (A-Z).” The net effect is that we’re searching for any letter (lowercase or uppercase). If there’s a number or a punctuation mark or anything but the letters A through Z then this won’t be considered a match.

OK, so we’re searching for letters (and only letters) enclosed in parentheses. But how many letters are we searching for? Well, we don’t know. The word office has 5 letters; the word cell has 4. Technically, we’re looking for 0 or more letters; that’s what the asterisk (*) represents. In other words, we’re looking for a phone number, followed by a blank space, followed by a parenthetical statement that contains 0 or more letters (and only letters).

Confused? We don’t blame you. Truthfully, the best way to understand how regular expressions work is to play with them a bit. For example, try removing the a-z section from the pattern and then running the script. The script won’t find any phone numbers. Why not? Because, with a-z removed, all that’s left is A-Z, meaning that we’re looking only for uppercase letters (e.g., OFFICE). Now, go into the text file, make everything uppercase, and try the script again.

Speaking of trying the script, here’s what we get back when we try the script:

425-555-1212 (office)
425-555-5656 (cell)
425-555-3434 (home)

Which is exactly what we wanted to get back.

OK, so now we know how to locate phone numbers in a text file. That’s good, but it doesn’t fully answer MN’s question; after all, she asks how to perform this task on scores of text files. That’s a good question, and it’s one we get asked quite a bit: how do I perform the same task on a whole bunch of text files, all at the same time. That’s something we’ll address tomorrow, as the conclusion to a rare two-part Hey, Scripting Guy! column.

Note. Will that result in these two columns becoming a collector’s item of some kind? Sure; why not?

In conclusion, remember, that’s booth 1301 (the CMP Media booth) in the Partners Expo hall. The Scripting Guys will be there, they’ll have a fun little giveaway for everyone, and you’ll have the opportunity to win your very own Dr. Scripto bobblehead doll.

Actually we should clarify that a bit: Scripting Guy Greg Stemp will be there in booth 1301. However, if her trip to Florida goes anything like her trip to San Diego you’ll need to go to the Orlando Regional Medical Center if you want to talk to Scripting Guy Jean Ross. But be sure and drop by there and say hi. (And maybe give her a pint of blood or something while you’re there.)

Editor’s Note: Scripting Guy Jean Ross had the staples removed weeks ago, is perfectly healthy, and will be in booth 1301. Or so she insists.


How Can I Search for Two Items in a Text File?

Hey, Scripting Guy! You’ve shown us how to search a text file for a single word or phrase, but how can I search a text file for two phrases? I need to know if a file contains either Windows 2000 or Windows XP.

-- JR

Hey, JR. You know, it’s hard enough to get the Scripting Guys to do one thing; getting them to do two things would seem to be well-nigh impossible. But we’ll tell you what: as long as you don’t mind us showing you an easy way to search for multiple items in a text file then we’ll show you how to search for multiple items in a text file.

Note. Why is this the “easy way?” Well, we’re not going to bother setting up an array or some other complicated framework for carrying out multiple searches. Instead we’re going to search the file once for the first term, then search it a second time for the second term. It’s not elegant, but it’s easy, and it works.

Here’s a simple little script that can tell you if either the term Windows 2000 or the term Windows XP can be found in the text file C:\Scripts\Text.txt:

Const ForReading = 1
blnFound = False

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)
strContents = objFile.ReadAll
objFile.Close

If InStr(strContents, "Windows 2000") Then
blnFound = True
End If

If InStr(strContents, "Windows XP") Then
blnFound = True
End If

If blnFound Then
Wscript.Echo "Either Windows 2000 or Windows XP appears
in this file."
Else
Wscript.Echo "Neither Windows 2000 nor Windows XP
appears in this file."
End If

This script begins by defining a constant named ForReading and setting the value to 1; we’ll need to use this when we open our text file. We also create a variable named blnFound and assign it the value False; we’ll use this to keep track of whether or not either of our search terms are found in the file. If we find at least one of the terms we’ll change the value of blnFound to True; otherwise, the value of blnFound will remain False.

Next we open the file C:\Scripts\Test.txt for reading, then use the ReadAll method to read the entire contents of the file into a variable named strContents; we’ll actually conduct our searches on this “copy” of the file stored in memory. Because we don’t need the physical file any more, we call the Close method to close the file.

Now we’re ready for our first search. This line of code uses the InStr function to determine whether the string Windows 2000 can be found anywhere in the variable strContents:

If InStr(strContents, "Windows 2000") Then

If InStr is True, then we set the value of blnFound to True; if InStr is False we simply skip to the next search. In that next search we repeat the process, this time searching for the string Windows XP:

If InStr(strContents, "Windows XP") Then

If we found either Windows 2000 or Windows XP (or both), blnFound will beTrue; if we didn’t find either phrase, then blnFound will still be False. At the end of the script we check the value of blnFound, and indicate whether or not one or more of the search phrases were found in the file.

But what if you need to know if the file contains both of the search phrases? We won’t explain this is any real detail, but here’s a script that will tell you whether or not both of the target phrases can be found in the file:

Const ForReading = 1
intFound = 0

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)
strContents = objFile.ReadAll
objFile.Close

If InStr(strContents, "Windows 2000") Then
intFound = intFound + 1
End If

If InStr(strContents, "Windows XP") Then
intFound = intFound + 1
End If

If intFound = 2 Then
Wscript.Echo "The text file contains both Windows 2000
and Windows XP."
Else
Wscript.Echo "The text file does not contain both
Windows 2000 and Windows XP."
End If

Yes, it is similar to our previous script. The big difference is that we don’t use a True-False variable; instead we use a counter variable named intFound. The script first searches for Windows 2000; if the phrase is found then intFound is incremented by 1. (Because intFound starts off at 0, that means intFound would now be equal to 1.)

The script then searches for Windows XP and, if the phrase is found, increments the value of intFound by 1. The net result? At the end of the script, intFound will equal 2 only if both target phrases were found; if intFound equals 0 or 1, then either neither target phrase or only one target phrase was found. All that’s left at that point is to echo back the results of our search.


How Can I Tally Items in a Text File?

Hey, Scripting Guy! How can I tally items in a text file?

-- JA

Hey, JA. If we understand you correctly you have a text file that consists of a number of lines, each line representing a particular item. Just to keep things simple, let’s say your text file consists of city names:

City
Seattle
Seattle
Los Angeles
Chicago
Seattle
Los Angeles
Pittsburgh

What you’re looking for is a script that can read through the file and report the number of occurrences for each item; in other words, a script that can return a report similar to this:

Pittsburgh 1
Chicago 1
Los Angeles 2
Seattle 3

There are several different ways you can do this, but we decided to show you a very powerful and flexible method for dealing with text files as databases (which is essentially what you want to do). It’s not well-known, but you can actually use ADO (ActiveX Data Objects) to run database queries against a text file. And that’s exactly what we do with this script:

On Error Resume Next

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordset = CreateObject("ADODB.Recordset")

strPathtoTextFile = "C:\Scripts"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=" & strPathtoTextFile & ";" & _
        "Extended Properties=""text;HDR=Yes;FMT=Delimited"""

strFile = "City_Names.txt"

objRecordset.Open "Select City, Count(City) AS CountOfCity FROM " & strFile & _
    " GROUP BY City", objConnection, adOpenStatic, adLockOptimistic, adCmdText

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("City"), objRecordset.Fields.Item("CountOfCity")
    objRecordset.MoveNext
Loop

As you might expect, most of this script is devoted to creating ADO Connection and Recordset objects and then connecting to the text file as though it was a database. We won’t discuss those steps in any detail today; if you’re interested you might take a look at this Scripting Clinic column on the topic. What we’ll focus on today is the query we use to retrieve data from the text file, as well as the lines of code that display the returned information.

Let’s start with the SQL query we use to retrieve data:

objRecordset.Open "Select City, Count(City) AS CountOfCity FROM " & strFile & _
    " GROUP BY City", objConnection, adOpenStatic, adLockOptimistic, adCmdText

At first glance you might be confused by this query. As you probably recall, our text file consists of only a single field: each line in the file contains a city name and nothing else. And yet, even though we have only one field in our “database,” our SQL query specifies two separate items: City and Count(City) AS CountOfCity. What gives?

Actually we aren’t selecting two different fields; instead we’re selecting one field (City) and one “calculated field” we can use to tally up the number of times each city appears in the database. (A calculated field is a field that we calculate based on data found in other fields; the field itself does not actually exist in the database.) The item Count(City) AS CountOfCity (in conjunction with the clause GROUP BY City) can be read like this: Count up the number of times each city appears in the file and then store that number in a calculated field named CountOfCity.

In other words, the query will generate a recordset that looks like this, with Pittsburgh and Chicago as instances of the City field and 1 and 1 instances of the CountOfCity calculated field:

Pittsburgh 1
Chicago 1

The nice thing is that we get back this recordset with the city totals calculated for us. Alternatively we could have opened the text file, read each line, and used an array or a Dictionary object to keep track of the number of times each city name appeared in the file. But ADO makes this process far easier.

You might have noticed this as well: instead of selecting from a database table (because we have neither a database nor any tables), we specify the name of our text file in our SQL query. Thus, we select data from the variable strFile, which just happens to represent the name of our text file (City_Names.txt).

Once we get a recordset back we simply use this code to loop through all the records, echoing back the name of the city and the number of times each city appears in the file:

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("City"), objRecordset.Fields.Item("CountOfCity")
    objRecordset.MoveNext
Loop

As you can see, we do that by echoing the values of two items: City and CountOfCity. Even though CountOfCity is a calculated field, it’s treated as if it was a real field actually stored in the database. All in all, a very easy way to retrieve information (as opposed to raw data) from a text file.


How Can I Tally Up All the Words Found in a Text File?

Hey, Scripting Guy! While browsing the Internet I found a script that showed me how to get a list of all the unique words in a text file. That’s useful, but I’d like to go one step further: how can I determine the number of times each of those words occurs??

-- TZ

Hey, TZ. As it turns out, that’s a lot easier than you might think. In fact, all you have to do is – hang on a second, we just got an email. And not just any email: based on the subject line – underpaid and not appreciated? – this must be a legitimate email that’s truly intended for the Scripting Guy who writes this column. Let’s see what it says:

I wanted to write and tlel you about a great new De!gree program, i tried it out and it worked!

I got my M!asters in 2 weeks ;]. Call the folowing number, this program really works great i was very surprised!

Now that’s a good deal. Although you might find this hard to believe, the Scripting Guy who writes this column already has a Masters degree, and from the University of Washington to boot. (Further proof that the value of a college education is highly overrated.) But the Scripting Guy who writes this column didn’t get his degree in just two weeks; instead, it took him almost two years. Furthermore, in his degree program he had to know spelling and grammar along with everything else. This new program sounds way better! And while we aren’t positive that a M!asters degree is the same thing as a Masters degree, as long as it enables the Scripting Guy who writes this column to get the money and appreciation he deserves, well ….

Anyway, it looks like the Scripting Guy who writes this column will be going back to college, at least for two weeks anyway. That means he has a lot to do: buy some cinder blocks to build a bookcase; stock his cupboards with Top Ramen and boxed macaroni-and-cheese; and write home to his parents asking if they can send him some money. Oh: and show you how to tally up the words found in a text file. You know, by using a script like this one:

Const ForReading = 1

Set objDictionary = CreateObject("Scripting.Dictionary")

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

strText = objFile.ReadAll
objFile.Close

strText = Replace(strText, ",", " ")
strText = Replace(strText, ".", " ")
strText = Replace(strText, "!", " ")
strText = Replace(strText, "?", " ")
strText = Replace(strText, ">", " ")
strText = Replace(strText, "<", " ")
strText = Replace(strText, "&", " ")
strText = Replace(strText, "*", " ")
strText = Replace(strText, "=", " ")
strText = Replace(strText, vbCrLf, " ")

arrWords = Split(strText, " ")

For Each strWord in arrWords
If Len(strWord) > 0 Then
If objDictionary.Exists(strWord)
Then
objDictionary.Item(strWord) = objDictionary.Item(strWord) + 1
Else
objDictionary.Add strWord, 1
End If

End If
Next

colKeys = objDictionary.Keys

For Each strKey in colKeys
Wscript.Echo strKey & " -- " &
objDictionary.Item(strKey)
Next

In case any of you are thinking that the Scripting Guy who writes this column is too old and too out-of-touch to go back to college, we can set your mind at ease by pointing out that he cheated in order to finish today’s assignment: in particular, he copied an existing script from the Internet and then just modified it slightly to meet his needs. And even though he cheated, he still waited until the last possible minute to complete the assignment. If that doesn’t sound like a college student, well, we don’t know what does.

Let’s take a few minutes to discuss how this script works; that will be good practice when it comes time for the Scripting Guy who writes this column to defend his master’s thesis. (And yes, we are a little concerned about having just two weeks to complete 45 credits of coursework and write a master’s thesis. But no doubt this school knows what it’s doing.)

The script starts out by defining a constant named ForReading and setting the value to 1; we’ll need this constant when we open our text file. Next we create an instance of the Scripting.Dictionary object. Why? We’ll get to that in just a second. For now, let’s forget about the Dictionary object and focus on the next two lines of code, which create an instance of the Scripting.FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Once the file is open we use the ReadAll method to read the entire file into memory and store it in a variable named strText:

strText = objFile.ReadAll

Now that the contents of Test.txt are stored in memory we call the Close method and close the file.

Our next task is to identify the individual words in the file. In general that’s fairly easy; as you’ll see, we simply use the Split method to create an array from all the words in strText. And how will we identify the individual words? By splitting on the blank space (“ “), acting under the assumption that all the words in the file are separated by blank spaces.

For the most part, that will work pretty well. However, there is a potential problem here. For example, consider this sample text file:

I saw the cat. The cat was black.

How many times does the word cat appear in this file? We’d agree: it appears twice. However, our script won’t agree; instead, the script sees two different words that happen to include the letters c-a-t:

· cat.

· cat

See the problem? It’s the period immediately following the first instance of cat. Because our script doesn’t know anything about punctuation (which definitely makes it a candidate for a M!asters degree) it doesn’t know to ignore the period at the end of the sentence. Punctuation – and carriage return-linefeeds – can create problems in this script. Therefore, we use a series of Replace commands to find these characters and replace them with blank spaces:

strText = Replace(strText, ",", " ")
strText = Replace(strText, ".", " ")
strText = Replace(strText, "!", " ")
strText = Replace(strText, "?", " ")
strText = Replace(strText, ">", " ")
strText = Replace(strText, "<", " ")
strText = Replace(strText, "&", " ")
strText = Replace(strText, "*", " ")
strText = Replace(strText, "=", " ")
strText = Replace(strText, vbCrLf, " ")

That turns our practice file into something that looks like this:

I saw the cat  The cat was black

In turn, our script now tells us that the word cat appears twice.

Note. For a somewhat more detailed discussion of this issue, see this Hey, Scripting Guy! column.

After we’ve cleaned up our text file, we then use the Split function to create an array consisting of the individual words found in the text file. For our simple example, that means the array arrWords will contain these items:

I
saw
the
cat
The
cat
was
black

And now it’s time to start tallying the number of times each individual word occurs. That’s what this block of code, and the Dictionary object, is for:

For Each strWord in arrWords
    If Len(strWord) > 0 Then
        If objDictionary.Exists(strWord) Then
            objDictionary.Item(strWord) = objDictionary.Item(strWord) + 1
        Else
            objDictionary.Add strWord, 1
        End If

    End If
Next

What are we doing here? Good question. The first thing we’re doing is setting up a For Each loop that will loop through all the items in the array; in other words, through all the words in the text file. For each word we first verify that the length (Len) is greater than 0 characters. (Why? See our previous column on this topic for details.) Assuming that the length is greater than 0 we then use the following line of code the see if the word in question already exists in our Dictionary:

If objDictionary.Exists(strWord) Then

Let’s assume that the word can’t be found in the Dictionary. In that case, we use the Add method to add the word as a new Dictionary Key. At the same time, we set the value of the corresponding Dictionary Item to 1:

objDictionary.Add strWord, 1

Why 1? Because, so far, we’ve found 1 occurrence of that particular word.

If the word already exists in the Dictionary we don’t try adding it a second time; that would cause an error. Instead, we simply increment the value of the Item property by 1:

objDictionary.Item(strWord) = objDictionary.Item(strWord) + 1

You probably don’t need us to tell you this, but if the Item was equal to 1 then, after we execute this line of code, the Item will be equal to 2. You probably also don’t need is to tell you why we chose to use the Dictionary object; unlike an array, it’s easy to 1) locate a specified key; and 2) determine whether a key already exists. (See this Sesame Script article for more on how the Dictionary object works.

From there all we do is loop around and repeat the process with the next word in the array.

After we’ve finished our For Each loop we use this block of code to report back all the Keys and Item values in the Dictionary:

colKeys = objDictionary.Keys

For Each strKey in colKeys
    Wscript.Echo strKey & " -- " & objDictionary.Item(strKey)
Next

That’s going to give us a report similar to this:

I -- 1
saw -- 1
the -- 1
cat -- 2
The -- 1
was -- 1
black – 1

And yes, it would be nice if those words were sorted alphabetically, wouldn’t it? But that’s a task for another day.

As for going back to college, the Scripting Guy who writes this column is actually having second thoughts. Granted, the idea that you could get a Masters degree in two weeks is a bit suspicious; it’s even more suspicious that the telephone number provided in the email is an unlisted number. But the big problem is that, as near as he can tell, the alleged school has neither a football team nor a basketball team. No football team or basketball team? Then why even have a college?

Besides, there’s no need for him to waste two weeks of his life getting a Masters degree. After all, according to another email he just received the Scripting Guy who writes this column can make $50,000 a month while working home; that works out to $600,000 a year. Sure, that would be a bit of a pay cut, but it might be worth giving up a little money for the chance to work from home.


How Can I Tell Whether a Phrase Occurs At Least Twice in a Text File?

Hey, Scripting Guy! How can I tell whether or not the phrase 226 transfer complete occurs at least twice in a text file?

-- JR

Hey, JR. You know, this was a tough one for us to answer; that’s because the Scripting Guys are always content with just one of everything. Take the Scripting Guy who writes this column, for example. He has one son; he writes one column; he was right one time in his life. (Editor’s Note: When was that?) A second piece of pie? No, thank you; one is plenty.

Um, what kind of pie are we talking about?

The point here (assuming that there is a point here) is that determining whether or not a particular phrase occurs more than once in a text file is something a Scripting Guy would never do; we’re not the greedy type. On the other hand, though, if someone asked us for help it wouldn’t be very polite to ignore them, would it? Tell you what: we’ll see what we can do.

Oh: and we’ll take that second piece of pie, too. Just to be … polite.

As you pointed out, JR, this task is actually a bit more complicated than it might first appear. Sure, you can use the InStr function to determine whether the string 226 transfer complete appears in the file (although even there we have a problem, as we’ll explain in a moment). However, InStr simply gives you back a yes-no answer: yes, the target phrase was found, or, no, the target phrase was not found. What InStr won’t tell you is how many times the target phrase can be found.

Of course, we could try using the Split command to split the contents of the file on the target phrase; that would give us an array that – after a little mathematical wizardry – would eventually tell us how many instances of the target phrase occurred. Except for one thing: as JR noted, there’s no guarantee that all the words of the target phrase will appear on the same line of the text file. For example, suppose we had this very simple text file:

226
transfer complete

Does the phrase 226 transfer complete appear in this file? Believe it or not, it doesn’t: if you try splitting the file on the phrase 226 transfer complete nothing will happen. Why not? Because of the carriage return-linefeed that appears at the end of the first line. Technically, this is the string that makes up the contents of our sample file:

226 vbCrlf transfer complete

That’s a problem.

Now, admittedly, there are a couple of clever ways that we could manipulate this file and then still use the Split function to count up the number of times the target phrase appears. But the Scripting Guys are too lazy to do anything clever. Because of that, we used this script instead:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\scripts\Test.txt")

strContents = objFile.ReadAll

objFile.Close

Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.IgnoreCase = True
objRegEx.Global = True
objRegEx.Pattern =
"226\W{1,}transfer\W{1,}complete"

Set colMatches = objRegEx.Execute(strContents)

Wscript.Echo "Total Matches: " & colMatches.Count

The secret to this approach is that we use a regular expression to search for the phrase 226 transfer complete. Why do we use a regular expression? That’s easy: regular expressions return a collection of all the matches found. To determine how many instances of our target phrase occur in the file all we have to do is determine how many items are in the collection of matches.

Of course, even with a regular expression we can’t just search for the phrase 226 transfer complete. Why not? Because we still face the problem of handling instances of the target phrase that get broken across lines:

226
transfer complete

Did we find a way to deal with that problem? To find out, just keep reading.

Note. Sorry, but we don’t want to spoil the suspense.

Let’s see if we can figure out how the script works. To begin with, we define a constant named ForReading and set the value to 1; we’ll use this constant in order to open the text file for, well, reading. We create an instance of the Scripting.FileSystemObject , then use this line of code to open the file:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as we have the file open we use the ReadAll method to read in the contents of the file and store that information in a variable named strContents. We need to do this because we can’t actually search the file itself; instead we need to search a copy of the file stored in memory. And because we can’t actually search the file itself we then use the Close method to close the file as soon as we’ve finished reading in the contents.

Now the fun begins. We start out by creating an instance of the VBScript.RegExp object. We then configure two property values for the regular expressions object:

· IgnoreCase. We set this value to True, which means our search will not be case sensitive. (In other words, 226 Transfer Complete and 226 transfer complete will both register as matches.)

· Global. We set this value to True to ensure that we locate all instances of the target phrase. If set to False the regular expression object would look for the first instance of the target phrase and then stop looking.

That brings us to this line of code:

objRegEx.Pattern = "226\W{1,}transfer\W{1,}complete"

As you might have guessed, the Pattern property represents our target phrase. If you squint your eyes and hold your monitor up to the light, you can probably see the words 226, transfer and complete in the value we’re assigning to the Pattern property. But what’s the deal with those two instances of \W{1,}?

Good question. We’ve already determined that we can’t just search for the phrase 226 transfer complete. Why not? Well, for one thing, the word 226 could be followed by a blank space; however, it could also be followed by a carriage return-linefeed. And that makes a big difference: 226 blank space is definitely not the same thing as 226 carriage return-linefeed.

Fortunately, regular expressions are designed to deal with ambiguous situations like that. What does \W{1,} mean? To begin with, the \ tells VBScript that the next character in the string is a special character; in other words, we’re saying, “Don’t look for a W. Instead, look for a ‘non-word’ character.” In regular expressions, a non-word character is any character that does not begin with a letter or a number. Neither the blank space nor the carriage return-linefeed begins with a letter or a number, so \W enables us to match either a blank space or a carriage return-linefeed.

Cool, huh?

So then what’s the {1,} for? What we’re doing here is specifying the number of non-word characters allowed to come after 226. The 1 tells the script that there must be at least one non-word character after 226. The comma followed by nothing tells the script that while there must be at least one non-word character there could be more than one; we’re fine with that. And that’s good, because, technically, a carriage return-linefeed actually consists of two characters: the carriage return and the linefeed. That’s why we can’t match just one character: criteria like that would find the blank space but not the carriage return-linefeed.

Note. So how do we know all this stuff? Well, to tell you the truth; we don’t. What we do know, however, is how to look up regular expression syntax in the VBScript Language Reference.

Of course, we need to put this same little construction – \W{1,} – after the word transfer. That’s because we could end up with either a blank space or a carriage return-linefeed in there, too.

From here on out it’s easy. With this line of code we call the Execute method; in turn, this causes the regular expression object to start searching through the strContents, looking for the Pattern we set just a second ago:

Set colMatches = objRegEx.Execute(strContents)

As we mentioned earlier, the Execute method returns a collection of all the matches found (a collection we named colMatches). To determine how many instances of the target phrase appear in the text file we simply echo back the value of the collection’s Count property:

Wscript.Echo "Total Matches: " & colMatches.Count

That’s all we have to do.

Well, that and go ahead and take a third piece of pie. After all, we don’t want you to think we left that last piece of pie because we didn’t like it. Although we’d just as soon not eat it we don’t want to hurt anyone’s feelings.

Could you pass the whipped cream, please?


How Can I Tell Which Numbers Are Missing From a Sequence of Numbers Found in a Text File?

Hey, Scripting Guy! How can I look at a sequence of numbers in a text file and identify which numbers are missing from the sequence?

-- TP

Hey, TP. Don’t you know that it’s Friday? This sounds like a tricky question to answer, and the Scripting Guys have an agreement with Microsoft that we don’t have to think very hard on Fridays. OK, granted, Microsoft isn’t aware that we have such an agreement. But let’s not nitpick over details. The point is that there’s absolutely no way we’re going to answer this question, at least not today. Sorry, but that’s just the way it goes.

Oh, come on, TP; don’t look at us with those sad, puppy dog eyes – OK, fine, we’ll make a deal with you. We’ll give you an answer to your question; however, this answer will be based on the assumption that the numbers in your text file are already in numerical order (that is, 1-2-3-4 and not 2-1-4-3). If the numbers are not in numerical order you’ll need to sort them before the script we’re about to show you will work. (How can you sort the contents of a text files? This Hey Scripting Guy! column offers one suggestion.)

With that in mind, let’s assume we have a text file containing the following sequence of numbers:

2
4
8
9
10

How are we going to determine which numbers (1, 3, 5, 6, and 7) are missing from the sequence? Like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt")

i = 1

Do Until objFile.AtEndOfStream
    intLine = CInt(objFile.ReadLine)

    Do Until x = 1
        If intLine = i Then
            Exit Do
        End If
        Wscript.Echo i
        i = i + 1
    Loop
    i = i + 1
Loop

objFile.Close

What’s that? Explain how this script works? Come on, TP; didn’t we mention that it was Friday?

Oh, all right. We start off by defining a constant named ForReading and setting the value to 1; we’ll need this constant in order to open and read our text file. Next we create an instance of the Scripting.FileSystemObject, and then use the OpenTextFile method to open the file C:\Scripts\Test.txt.

That brings us to this line of code:

i = 1

Sure, that looks like a meaningless line of code. In reality, though, it’s the key to this entire script. We’re going to use the counter variable i as a way to keep track of which number we should be on in our sequence. We set i to 1 because the first number in our sequence (and thus the first number in the text file) should be 1. If the first number in the text file is 1 and the value of i (the first number in the sequence) is 1 that means that the number 1 is present and accounted for. If the two items don’t match then the number 1 must be missing.

Don’t worry; that will make more sense in a minute or so.

After assigning a value to the counter variable we set up a Do Until loop that runs until the entire file has been read (or, if you want to impress your friends and neighbors, until the AtEndOfStream property is True). Inside that loop the first thing we do is read in the initial line from the text file and assign that value to a variable named intLine:

intLine = CInt(objFile.ReadLine)

Note. What’s the CInt for? That’s a VBScript function that converts character data to an integer value. We’re doing that to make sure VBScript treats the line of text as a number and not as a string.

We’re now ready to see if the first number in the text file (which, in our example, happens to be 2) is equal to the first number in our sequence (1). To do that we set up a second Do Until loop, one that’s designed to run until x is equal to 1. (Technically, x will never be equal to 1; this is just a little trick designed to keep the loop running until we explicitly tell it to stop.) Inside this second loop we use the following line of code to determine whether or not the value read from the text file (intLine) is equal to the first number in the sequence (the counter variable i):

If intLine = i Then

As you probably know, 2 is not equal to 1. (No, really: we wouldn’t kid you about that.) Therefore, we do these two things:

· We echo the value of i. Why? Because the value of i (which, the first time through the loop, is equal to 1) is not in the text file.

· After reporting that the value 1 was not found we then increment the value of i by 1. That makes our counter variable equal to 2 (1 + 1), which is, of course, the next value in the sequence.

We then loop around and check to see if the value read from the text file (we’re still working with the first line in the text file, so the value of intLine is still 2) is equal to the new value assigned to the counter variable (2). In this case the two numbers are equal; therefore, we don’t want to echo the value because the value (2) does exist in the text file. Instead we call the Exit Do statement to exit the secondary loop. Having found what we were looking for (the value 2) we’re ready to move on.

The Exit Do statement pops us back to the main loop; from there we first increment i (which now gives i a value of 3), read the second line in the text file and repeat the process all over again. The second time around the value read from the text file (4) will not be equal the value of the counter variable (which, as you recall, is now set to 3). Therefore, we echo 3, increment the counter (to 4) and check that against the value from the text file (also a 4).

Etc., etc.

When all is said and done we should get back a “report” that looks like this:

1
3
5
6
7

Hope that helps, TP. And, now seeing as how it is Friday we – what’s that? No, we are not going to wash your car for you. No, sorry; we have to draw the line somewhere. And don’t look at us with those sad puppy dog eyes, don’t – fine; we’ll go get a bucket and the hose.

So much for TGIF.


How Can I Use ADO to Open a Text File That has Spaces in the File Name?

Hey, Scripting Guy! How can I use ADO to open a text file that has spaces in the file name?

-- TL

Hey, TL. You might not even remember sending us this question; to say it’s been awhile would be an understatement. (You should have seen all the dust and cobwebs we had to brush off this question before we could post it in the Script Center.) A long time ago you wrote to us asking a simple question: how can you use ADO to open a text file if that file happens to have spaces in the file name? That seemed easy enough, but – to tell you the truth – we couldn’t figure it out. We tried inserting double quotes and single quotes, we tried using short file names, and we scoured the Web looking for the answer. In baseball parlance, we struck out.

Note. We even read the collected works of Albert Einstein and Sir Isaac Newton, hoping one of them had come up with an answer. Unfortunately, they don’t appear to have had any more luck than we did.

So did we give up? As a matter of fact, we did. Nevertheless, we always had this in the back of our minds, and every now and then we’d try something different, just in case. Each time we tried something new we failed.

And then the other day, while looking up some information on Microsoft Excel, we stumbled upon the answer. (Actually we stumbled upon the answer to a different question, but as it turned out the same technique that worked for Excel also works for text files.) So at long last, TL, we can give you (or at least your great-great-grandchildren) an answer to the question, “How can I use ADO to open a text file that has spaces in the file name?”

Before we do that we should probably explain the situation to the rest of the world. ADO (ActiveX Data Objects) enables you to use database techniques to read data from a text file. We won’t go into all the details today; for more information, take a look at this Scripting Clinic column on MSDN. But suppose you have a simple text file that looks like this (just pretend there are a thousand or so lines in the file):

Name,Department,Title
Ken Myer,Finance,Fiscal Specialist
Pilar Ackerman,Research,Manager
Jonathan Haas,Headquarters,Fiscal Specialist

If you’d like a list of all the Fiscal Specialists, you can easily retrieve that information using an ADO script similar to this, and without having to read through the text file line-by-line:

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

strPathtoTextFile = "C:\Scripts\Text files"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=" & strPathtoTextFile & ";" & _
        "Extended Properties=""text;HDR=YES;FMT=Delimited"""

strFile = "Employees.txt"

objRecordset.Open "SELECT * FROM " & strFile & " WHERE Title = 'Fiscal Specialist'", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("Name")
    Wscript.Echo objRecordset.Fields.Item("Department")
    Wscript.Echo objRecordset.Fields.Item("Title")
    objRecordset.MoveNext
Loop

What we’re doing here is connecting to the folder C:\Scripts\Text files and then querying the file Employees.txt. Here’s the line of code that specifies the name of the file we want to work with:

strFile = "Employees.txt"

This works great: the script will run, and it will report back the name, department, and title of all the Fiscal Specialists.

So what’s the problem? Well, suppose our text file is named Fabrikam Employees.txt. No big deal, right? After all, you just change the value of the variable strFile to this:

strFile = "Fabrikam Employees.txt"

Well, that’s what we thought, too. But here’s what we got when we ran the script:

Microsoft JET Database Engine: Syntax error in FROM clause.

Yikes. Obviously the space in the file name Fabrikam Employees.txt is causing problems; that we could figure out. What we couldn’t figure out was how to work around that problem. We tried all sorts of different permutations and combinations of single quotes and double quotes, only to discover that the answer is actually much easier: all we have to do is enclose the file name in square brackets.

Yep, square brackets. If we set the value of strFile to this the script will work just fine:

strFile = "[Fabrikam Employees.txt]"

To be honest, this was a little disappointing. After spending all that time and effort we were hoping the answer would be really complicated; that way we could say, “Well, it took us a long time, but that’s only because this is such a complex problem that it required incredible feats of intellectual reasoning and ability before we could determine the answer.” Instead all we can say is, “Just enclose your file name in square brackets.” But, hey, at least it works, right? And, like we said, we couldn’t find anything in their writings to indicate that either Albert Einstein or Isaac Newton ever figured this out. So there.

By the way, here’s what the complete script looks like:

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

strPathtoTextFile = "C:\Scripts\Text files"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
    "Data Source=" & strPathtoTextFile & ";" & _
        "Extended Properties=""text;HDR=YES;FMT=Delimited"""

strFile = "[Fabrikam Employees.txt]"

objRecordset.Open "SELECT * FROM " & strFile & " WHERE Title = 'Fiscal Specialist'", _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("Name")
    Wscript.Echo objRecordset.Fields.Item("Department")
    Wscript.Echo objRecordset.Fields.Item("Title")
    objRecordset.MoveNext
Loop

Added bonus. Ironically, after trying so hard to find one way to access text files that have spaces in their file names we ended up finding two ways. You can also enclose the file name using the ` character. This isn’t the single quote mark; instead it’s the accent grave character found on the same key as the ~ character (on U.S. English keyboards anyway). In other words, you can also set the value of strFile to this and your script will work just fine:

strFile = "`Fabrikam Employees.txt`"


How Can I Use ADO to Read a Text File Where All the Data is on a Single Line?

Hey, Scripting Guy! I have a fixed-width text file which includes first name (6 characters), last name (13 characters), and department (9 characters) all on a single line. How do I read that using ADO?

-- DW

Hey, DW. Well, to be perfectly honest, we don’t think you can read a file like this using ADO (ActiveX Data Objects). ADO is a fantastic way to interact with text files (for more information, see the article Much ADO About Text Files), but – really – ADO is a bit of a prima donna: everything has to be set up just right or ADO will refuse to work. (Unlike the Scripting Guys, who refuse to work even if everything is set up just right.) As a database technology, ADO requires – to steal a term from the XML world – a “well-formed” database; it’s simply not equipped to take a file like yours and make a database out of it. (In this case, by chopping your huge line into individual records.)

But that’s all right; after all, we can use good old, reliable VBScript to create a well-formed database. What we’ll do today is show you how you can quickly and easily carve your huge string of data into individual records. We’ll do that simply by echoing those individual records (i.e., individual records in the text file) back to the screen. However, you could easily write those lines to another text file and then use ADO to read in that second text file. We’re just going to help you get started, then let you take it from there.

To begin with, we’re assuming you have a text file that looks something like this, with field names and a couple of records all contained on a single line:

First Last         Dept     Ken   Meyer        Finance  Pilar Ackerman     HR

What we want to end up with is something that looks more like this:

First Last         Dept
Ken   Meyer        Finance
Pilar Ackerman     HR

Is that possible? Of course it is:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

i = False

Do Until i = True
    intLength = Len(strContents)
    If intLength < 28 Then
        Exit Do
    End If
    strLines = strLines & Left(strContents, 28) & vbCrLf
    strContents = Right(strContents, intLength - 28)
Loop

Wscript.Echo strLines

We start out by defining a constant named ForReading and set the value of that constant to 1; we’ll use that to open our text file for reading.

Note You know how your mother would always tell you to put your hood up, or to look both ways when crossing the street? Well, we Scripting Guys always tell you that you can’t open a text file without specifying how you want to open that text file: for reading, for writing, for appending. And you can only do one operation at a time: you can’t open a text file for both reading and writing. Hence the need for the constant ForReading.

Oh: and put your hood up, too. It’s cold outside.

Next we create an instance of the FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Test.txt. We call the ReadAll method in order to read the entire contents of that file into the variable strContents, then use the Close method to close the file.

After that we have this odd little line of code:

i = False

What we’re going to do in a second is set up a Do loop that enables us to read the contents of the file (well, OK, the value of the variable strContents) in 28-character increments. Why 28 characters? Well, we have 6 characters in each record reserved for the first name, 13 characters set aside for the last name, and 9 characters allotted for the department. Thus:

6 + 13 + 9 = 28

In order to keep our loop going until we’ve read through the entire file, we set the variable i to False, then tell our loop to keep going until i is equal to True:

Do Until i = True

As it turns out, i never will equal True, but don’t worry: we’ll still be able to get out of the loop, and we’ll show you that Houdini-like feat in a moment.

Inside the loop we first use the Len function to determine the number of characters in the string strContents (with our sample file, and counting the header line as a record, that should be 28 characters per record times 3 records, or 84). We then have this little block of code:

If intLength < 28 Then
    Exit Do
End If

What we’re doing here is checking to see if the length of our string is less than 28 characters. If it is, then we must have reached the end of the file. Therefore, we use the Exit Do command to exit the loop. (And that’s how the amazing Scripting Guys managed to escape the clutches of the never-ending Do loop!)

Assuming the length is 28 characters or more, we then execute this line of code:

strLines = strLines & Left(strContents, 28) & vbCrLf

Here we’re creating a new string – strLines – that divides the contents of our text file into separate records, each record consisting of 28 characters plus a carriage return-linefeed (vbCrLf). To do that, we simply combine the current contents of strLine with the first 28 characters in the string (that’s what the Left function does) and the VBScript constant vbCrLf. The first time through the loop strLines will look like this:

First Last         Dept

See how that works?

As soon as our first line (that is, our first record) is safely tucked away in the variable strLines, we then need to delete that information from strContents. That’s what we do here:

strContents = Right(strContents, intLength - 28)

This time we’re using the Right function to take x number of characters from the end of the string, working our way backwards. What, exactly, do we mean by x number of characters? To calculate x we take the length of strContents (84) and subtract 28 (the length of an individual record). That leaves us with 56 (84 – 28 = 56) so we start at the end of the string and count back 56 characters. That means, the first time through the loop, we’ll end up with this:

Ken   Meyer        Finance  Pilar Ackerman     HR

Note. Yes, there are other ways we could do this, some of which might save you a line or two of code. However, we thought this approach was the easiest.

As you can see, all we’ve done is remove the first record. We’re now ready to go back through the loop and repeat the process. This time, of course, the length of strContents will be 56; that’s because we just eliminated 28 characters. In turn, that means we’ll end up pulling out this data and adding it to the variable strLines:

Ken   Meyer        Finance

We continue this process until we run out of characters in the variable strContents. At that point we simply echo back the values of strLines:

First Last         Dept
Ken   Meyer        Finance
Pilar Ackerman     HR

If that’s not a well-formed database, well, we don’t know what is.

Like we said, if you wanted to you could save the data to a text file and read it using ADO, or you could use simply use VBScript’s string-handling functions to divvy each line into individual records. We’ll leave that up to you.

Oh, and one other thing before we go: didn’t we tell you to put your hood up?


How Can I Verify That All the Items in a List Also Appear in Another List?

Hey, Scripting Guy! I have a dumb question for you: If I have two lists how can I verify that all the items in list 2 can be found in list 1?

-- SS

Hey, SS. Trust us; we’ve heard sillier questions. For example, awhile back the Scripting Son came downstairs on a Saturday morning to find the Scripting Dad eating breakfast. “What’s that?” he said, pointing to his father’s bowl.

“Oatmeal.”

“Isn’t oatmeal supposed to have milk on it?” asked the Scripting Son.

“Regular oatmeal, like the kind Grandma makes, does,” said the Scripting Dad. “But this is instant oatmeal: you just add hot water to instant oatmeal.”

“Oh,” said the Scripting Son. “Maybe that’s why I don’t like instant oatmeal.”

As it turns out years ago, when he was little, the Scripting Son decided to make himself some instant oatmeal. He took the package out of the box, poured it into a bowl, then added cold milk. Not too surprisingly, he didn’t really like it.

Note. No, the Scripting Dad didn’t poke fun at the Scripting Son. After all, the Scripting Dad didn’t want to be reminded of the time he and the Scripting Son were driving home from a baseball game when the Scripting Dad was suddenly struck by a terrible thought. “Uh-oh,” he said as they cruised down the road. “I can’t remember whether or not I took my keys out of the bat bag.”

“You mean the keys you used to start the car, the ones that are in the ignition right now?” asked the Scripting Son.

Yes, those keys.

Needless to say, SS, when it comes to the silliest questions of all time, well, yours just doesn’t stack up all that well. However, and if it’s any consolation, your question has officially become one of the questions answered in this column:

Const TextMode = 1

arrList1 = Array("a","b","c","d","e","f")
arrList2 = Array("a","b","e","g","h")

Set objDictionary = CreateObject("Scripting.Dictionary")
objDictionary.CompareMode = TextMode

For Each strValue in arrList1
    If Not objDictionary.Exists(strValue) Then
        objDictionary.Add strValue, strValue
    End If
Next

For Each strValue2 in arrList2
    If Not objDictionary.Exists(strValue2) Then
        strNotFound = strNotFound & strValue2 & vbCrLf
    End If
Next

If strNotFound <> "" Then
    Wscript.Echo "Items in list 2 that are not in list 1:" & vbCrLf & strNotFound
End If

Let’s see if we can figure out how this works. The script starts out by defining a constant named TextMode and setting the value to 1; we’ll use this constant later on to tell our Dictionary object to perform case-insensitive searches. We do that because, by default, the Dictionary object sees A (uppercase) and a (lowercase) as totally separate items. If you’d prefer to do a case-sensitive search (where a and A are considered different characters), then leave out this line and the line of code where we configure the CompareMode property.

Note. And if you’d like to know more about the difference between case-sensitive and case-insensitive searches, take a peek at the scripting puzzle Everyone is Special … Except You, James.

After defining the constant we then set up two very simple arrays, arrays that represent our two lists. The array arrList1 consists of the following items:

a
b
c
d
e
f

Meanwhile, the array arrList2 consists of these items:

a
b
e
g
h

Our task is to figure out whether there are any items in arrList2 that are not in arrList1.

How do we plan on doing that? Why, by using the Scripting.Dictionary object, of course. That’s why we use these two lines of code to create an instance of the Dictionary object and then set the CompareMode to the constant TextMode (which will result in case-insensitive searches):

Set objDictionary = CreateObject("Scripting.Dictionary")
objDictionary.CompareMode = TextMode

Good question: why are we using the Dictionary object anyway? We opted to go this route primarily because it’s very tedious to search for specific items in a VBScript array. Suppose we want to know if the value z appears in the array. We can’t just say, “Hey, array: do you have a z anywhere?” Instead, we have to loop through and individually check each item in that array. And then we have to repeat that process for the next value, and the value after that, and – well, you get the idea. With the Dictionary object, however, we can just ask it whether or not it has any z’s, a process we’ll explain momentarily.

Before we can do that, however, we need to grab all the elements in arrList1 and get them into the Dictionary. That’s what these lines of code are for:

For Each strValue in arrList1
    If Not objDictionary.Exists(strValue) Then
        objDictionary.Add strValue, strValue
    End If
Next

As you can see, all we’re doing here is looping through the collection of values stored in the array arrList1. For each of these values we then use the Exists method to determine whether or not the value is already in the Dictionary:

If Not objDictionary.Exists(strValue) Then

Is that important? Well, it could be; after all, the Dictionary object won’t accept duplicate entries. Because the best way to avoid duplicate entries is to check each value before we add it to the Dictionary we use the following line of code to determine whether or not our Dictionary object already has an entry for a, the first item in arrList1:

If Not objDictionary.Exists(strValue) Then

Yes, we know: the syntax – If Not objDictionary.Exists(strValue) – is a little clumsy. But all we’re saying is this: let us know if you don’t have an entry for the letter a. If you already have a key named a that’s fine; we’ll just loop around and check the next item in the list. If you don’t have an a, then we’re going to use this line of code to add a to the Dictionary, setting both the Dictionary key and value to, well, a:

objDictionary.Add strValue, strValue

Note. What’s the difference between a Dictionary key and a value? Tsk, tsk, tsk: you haven’t been reading Sesame Script each month, have you?

When we’re all done our Dictionary will include the following items, which just happen to be the same items found in arrList1:

a
b
c
d
e
f

We’re now ready to start looping through the items in the second list (arrList2) and determine whether or not those items can be found in arrList1. To do that we set up another For Each loop, this one looping through the array arrList2:

For Each strValue2 in arrList2
    If Not objDictionary.Exists(strValue2) Then
        strNotFound = strNotFound & strValue2 & vbCrLf
    End If
Next

Nothing too fancy here, either. Again, we’re using the Exists method to determine whether or not the first value in arrList2 can be found in the Dictionary. As it turns out, the first value (a) actually does exist in the Dictionary. That means that a must also exist in arrList1, which, in turn, means we don’t care about it. (Remember, we’re only looking for items in arrList2 that aren’t in arrList1.) With that in mind, we loop around and try the next item in the list.

Eventually we’re going to run into an item – g – that isn’t in the Dictionary. That will trigger this line of code:

strNotFound = strNotFound & strValue2 & vbCrLf

Here we’re assigning a value to a variable named strNotFound. To be more precise, we’re assigning this variable its existing value (which, at the start of the script, is nothing) plus the value of the item we failed to find in the Dictionary (g) plus a carriage return-linefeed (vbCrLf). In other words, after failing to find g in the Dictionary, strNotFound will be equal to this:

g

We then loop around and try the next item in the list; that’s h, which doesn’t appear in the Dictionary either. We thus append this value and a carriage return-linefeed to the existing value of strNotFound, meaning strNotFound is now equal to this:

g
h

You get the idea. If there were more values in arrList2 we’d simply continue this process until we’ve tested for the existence of each and every one.

And then what happens, after we’ve tested each and every one of those values? This happens:

If strNotFound <> "" Then
    Wscript.Echo "Items in list 2 that are not in list 1:" & vbCrLf & strNotFound
End If

Here we’re simply checking the value of strNotFound. If strNotFound is an empty string that means that all the values in arrList2 were found in arrList1; therefore, we don’t do anything. If strNotFound is not an empty string that means we found at least one value in arrList2 that doesn’t appear in arrList1. Consequently, we echo back a message similar to this:

Items in list 2 that are not in list 1:
g
h

Needless to say, g and h just happen to be the two items in list 2 that don’t appear in list 1. Pretty cool, huh?

We hope that helps, SS. And remember, there’s no such thing as a dumb question, not even the age-old query, “Does this dress make me look fat?” The truth is, that question isn’t dumb; what’s dumb would be giving an honest and thoughtful answer to that question. (Not that any of the Scripting Guys have ever done that, mind you.)


Can I Combine Multiple Text Files Using a Script?

Hey, Scripting Guy! From the command prompt the command copy a.txt+b.txt ab.txt will take the contents of a.txt and the contents of b.txt and combine them into a new file named ab.txt. Can I do the same thing with a script?

-- DL

Hey, DL. In yesterday’s column we dealt with text files; more specifically, we talked about how you can use a script to modify .INI files. We told you that the solution wasn’t very elegant, but that it would do the job. Well, we have the same situation here. Can we use a script to combine text files? Yes we can. We have to do it in a slightly cumbersome way, but it’ll work just fine.

The problem we face is that neither WSH nor VBScript has a way to combine text files with a single command (e.g., objFile.AddTextFiles(“file1.log”,”file2.log”). That’s a shame, but it won’t stop us from combining text files; we’ll just have to go through a few extra steps in order to do this. For example, to combine File1.log and File2.log into a single file (which we’ll call Output.txt) we’ll have to first read File1.log and append the contents of that file to output.txt, then we’ll have to read File2.log and append the contents of that file to Output.txt. In fact, we’ll have to use a script that looks a lot like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objOutputFile = objFSO.CreateTextFile("output.txt")

Set objTextFile = objFSO.OpenTextFile("c:\logs\file1.log", ForReading)

strText = objTextFile.ReadAll
objTextFile.Close
objOutputFile.WriteLine strText

Set objTextFile = objFSO.OpenTextFile("c:\logs\file2.log ", ForReading)

strText = objTextFile.ReadAll
objTextFile.Close
objOutputFile.WriteLine strText

objOutputFile.Close

As you can see, this isn’t exactly rocket science. We start out by defining a constant (ForReading) that we’ll use to open each log file. We then create an instance of the FileSystemObject (the scripting technology used to manipulate text files) and use the CreateTextFile method to create a new file named Output.txt.

After that, we open our first file (C:\Logs\File1.log) for reading. We use the ReadAll method to read in the entire text file and store that information in the variable strText. We close File1.log, then use the WriteLine method to append the information we just read in to our new file, Output.txt. We then repeat the process for the next file (C:\Logs\File2.log). After we’ve read in this second file, Output.txt will consist of all the information found in the first file plus all the information found in the second file. Hey, we did it!

Ok, ok, we know what you’re thinking: sure, the preceding script works, but it also requires you to know – in advance – the names of all the files in the folder C:\Logs. Wouldn’t it be better to have a script that could grab all the files in C:\Logs and then combine them all for us? Hmmm, we never thought about that. You mean something like this:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objOutputFile = objFSO.CreateTextFile("output.txt")

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

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

For Each objFile In FileList
    Set objTextFile = objFSO.OpenTextFile(objFile.Name, ForReading)
    strText = objTextFile.ReadAll
    objTextFile.Close
    objOutputFile.WriteLine strText
Next

objOutputFile.Close

All we’re really doing here is getting a collection of all the files in the C:\Logs folder; that’s what this WMI Associators of query does:

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

As soon as we have this collection, we use a For-Each loop to open each file and then read in the text (using the ReadAll method, just like we did before). We close the file, and append the text to our output file. We then loop around and repeat this process for the next file in the collection. In no time at all, we’ll have taken all the text from all the files in C:\Logs and combined into a new file named output.txt. It’s that easy.


How Can I Add a Line to the Top of a Text File?

Hey, Scripting Guy! How can I add a line to the top of a text file?

-- FT

Hey, FT. You know, at one point in his career Sir Arthur Conan Doyle actually killed off Sherlock Holmes, figuring he’d written everything anyone could ever write about the master detective. Public outcry soon made him change his mind, and in short order he brought Sherlock Holmes back to life. (Creating, along the way, the basic plotline for every soap opera ever written.)

We Scripting Guys can empathize with Sir Arthur Conan Doyle. After all, we periodically think, “Well, that’s it; we’ve finally written everything anyone could ever write about text files.” No sooner do we think that than we seem to get a flurry of questions about text files, including three people who asked this same thing: how can I add a line to the top of a text file?

Elementary, my dear Watson (uh, my dear FT). All you have to do is use a script similar to this:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

strFirstLine = "This is the new first line in the text file."
strNewContents = strFirstLine & vbCrLf & strContents

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strNewContents

objFile.Close

Note. Interestingly enough, in the original stories Sherlock Homes never said, “Elementary, my dear Watson.” No, we don’t know why he didn’t say it; he just didn’t.

Ah, yes, now the game is afoot, isn’t it? We begin by creating a pair of constants – ForReading and ForWriting – that we’ll use when working with our text file. We create an instance of the FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Now that the file is open we use the ReadAll method to read the entire contents of the file and store those contents in a variable named strContents. We then immediately close the file Test.txt. Why? Well, the FileSystemObject allows you to open a file for reading or for writing, but you can’t do both at the same time. To add a new line to the top of the file, we’re going to have to write to the file; that means we’ll have to reopen it, but for writing.

Next we need to construct the new contents for the file. We can’t directly add a line to the top of the text file; the FileSystemObject only allows you to add new lines to the end of a text file. Therefore, what we’ll have to do is create an entirely new file in memory and then replace the existing contents of Test.txt with this new file. Our new file will consist of three parts: a new first line; a carriage return-linefeed; and the existing contents of the file. To construct this file we start off by using this code to store the new first line in a variable named strFirstLine:

strFirstLine = "This is the new first line in the text file."

We then use this line of code to concatenate the new first line, a carriage return-linefeed (using the VBScript constant vbCrLf) and the existing contents of the file (which we stored in the variable strContents):

strNewContents = strFirstLine & vbCrLf & strContents

All that’s left now is to reopen Test.txt (for writing this time), then use the WriteLine method to replace the exiting contents with our new file:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strNewContents

We then call the Close method and, just like that, case closed. Well, OK, file closed. Hey, we’re just trying to do what Sherlock Holmes would do.

Speaking of which, did you know that Sherlock Holmes had an older brother named Mycroft? It’s true. Supposedly Mycroft had a lot of potential, but was too lazy to really do anything interesting or useful.

Hey, what do you mean that sounds familiar? Never mind; you’re probably thinking about some other daily scripting columnist.


How Can I Write Double Quotes to a Text File

How Can I Add a Word to a Text File if That Word Isn’t Already in the File?

Hey, Scripting Guy! Hey, Scripting Guy! How can I check a text file to see if a word appears in that file and, if it doesn’t, add the word to the file?

-- JO

Hey, JP. According to your email, you’re trying to write the following line to your XML file:

Hey, JO. We seem to have gotten quite a few questions on this topic lately. You mentioned adding words to a custom dictionary for Microsoft Word; other people mentioned adding an entry to an LMHosts file or something similar. What we’ll do today is provide a generic solution – adding a computer to a list of computer names stored in a text file – but you shouldn’t have any problem adapting our generic example to your specific needs.

To begin with, let’s assume we have a very simple text file (C:\Scripts\Computers.txt) that features the names of all our computers, one computer per line:

atl-dc-01

atl-dc-02

atl-dc-03

We just got a new computer – atl-dc-99 – and we want to add that to the list, provided, of course, that atl-dc-99 isn’t already on the list. To do that we need to read through Computers.txt and see if atl-dc-99 is already in there. If it is, we’re done; if it isn’t, we need to programmatically add the computer to the list. This is going to require three steps: read the file; check to see if the computer is already in there; and, if necessary, add the computer to the file.

Let’s start with reading the file. Here’s some code that will open the file C:\Scripts\Computers.txt, read the entire contents into a variable named strWordList, and then close the file:

Const ForReading = 1

Const ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile _

    ("C:\Scripts\Computers.txt", ForReading)

strWordList = objFile.ReadAll

objFile.Close

We begin by defining two constants – ForReading and ForAppending – that we’ll use when working with the text file. For now the only constant we need is ForReading, which tells the FileSystemObject that we want to open a file and read the contents. (You can open a file for reading, for writing, or for appending, but you can’t perform more than a single operation at any one time.)

Next we create an instance of the FileSystemObject and use the OpenTextFile method to open the file for reading. We then use the ReadAll method to read the entire file and store the contents in the variable strWordList. With that done, we call the Close method to close the file.

Note. Why close the file so soon after opening it? Well, if atl-dc-99 is already in the file, then we’re basically done; we won’t need to do anything more with Computers.txt. But what if atl-dc-99 isn’t in the file? In that case, we need to append the new computer to the file; however, as we mentioned, we can’t simultaneously read from and append to a file. Consequently, we need to close the file (which was initially opened for reading) and then reopen it, this time for appending.

So much for step 1. In step 2, we need to determine whether atl-dc-99 is already included in our text file. To do that, we start by defining the string we’re searching for, something we do here:

strSearchWord = "atl-dc-99" & vbCrLf

Notice that we’re constructing a string that consists of the search word (atl-dc-99) plus another carriage return-linefeed. Why go to all that trouble? Well, we want to make sure that any instance of atl-dc-99 we find is a complete computer name and not part of a computer name. Suppose we didn’t specify that a carriage return-linefeed comes before and after our search word, suppose we just said, “Find all instances of atl-dc-99.” In that case, all of these computer names would be scored as “hits:”

atl-dc-999

atl-dc-9999

atl-dc-99-mail-server

You get the idea. We want to ensure that any instance of atl-dc-99 is on a line all by itself, like this:

atl-dc-99

In other words, did the person who created the list type the name atl-dc-99 and then press ENTER? That’s why we look for a carriage return-linefeed immediately following atl-dc-99.

Note that even this approach isn’t foolproof; for example, the script will be “fooled” by a name like this:

seast-atl-dc-99

There are ways to work around problems like this, but they get a little more complicated. If you’re interested, you might take a look at the Scripting Guys webcast on using regular expressions.

So how can we tell if our search term appears in the text file? Well, remember, we have the contents of the file stored in the variable strWordList. Because of that we can use the VBScript InStr method to determine whether or not our search term appears anywhere in the file:

If InStr(strWordList, strSearchWord) = 0 Then

If found, InStr will return the character position in the variable where the search term begins; if the search term can’t be found, then InStr returns a 0. To determine whether or not atl-dc-99 is already in the text file, we just check the return value for InStr. If this value is anything other than 0, that means the search term was found and we’re done. If the return value is 0, then we need to add atl-dc-99 to the text file. We do that with these lines of code:

Set objFile = objFSO.OpenTextFile _

    ("C:\Scripts\Computers.txt ", ForAppending)

objFile.WriteLine "atl-dc-99"

objFile.Close

All we do here is reopen the file – this time for appending – and then use the WriteLine method to write atl-dc-99 at the bottom of the file. (By definition, “appending” to a file adds the information to the end of the file.) We then close the file, and move on to bigger and better things.

Here’s what the completed script looks like:

Const ForReading = 1

Const ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile _

    ("C:\Scripts\Computers.txt", ForReading)

strWordList = objFile.ReadAll

objFile.Close

strSearchWord = "atl-dc-99" & vbCrLf

If InStr(strWordList, strSearchWord) = 0 Then

    Set objFile = objFSO.OpenTextFile _

        ("C:\Scripts\Computers.txt ", ForAppending)

    objFile.WriteLine "atl-dc-99"

    objFile.Close

End If


How Can I Add an HTML Tag to the End of Each Line in a File?

Hey, Scripting Guy! I have a bunch of documents I’m trying to transfer to MySpace. In order to do so, I need to put the <br> tag at the end of each and every line in the document. Is there any way to do that using Microsoft Word, or even Notepad?

-- MM

Hey, MM. You know, most people dislike spam. (We mean the unsolicited email, not the lunch meat. The lunch meat is surprisingly good, provided you grill it before eating.) And while the Scripting Guy who writes this column doesn’t necessarily like spam, he does find it interesting, and he often reads those unsolicited emails before deleting them. (The emails he deletes without reading them tend to be messages sent by his fellow Microsoft employees.)

Today’s email brought him a particularly intriguing piece of spam. “Are you experiencing mail performance problems?” it asked. “If so, we can help. Try our generic-quality natural performance enhancers.”

Needless to say, there’s a lot of interesting stuff packed into that little message. Generic-quality natural performance enhancers? To tell you the truth, the Scripting Guy who writes this column has no idea what those could be. But he definitely likes the idea of getting performance enhancers that are of “generic quality.” That sounds like something he could strive for in his own work.

Even better is the fact that these performance enhancers are for mail performance problems. Needless to say, those babies would come in handy. For example, sometimes the Scripting Guy who writes this column doesn’t get his copy of Sports Illustrated until Saturday. With a mail performance enhancer, he might get that same magazine on Thursday, or maybe even on Wednesday. Getting Christmas cards in July? Receiving a shipment from Amazon.com before you’ve even submitted the order? Wouldn’t that be cool?

Medical Disclaimer. If you take any medicines that have nitrates in them you should not use mail performance enhancers, not even those of generic quality. The most common side effects of mail performance enhancers are headache, facial flushing, and upset stomach. If you continue to receive the same magazine for more than four hours at a time please seek immediate medical help.

Of course, eventually the Scripting Guy who writes this column figured out that these were supposed to be performance enhancers for male performance problems. Does the Scripting Guy who writes this column suffer from male performance problems? Good question; it’s been so long since anyone has asked him to perform that he’s forgotten.

So we’ll just say “No” and let it go at that.

Fortunately, the Scripting Guy who writes this column hasn’t forgotten how to write a script that can add a <br> tag to the end of every line in a file:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close
strContents = Replace(strContents, vbCrlf, "<br>" & vbCrLf)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

objFile.Write strContents
objFile.Close

Let’s see if we can figure out how this script works. As you can see, we start out by defining a pair of constants, ForReading and ForWriting; we’ll use these constants to tell the script whether we want to open our file and read from it (the ForReading constant) or whether we want to open our file and write to it (the ForWriting constant). And, yes, we need both of these constants because we’re going to have to open the file twice: once to read the existing contents, and a second time to write out the modified contents (that is, the file with the <br> tag at the end of each line).

After defining the constants we next create an instance of the Scripting.FileSystemObject object, then use the following line of code to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Next we call the ReadAll method to read in the entire contents of the file, storing that information in a variable named strContents. We then call the Close method and close the file. But don’t despair; we’ll be reopening the file, this time for writing, in a minute or two.

So why did we read in the entire contents of the file and then store that information in a variable? That’s easy: we did that because the FileSystemObject doesn’t allow us to directly modify the contents of a file. Instead, we need to read the contents into memory, modify the “virtual” version of that file, and then overwrite the existing file with these modified contents. We’ve already done part 1 of this 3-part process: we’ve read the contents into memory. Now it’s time for part 2, modifying the virtual version of our file:

strContents = Replace(strContents, vbCrlf, "<br>" & vbCrLf)

Our original file, C:\Scripts\Test.txt, is just a regular old text file: nothing but a bunch of lines of text. How do you delineate a line in a text file? You know the answer to that one: you just keep typing along until you want to start a new line; at that point, you simply press the ENTER key on the keyboard. In turn, that causes a carriage return-linefeed character to be added to the end of each line. (You won’t see that character if you view the file in Notepad, but trust us, it’s there.)

The important thing is this: we can determine the end of each line simply by looking for instances of the carriage return-linefeed character (represented by the VBScript constant vbCrLf). That also makes it very easy to add the <br> tag to the end of each line; to do that all we need to do is replace each carriage return-linefeed character with the <br> tag plus a carriage return-linefeed character. Suppose our text file looks like this, with the asterisk representing the carriage return-linefeed character:

This is line 1.*
This is line 2.*
This is line 3.*

If we replace each carriage return-linefeed (in this example, each asterisk) with the <br> tag and a carriage return-linefeed character our text file will end up looking like this:

This is line 1.<br>*
This is line 2.<br>*
This is line 3.<br>*

Which is good; that’s what we want it to look like.

To make a long story short, that’s what we use the Replace function for: we search the value of strContents for each instance of vbCrLf, replacing those instances with the <br> tag plus vbCrLf. Just like the line of code suggests:

strContents = Replace(strContents, vbCrlf, "<br>" & vbCrLf)

That was easier than you thought it was going to be, wasn’t it?

The next part, part 3, is even easier: we reopen the file C:\Scripts\Test.txt, this time for writing, then call the Write method to write the revised value of strContents to the file:

objFile.Write strContents

At that point we call the Close method to close the file and call it a day. The net result? Each line in the text file now has the <br> tag at the end (again, using an asterisk to represent the invisible carriage return-linefeed character):

This is line 1.<br>*
This is line 2.<br>*
This is line 3.<br>*

That should do it, MM. Now, if you wouldn’t mind, could you do us a favor in return: could you check your spam mail every now and then and see if anyone is selling generic-quality natural performance enhancers for work performance problems? Based on his last performance review that’s something the Scripting Guy who writes this column could really use.


How Can I Add Leading Zeroes to All the Lines in a Text File?

Hey, Scripting Guy! I have a text file where each line is a set of characters like this: 56A63, 78A41, etc. I need to put leading zeroes in front of these strings in order to make them all nine characters long: 000056A63, 000078A41, etc. Oh: and my text file has 20,000 lines that need to be modified like this. How can I do all that using a script?

-- JC

Hey, JC. You know, at first we were going to ask another one of the Scripting Guys to answer this question. For one thing, this Scripting Guy happens to be a world-class expert on scripting, and does a far better job of explaining complicated scripting techniques than the rest of us could even dream of doing. More important, she’s playing in a Microsoft-sponsored softball league, and from what she’s told us her batting average is filled with zeroes; she should have more than enough to share with you. (Editor’s Note: Keep in mind these biting comments came from the Scripting Guy who insists he’s too good to play with the rest of us. Funny how none of us has ever actually seen him play though.)

But then we thought, “Gosh, that would hardly be like Hey, Scripting Guy! to make fun of a co-worker.” And so we decided to throw together this script instead:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
strText = objFile.ReadLine
intLength = Len(strText)
intZeroes = 9 - intLength
For i = 1 to intZeroes
strText = "0" &
strText
Next
strContents = strContents & strText & vbCrlf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strContents

objFile.Close

Incidentally, we actually tested this on a text file with 20,000 lines in it. Not only did it work, but it worked pretty dang fast: in less than 30 seconds we were done. Which is just about the length of time it takes our fellow Scripting Guy to walk to the plate and then head back to the dugout, another at-bat over and done with.

Not that we would mention anything like that, mind you.

Note. At first we were concerned that our co-worker might work on her swing by taking a few cuts at us. But considering the way she hits a softball, well, we were willing to take that chance.

And, yes, now that you mention it we are starting to get a little concerned for our safety.

But what about the script? Well, we start out by defining a pair of constants, ForReading and ForWriting; we’ll use these constants to indicate how we want to open the text file in question. As you know, we can’t simultaneously read to and write from a text file; instead, we have to open the text file, read in the contents, modify those contents (by adding the leading zeroes), close the file, re-open the file, and then replace the existing lines with the modified lines. It’s a tad bit cumbersome, but we don’t have a lot of choice in the matter.

After defining out constants we create an instance of the Scripting.FileSystemObject, the scripting object used for working with text files. We then call the OpenTextFile method, passing two parameters: the path to the file we want to open (C:\Scripts\Test.txt) and the constant ForReading. (Why ForReading? Because, as we noted a moment ago, the first step in this process is to read in the contents of the file.)

With the file open we’re ready to roll up our sleeves and get to work. What we’re going to do next is read the file line-by-line, adding the leading zeroes as we go. To do that we set up a Do Until loop that runs from the beginning of the file to the end of the file. (How do we know that we’ve reached the end of the file? We’ve reached the end when the AtEndOfStream property is True.) Inside that loop we use this line of code to read the first line of the file and store the contents in a variable named strText:

strText = objFile.ReadLine

After we read in the line we’re ready to add the leading zeroes. However, there’s one complication here: as JC noted in her email, the number of characters in each line varies. That is, we might have 123 on one line, then have 12345 on another line. That means we can’t simply add the same number of leading zeroes to each line and call it good. (Remember, the goal is to end up with a string nine characters long.)

But that’s a minor complication. To begin with, we use the Len function to determine the number of characters in strText. For example, suppose the first line in the text file is 1234; in that case, Len will return a 4, because strText contains 4 characters. That’s what this line of code does:

intLength = Len(strText)

Next we subtract the variable intLength (the length of the string) from 9. Why? Well, that tells us how many leading zeroes we need to add. With a 4-character string we subtract 4 from 9, leaving 5. That means we need to add 5 leading zeroes. Here’s the line of code that carries out that calculation, with the required number of zeroes getting stored in a variable named intZeroes:

intZeroes = 9 - intLength

As far as adding the leading zeroes, we decided the easiest way to do that was to use a For Next loop that runs from 1 to the number of zeroes we need to add:

For i = 1 to intZeroes
    strText = "0" & strText
Next

As you can see, each time through the loop we assign a new value to the variable strText: that new value is simply a 0 (as a character, not as a number) followed by the existing value of strText. The first time through the loop we’ll have 0 followed by 1234, making strText equal to 01234. The second time through the loop we’ll have 0 followed by 01234, yielding 001234. This continues until we have 9 characters in the string.

The net effect: we’ve now taken the first line of the text file and added the necessary leading zeroes. Ideally, at this point we’d simply write the revised line back to the file. As we pointed out earlier, though, the FileSystemObject doesn’t let us read and write to a file at the same time. Therefore we need to temporarily store all the revised lines in memory, something we do here:

strContents = strContents & strText & vbCrlf

All we’re doing here is basically building our revised text file in memory. After the new file has been constructed we can then go ahead and write it to the actual file C:\Scripts\Test.txt.

Next we loop around and repeat the process with the second line in the text file, eventually adding the modified second line (plus a carriage-return linefeed, represented by the VBScript constant VbCrLf) to the variable strContents. That means that, after we’ve read through the entire file, strContents will contain 20,000 modified lines of text.

All we need to do at that point is replace the existing contents of the text file with the 20,000 modified lines stored in strContents. To do that we close the file, then immediately re-open it, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

We then call the WriteLine method to write the revised contents to the text file (overwriting the existing contents), a process that looks like this:

objFile.WriteLine strContents

And that’s it. We close the text file, and we’re free to move on to something else. Something like, say, batting practice.

Well, for those of us who need it, that is ….


How Can I Add Two Blank Lines Between Each Line in a Text File?

Hey, Scripting Guy! How can I add two blank lines between each line in a text file?

-- LW

Hey, LW. You know, when we wrote the Microsoft Windows 2000 Scripting Guide we included 4 or 5 examples of how you might work with text files. And then we thought to ourselves, “Well, we’ve pretty much exhausted all the things you can do with text files. We’ll never be called upon to answer a text file question ever again.”

OK, so we got that wrong. As it is, 1 out of every 10 or so questions sent to us deals with text files; we get 3 or 4 text file-related questions every day. And most of them deal with situations we never even thought about, situations just like this: how can I add two blank lines between each line in a text file?

Well, let’s see what we can do about that. Suppose we have a text file – left over from a previous column – that consists of a bunch of names:

Ken Myer
Pilar Ackerman
Jonathan Haas
Syed Abbas

Is there an easy way to put two blank lines between each of these names? You bet there is:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\namelist.txt",
ForReading)

strContents = objFile.ReadAll()
objFile.Close

strOldText = vbCrLf
strNewText = vbCrLf & vbCrLf & vbCrLf

strContents = Replace(strContents, strOldText, strNewText)

Set objFile = objFSO.OpenTextFile("c:\scripts\namelist.txt",
ForWriting)
objFile.Write strContents
objFile.Close

We begin by defining a pair of constants – ForReading and ForWriting – that we’ll use when working with our text file. We create an instance of the FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Namelist.txt. We use the ReadAll() method to read the entire contents of the file into a variable named strContents, and then close the file.

As you know, when you create a text file such as our list of names you type some information on line 1, press ENTER, and type something on line 2. Each time you want to start a new line you press ENTER, an action which – on a more technical level – inserts a carriage return-linefeed character into the file. You won’t actually see this character, but it’s there, and VBScript knows it; in fact, VBScript has a built-in constant – vbCrLf – that maps to the carriage return-linefeed.

Why do we care about that? Well, how would you put two blank lines between each line in a text file? If you’re doing this manually, you’d type in line 1, then press ENTER three times. Press ENTER once and line 2 falls directly under line 1. Press ENTER three times and you’ll end up with line 1, two blank lines, and then line 2. What does that mean? Well, our variable strContents includes carriage return-linefeeds. If we can replace each of those carriage return-linefeed characters with three such characters we’ll end up with two blank lines between each line in the text file. (Try to visualize that and you’ll see what we’re talking about.)

That’s the purpose of the next two lines of code. There we assign values to two variables: strOldText, which gets the value of vbCrLf, equivalent to pressing ENTER once; and strNewText, which gets assigned the value of three vbCrLf’s, equivalent to pressing ENTER three times.

All that is a prelude to this line of code:

strContents = Replace(strContents, strOldText, strNewText)

What’s going on here? Well, we’re using the VBScript Replace function to search through the variable strContents (which, as you recall, contains the contents of our text file). The Replace function will locate each carriage return-linefeed character and replace it with three such characters; that’s going to give us two blank lines between each line in the file. After we’ve modified strContents we then reopen our text file – this time for writing – and use the Write method to replace the existing contents with our new information. If we run the script and then open the text file, it’s going to look just like this:

Ken Myer

Pilar Ackerman

Jonathan Haas

Syed Abbas

Now if that isn’t everything you could ever want to know about text files – uh, never mind. Never again will we underestimate your ability to come up with new and interesting uses for the good old-fashioned text file. Cross our hearts!


How Can I Append a Blank Line to a Text File, Then Append the Current Date and Time?

Hey, Scripting Guy! How can I append a blank line to a text file, then append the current date and time?

-- PL

Hey, PL. You know, back in the 1600s people were inordinately worried about witches and witchcraft. Fortunately, though, they developed a sure-fire test to determine whether or not someone was a witch: the accused was weighted down with rocks, then tossed into a lake. If they floated back up to the top they were a witch; after all, no one can float in a lake if they’ve been weighted down with rocks, at least not unless they were in league with the Devil. In that case the accused would be hauled out of the lake and hung. (No, we’re not completely sure why their magic would work when weighted down with rocks and tossed in a lake, yet be useless when faced with the hangman’s noose.)

If you sank to the bottom and died, well, that was actually good; that meant you weren’t a witch. In turn, that also meant that you were entitled to an honorable Christian burial. Hooray!

Now, you might think that people were troubled by this: after all, if you were accused of witchcraft then, one way or the other, you were going to end up dead. But, for the most part, people didn’t lose much sleep over the matter. After all, if you were truly innocent then no one would accuse you of being a witch, would they? To be blunt, this was all your fault: you must have done something to warrant such serious accusations.

So how did an innocuous little question about text files get us started thinking about things like the Salem Witch Trials? That’s easy: when we saw a question about modifying a text file we immediately launched into accusatory mode. “You know, that’s way harder than you might think. There’s really no easy and obvious way to do this. Don’t blame us; the FileSystemObject wasn’t designed to handle situations like this.” Without even blinking we ran through all our standard warnings, caveats and excuses about why it’s so hard to modify the text in a text file.

It was only after we unleashed this torrent of accusations that we realized that you don’t really want to modify a text file, you just want to tack some additional information onto the tail end of that file. As it turns out, that is easy:

Const ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForAppending)

objFile.WriteLine
objFile.WriteLine
objFile.Write Now

objFile.Close

By the way, sorry about weighting you down with rocks and tossing you in the lake just because we thought you were asking a question about modifying text files. Would it help if we offered you an honorable Christian burial?

That’s what we were afraid you’d say. OK, then what do you say we explain how this script works? Let’s assume that we have a text file that looks like this:

A
B
C

Is our script really going to be able to add a blank line and a timestamp to the end of this file? Of course, it – hey, wait a minute: that sounds like something a witch would say. Hmmm ….

Actually there’s very little magic involved here. We begin by defining a constant named ForAppending and setting to value to 8; we’ll use this constant to specify the correct mode when opening our text file. We create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for appending:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForAppending)

Note. Why “for appending?” That’s easy. If you open a file for writing the FileSystemObject will erase the existing contents of the file and replace the old contents with whatever new stuff you’re adding. When you open a file for appending, however, new content is simply tacked on to the end of the file.

That was pretty easy; believe it or not, from here on it gets even easier. To add a blank line to the file we call the WriteLine method twice, without supplying any parameters; each time we call WriteLine in that fashion (that is, without parameters) that’s equivalent to pressing ENTER on the keyboard. (And pressing ENTER twice will give you a blank line in a document.) We then call the Write method, appending the value of the VBScript Now function to the file. Because Now provides us with the current date and time, we end up with a file that looks like this:

A
B
C

1/2/2007 8:07:21 AM

All that’s left now is to close the file and call it a day.

Before we go, we feel compelled to note that we 21st century types shouldn’t feel smug and superior towards people of the 1600s; the truth is, things haven’t changed all that much in the past 400 years. For example, that part about weighting people down with rocks, tossing them in a lake, and then standing around waiting to see what happens? That’s the exact same method Microsoft uses when conducting annual performance reviews. Like they say, the more things change the more they stay the same.

Editor’s Note: No, Microsoft doesn’t really do that. That would mean everyone who survives a review manages to float to the top, and we certainly wouldn’t want anyone to jump to any conclusions as to how they managed to do that.


How Can I Clear the Contents of a Text File?

Hey, Scripting Guy! How can I clear the contents of a text file?

-- JJ

Hey, JJ. Well, today is Monday, June 11th, and the Scripting Guys are finally back from a week in Orlando. After all that time spent lounging around in luxury hotels, eating in gourmet restaurants, and visiting theme parks you might think that the Scripting Guys would be excited about coming back to work. Let’s put it this way: if that’s what you’re thinking, then you’re thinking wrong.

In fact, when the Scripting Guy who writes this column got up this morning, the first thing he did was turn on the news, hoping to hear that Microsoft had gone out of business over the weekend. Even when they didn’t say anything about Microsoft closing up shop he still hesitated for a moment; after all, maybe that just wasn’t considered important enough to even make the morning news. Eventually, though, he drug himself into work, at which point he was struck by a terrifying thought: he had to write a Hey, Scripting Guy! for today.

Hurray ….

But you know what they say: when the going gets tough, the tough start looking around for a question to answer that they know the answer to off the tops of their heads, one that let’s them officially claim to have written a column even though they didn’t put much effort into it. You know, a question like, say, how can I clear the contents of a text file? In case you’re wondering, here’s how:

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

objFile.Write ""
objFile.Close

Needless to say, there’s not much to this script. We start out by defining a constant named ForWriting and setting the value to 2; we’ll need this constant in order to open the file in “for writing” mode. (And yes, we do have to open the file and write to it; that’s because there’s no command that directly erases the contents of a file.) We then use these two lines of code to create an instance of the Scripting.FileSystemObject and to open the file C:\Scripts\Test.txt for writing:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

As we noted, there’s no command (e.g., objFile.Erase) that we can call in order to erase the contents of a text file. But that’s no big deal. After all, what exactly is an erased file? Depending on how you want to look at it, that’s simply an empty file, a file (like C:\Scripts\Test.txt) that exists but has nothing in it. Going by that definition, that means we can effectively erase the contents of a file simply by replacing those contents with, well, nothing. That’s what we do with this line of code:

objFile.Write ""

When we open a file for writing, anything we write to that file replaces everything currently in the file. That’s a simple enough concept. In addition, the construction “” is VBScript’s way of saying “nothing;” that “” represents a zero-length string. With that single line of code, therefore, we replace the existing contents of Test.txt with nothing. Lo and behold, the file has been erased!

And no, we already thought of that: we tried using this script to erase the rest of this week, so that we wouldn’t have to come in to work for awhile. It didn’t work.

Incidentally, if you’re simply planning on re-using the same file over and over again (that is, each time you run a script) you don’t necessarily have to erase the file; just opening the file for writing and writing new data to the file deletes the old contents and makes way for the new. But if you really do need to erase a file or a set of files, well, now you know how.

As for the Scripting Guys, the party’s over, and it’s time for us to get back to our daily routine. Which, now that we think about it, typically involves sitting around doing nothing, going to lunch, then sitting around doing nothing until it’s time to go home. OK, maybe coming back to work won’t be so bad after all.


How Can I Clear the Contents of Any File Dropped on My Script Icon?

Hey, Scripting Guy! How can I write a script that will clear the contents of any file that I drag and drop on the script icon?

-- SN

Hey, SN. Say, you didn’t by any chance misplace a few hundred thousand insects did you? OK; just thought we’d ask. This past weekend the Scripting Guy who writes this column had to drive across the state to attend a wedding. As the family was tooling along, on a bright, sunny day, without a cloud in the sky, it suddenly started to rain. Or maybe it was hail. At first all they knew was that – out of the blue – the windshield was being pelted by … something. “How can it rain when there aren’t any clouds in the sky?” asked the Scripting Son.

“That’s not rain,” said the all-wise and all-knowing Scripting Dad. “Those are bugs.”

Sure enough, the Scripting Windshield had been assaulted by thousands and thousands of kamikaze bugs, with so many hitting the windshield it sounded like raindrops (although after a minute or so all you had to do was look at the windshield and you could tell that it wasn’t raining). Definitely one of the most surreal experiences the Scripting Guy who writes this column has been through lately.

Which is saying quite a bit considering that he works at Microsoft.

At any rate, SN, if you did misplace a bunch of insects, well, we know where you can find them.

Or at least what’s left of them.

Note. OK, technically we did just plow through the mass of insects without trying to avoid them; to make matters worse, we then continued driving without stopping to help the survivors. Cruel and callous? Maybe. But we’re not going to apologize for that. After all, they started it.

Of course, we can’t be sure that those were your bugs, SN; it was hard to read their little nametags at 70 miles per hour. If they were yours, however, well, we’d like to try and make it up to you. What do you say we show you a script that can erase the contents of any file that is dragged and dropped on the script icon? Would that help?

Let’s hope so:

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each strArgument in Wscript.Arguments
    Set objFile = objFSO.OpenTextFile(strArgument, ForWriting)
    objFile.Write ""
    objFile.Close
Next

As you can see, there’s really not much to the script. (But, then again, it’s not like we ran over 100,000 dogs or cats or something.) We start off by defining a constant named ForWriting and setting the value to 2; we’ll use this constant when we open the file (or files) that have been dropped on the script icon. Once we’ve defined our constant we then create an instance of the Scripting.FileSystemObject, the scripting technology of choice (OK, the scripting technology of necessity) when it comes to working with text files. (We’re assuming you want to erase text files, a topic we’ll revisit in a few minutes.)

Now comes the fun part. One of the more interesting – albeit underutilized – features of Windows Script Host is this: if you drag and drop a file (or group of files) on a script icon (that is, on the .VBS file in Windows Explorer) the complete path name for any dropped file is automatically passed to the script as a command-line argument. That means that the script knows exactly which files have been dragged and dropped on the script icon. That capability turns out to be the secret ingredient for today’s script.

With that in mind, we next set up a For Each loop that loops through the Wscript.Arguments collection, the collection of command-line arguments passed to the script. Inside that loop we use the OpenTextFile method to, one-by-one, open each of the files that were dragged on top of our script icon:

Set objFile = objFSO.OpenTextFile(strArgument, ForWriting)

Nothing very fancy here: we simply call OpenTextFile and pass it the path to the file we want to open (strArgument) followed by the constant ForWriting (which tells the FileSystemObject that we want to write to the file).

As it turns out, the FileSystemObject doesn’t have any sort of pre-defined method for erasing the contents of a file. But that’s OK; we can handle that ourselves. When you open a file for writing, anything you write will completely replace the existing contents of that file. We want to erase the file, which means we want an empty file, one that doesn’t have any contents. Therefore, all we have to do is write an empty string to the file, like so:

objFile.Write ""

That replaces the existing contents of the file with nothing, effectively erasing everything that came before. (Thanks; we thought it was clever, too.) We then close the file, loop around, and repeat the process with the next argument in the collection.

Having shown you the code we now feel compelled to issue a warning: be careful when you use this script. Ostensibly it’s designed to work only with text files; after all, that’s what the FileSystemObject is for. However, this script can erase other types of files: we tried it with Word documents, Excel spreadsheets, .ZIP archives, and .PDF files, and the script succeeded in erasing each and every file we dropped on it. That’s not bad, mind you; it’s just something to keep in mind. If you drag a file onto this script you can pretty much count on that file getting erased. The file itself remains; you’ll still have a file named C:\Scripts\Test.txt. It’s just that the file will now be empty.

Note. We won’t show you how to do this today, but it would be easy enough to have the script prompt you before deleting a file (e.g., “Are you sure you want to delete file x?”). In fact, in digging through the Hey, Scripting Guy! archives we uncovered a column that explains how to do something very similar.

In case you’re wondering, the fact that the Scripting Guy who writes this column was quickly able to identify the culprit as being a huge mass of bugs, as opposed to rain or hail or something else, wasn’t really that big of a deal. Like we said, he does work at Microsoft, and who would know more about a huge mass of bugs than someone who works at Microsoft?


How Can I Combine Lines in a Text File?

Hey, Scripting Guy! How can I combine lines in a text file?

-- JN

Hey, JN. So you need to know how to combine lines in a text file, huh? Well, to tell you the truth, we’re kind of in a hurry today; the Scripting Guy who writes this column is getting ready to head out to St. Peter’s and the Sistine Chapel. But tell you what: we don’t have to leave for a few minutes, so we’ll see what we can do.

Note. Before you ask, yes, it is just a coincidence that the Scripting Guy who writes this column is visiting the Vatican just days before he gets back the results of the past year’s performance review. Not that he’s opposed to using prayer for personal gain, mind you. It’s just that if this review goes anything at all like his last one did, well, needless to say, in that case even divine intervention isn’t going to help him much.

OK, let’s see … combine two lines in a text file. How in the world can we – oh, wait: we have an idea. One of the great things about traveling is that you have the opportunity to learn little-known facts about famous people. Take Michelangelo, for example. Most people know Michelangelo as the designer of St. Peter’s dome, the sculptor of the Pieta, and the painter of the Sistine Chapel. What most people don’t know, however, is that, before becoming an artist, Michelangelo worked as an apprentice script writer. That’s pretty cool; even cooler is the fact that one of the scripts he wrote during that time goes out and combines lines in a text file. Having photocopied a manuscript from the Vatican library, the Scripting Guy who writes this column is pleased to present Michelangelo’s solution to the problem of combining lines in a text file:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine1 = objFile.ReadLine
    strLine2 = objFile.ReadLine
    strNewLine = strLine1 & strLine2
    strNewContents = strNewContents & strNewLine & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strNewContents
objFile.Close

And some people think that the Sistine Chapel ceiling is Michelangelo’s finest creation. Ha! The Scripting Guy who writes this column isn’t ashamed to admit that he cried from sheer joy the first time he saw this code for combining lines in a text file. Beautiful, just beautiful.

Before we explain how Michelangelo’s code works, we should take a moment to discuss JN’s situation in a little more detail. JN has a text file that looks like this:

SID123456
789
SID234567
891
SID345678
912
SID456789
123

Yes, that is a nice little text file, isn’t it? Unfortunately, however, things are all messed up; somehow or another, the last three characters in each line got put on a separate line. In reality, the text file is supposed to look like this:

SID123456789
SID234567891
SID345678912
SID456789123

In this example, that means four lines of text, not eight lines.

Hey, why the long face? Listen, there’s no need to despair: not only can we repair this broken text file but, as you’re about to see, we can do this with a minimal amount of effort.

So how do we fix JN’s text file? Well, Michelangelo solved the problem by first defining a pair of constants, ForReading and ForWriting; as most of you know by now, these two constants will be deployed when we go to open the text file. (And, as most of you also know by now, we need to open this file twice: once to read in the existing contents, and a second time to write out the modified contents.) After defining the two constants we create an instance of the Scripting.FileSystemObject object, then use this line of code to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Our next task involves reading the existing contents of the file, line-by-line. To do that, we first set up a Do Until loop that runs until we’ve reached the end of the file (that is, until the file’s AtEndOfStream property is True). Inside the loop, we call the ReadLine method to read the first line of the file and store it in a variable named strLine1:

strLine1 = objFile.ReadLine

We then immediately call the ReadLine method again, thus reading the second line in the file and storing that value in a variable named strLine2:

strLine2 = objFile.ReadLine

What’s the point of all that? Well, strLine1 is now equal to SID123456 and strLine2 is now equal to 789. Guess what you get if you concatenate these two values? Good guess:

SID123456789

That, needless to say, is the very value we were hoping to get. With that in mind, we use this line of code to combine the two values and store the new string in variable named strNewLine:

strNewLine = strLine1 & strLine2

The net result is that we now have a new and improved line 1 for our file. Therefore, we use this line of code to append the new line (plus a carriage return-linefeed) to the variable strNewContents:

strNewContents = strNewContents & strNewLine & vbCrLf

We then loop around and repeat this process by combining lines 3 and 4 in the text file, and then appending that value to strContents as well. By the time we exit the loop, the variable strContents will be equal to this:

SID123456789
SID234567891
SID345678912
SID456789123

At that point we call the Close method to close the text file, then immediately reopen the same file, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

As soon as the file is open, we use the Write method to replace the existing contents of Test.txt with the value of the variable strNewContents:

objFile.Write strNewContents

From there we close the file one final time and we’re done. Like we said, sheer genius.

We should note that this script works only if the file has an even number of lines in it, which will always be the case in the example JN asked about. However, if you happen to have an extra line at the end of the file, the script will crash:

C:\scripts\hey0814.vbs(9, 5) Microsoft VBScript runtime error: Input past end of file

To avoid this, you can set up your script to check for the end of the file before it reads the second line, like this:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine1 = objFile.ReadLine
    strLine2 = ""
    If Not objFile.AtEndOfStream Then
        strLine2 = objFile.ReadLine
    End If
    strNewLine = strLine1 & strLine2
    strNewContents = strNewContents & strNewLine & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strNewContents
objFile.Close

Notice we added an If statement inside our Do Until loop. After reading in strLine1, we then clear the contents of strLine2 then check to see if the last line we read was the last one in the file. If it was, we simply bypass the next ReadLine and create the new line (strNewLine) with the contents of strLine1 and the empty strLine2. If we’re not at the end of the file, we read in strLine2.

We should probably note that, in all fairness, the Scripting Guy who writes this column doesn’t know as much about Michelangelo as he probably should. (Although he does know – or at least as he knows now, anyway – that this is the not same Michelangelo as the one from the Teenage Mutant Ninja Turtles.) He also knows that Michelangelo’s love of beauty didn’t extend to himself: he dressed poorly, ate even worse, and never bathed. (No, never.) Michelangelo was also a bit … eccentric. According to one story, after finishing his sculpture of Moses, Michelangelo took a hammer and whacked the statue on the knee, demanding, “Why don’t you speak to me?!?” OK ….

On top of all that, Michelangelo was also prone to bouts of depression. Towards the end of his life he noted that:

“I live alone and miserable, trapped as marrow under the bark of the tree …. Poor, old, beaten, I will be reduced to nothing, if death does not come swiftly to my rescue. Pains have quartered me, torn me, broken me and death is the only inn awaiting me."

Sounds like just another day at the office for the Scripting Guys.


How Can I Concatenate Files of the Same Name?

Hey, Scripting Guy! How can I concatenate all the files (taken from various folders) that have the same file name?

-- AS

Hey, AS. You know, the hardest part about selecting questions to be used in the Hey, Scripting Guy! column isn’t writing the scripts; instead, it’s figuring out how to go about testing the scripts. After all, many people write to us with questions we find both interesting and useful; unfortunately, in order to test them we’d have to set up two separate Active Directory forests, install both a Unix server and a Novell server, go back in time to the year 1542, and temporarily swap the orbits of Mars and Jupiter. Even for the Scripting Guys, that’s asking a lot.

At first glance, you might think that this question would fall into the same category; after all, how many people already have files with the same file name scattered all over their hard disk? Fortunately, though, the Scripting Guy who writes this column is lacking in both ambition and creativity; as it turns out, he had 49 different files named test.vbs located on his C: drive alone. That made it very easy for him to write—and then test—a script that grabbed all 49 of these files (regardless of location) and concatenated the contents into a file named C:\Scripts\Main_file.txt.

Interestingly enough, that was a script that looked an awful lot like this:

Const ForReading = 1
Const ForAppending = 8

strComputer = "."

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

Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_Datafile Where FileName = 'test' AND Extension = 'vbs'")

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objMainFile = objFSO.OpenTextFile("C:\Scripts\Main_file.txt", ForAppending, True)

For Each objFile in colFiles
    Set objFile = objFSO.OpenTextFile(objFile.Name, ForReading)
    strContents = objFile.ReadAll
    objFile.Close
    objMainFile.WriteLine strContents
Next

objMainFile.Close

To begin with, we define a pair of constants: ForReading, which we’ll use each time we open an instance of Test.vbs; and ForAppending, which we’ll use each time we add the contents of a Test.vbs file to Main_file.txt. Following that, we bind to the WMI service on the local computer, then use this line of code to retrieve a collection of all the files on the computer that have a FileName equal to test and a file Extension equal to vbs (note that it’s just the letters vbs and not .vbs, with a dot in front of it):

Set colFiles = objWMIService.ExecQuery _
("Select * from CIM_Datafile Where FileName = 'test' AND
Extension = 'vbs'")

This is the point where we usually say, “Of course, seeing as how we’re using WMI, you could just as easily run this script against a remote computer.” However, we aren’t going to say that this time around. Granted, we could use WMI to retrieve the equivalent collection of files from a remote computer. However, we’ll run into problems trying to open those files; that’s because the FileSystemObject isn’t designed to work with files stored on remote computers. Consequently, this particular script is for the local machine only.

Note. Of course, you could always try modifying the script so that it does work with files stored on remote computers. For a few hints on how to do that take a look at this Hey, Scripting Guy! column.

With our collection in hand we create an instance of the Scripting.FileSystemObject, then use this line of code to open the file C:\Scripts\Main_file.txt:

Set objMainFile = objFSO.OpenTextFile("C:\Scripts\Main_file.txt", ForAppending, True)

Note. What’s the second parameter – True – that we added to the OpenTextFile method? As it turns out, that’s an optional second parameter that tells the FileSystemObject to create the file Main_file.txt if that file doesn’t already exist.

Once the file is open we set up a For Each loop to walk through the previously-retrieved collection of files that have the name Test.vbs. Inside that loop we start off by opening the file in question (passing OpenTextFile the Name of the file, which is equivalent to the file path) and then reading the entire contents into a variable named strContents. That’s what we do with these two lines of code:

Set objFile = objFSO.OpenTextFile(objFile.Name, ForReading)
strContents = objFile.ReadAll

After closing the first instance of Test.vbs, we then use the WriteLine method to append the value of strContents to the file Main_file.txt:

objMainFile.WriteLine strContents

And then we loop around and repeat the process with the next instance of Test.vbs in the collection. After we’ve looped through the entire collection we close the file Main_file.txt and call it a day.

This should answer your question, AS. Oh, and by the way: thanks for asking a question that didn’t require us to go back in time. The last time we did that we went back to the year 1742 and – much to our horror – the first person we encountered was Scripting Guy Peter Costantini as a child. Hopefully that meeting won’t do anything to change the future. (Although, based on the direction the Scripting Guys are headed, maybe changing the future wouldn’t be such a bad thing.)


How Can I Convert a Comma-Separated Values File to a Tab-Separated Values File?

Hey, Scripting Guy! How can I convert a comma-separated values file to a tab-separated values file?

-- RV

Hey, RV. Depending on the nature of your comma-separated values (CSV) file this is either very easy or a little bit tricky. Let’s follow the Scripting Guys Philosophy of Life and look at the easy one first.

Note. Of course, in our real Philosophy of Life we’d take the easy one first and then hope that there wouldn’t be time to do the hard one as well. Today, however, we’ll make an exception and tackle both the easy one and the hard one.

Let’s assume we have a very simple CSV file, one that looks like this:

a,b,c,d,e,f
g,h,i,j,k,l
m,n,o,p,q,r
s,t,u,v,w,x
y,z

How can we convert this to a tab-separated values file (TSV)? Like we said, this is pretty easy. We’ll begin by opening the file and reading the entire contents into a variable. Once the file contents are in memory we’ll replace all the commas with tabs and then re-save the file. Just like magic we’ll have turned a CSV file into a TSV file. Here’s the code that does the trick:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\scripts\test.txt", ForReading)
Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strLine = Replace(strLine, ",", vbTab)
    strNewText = strNewText & strLine & vbCrLF
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\scripts\test.txt", ForWriting)
objFile.WriteLine strNewText

objFile.Close

We begin by defining two constants – ForReading and ForWriting – that we use when opening and then re-opening our file (C:\Scripts\Test.txt). And, yes, we said re-opening: when using the FileSystemObject to work with text files you can open a file for reading or you can open a file for writing; unfortunately, you can’t do both at the same time. Because of that we’ll need to open the file, read the contents into memory, close the file, and then re-open that same file for writing in order to save the modified data.

After defining our constants we create an instance of the FileSystemObject and use the OpenTextFile method to open the file for reading. We then create a Do Loop in which we’ll read in the existing CSV file and then construct our new TSV file.

How do we do that? We begin by using the ReadLine method to read the first line of the file and store it in a variable named strLine. Next we use the VBScript Replace method to replace all the commas in the variable with tab characters; that’s what happens here:

strLine = Replace(strLine, ",", vbTab)

Note that we use the VBScript constant vbTab to indicate the tab character; had we used something like “tab” we would have replaced all the commas with the word tab. Not exactly what we had in mind. Likewise, don’t try typing a set of double quotes, setting the cursor inside those double quotes and pressing the TAB key (“ “). That just won’t work.

After we replace the commas with tabs we can start putting together our revised file. In this line of code we assign the variable strNewText the current value of strNewText (which, the first time through the loop, will be nothing) plus the new, tab-delimited strLine plus a carriage-return linefeed (vbCrLf):

strNewText = strNewText & strLine & vbCrLF

The first time through the loop strNewText will look like this, with tabs replacing the commas:

a     b     c     d     e     f

We then loop around and repeat the process with line 2. By the time we’ve finished reading in all the lines of the file the variable strNewText will hold this value:

a     b     c     d     e     f
g     h     i     j     k     l
m     n     o     p     q     r
s     t     u     v     w     x
y     z

As you can see, we’ve removed all the commas and replaced them with tabs. All we have to do now is close the file and then re-open it for writing. With the file re-opened we can use the WriteLine method to replace the existing contents of the file with the value of strNewText. And that’s it: our CSV file is now a TSV file. And we did it all PDQ ASAP.

See, that was pretty easy, wasn’t it? Now, what about that harder scenario? Well, suppose we have a CSV file that looks like this:

"a","b,c,d,e","f"
"g","h","i"
"j","k","l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"

This is still a CSV file; the difference is that individual items are enclosed in double quote marks. That’s because some of the items include commas themselves. For example, the first line in the file is actually composed of these three items:

a     b,c,d,e     f

Why do we care about that? Well, suppose we use our previous script and simply replace all the commas with tabs. If we do that, our first line will be divvied up like this:

"a"     "b     c     d     e"     "f"

Oops; that’s not even close to being right. The problem (aside from the double quotes) lies with the commas found inside the double quote marks; those commas should not be converted to tabs. How are we going to deal with that?

Well, to be honest, the best way to deal with this issue would be to use regular expressions. However, explaining how regular expressions work lies way beyond the scope of this column; to learn more about that we encourage you to view the Scripting Guys webcast on the subject. In the meantime, we’ll show you a solution that, while not 100% foolproof, should work in most situations:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\scripts\test.txt", ForReading)
Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strReplacer = Chr(34) & "," & Chr(34)
    strLine = Replace(strLine, strReplacer, vbTab)
    strLine = Replace(strLine, chr(34), "")
    strNewText = strNewText & strLine & vbCrLF
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\scripts\test.txt", ForWriting)
objFile.WriteLine strNewText

objFile.Close

The difference between this script and the first one we showed you occurs when we go about replacing commas in a line. We now do that using these three lines of code:

strReplacer = Chr(34) & ","
& Chr(34)
strLine = Replace(strLine, strReplacer, vbTab)
strLine = Replace(strLine, chr(34), "")

In the first line we’re assigning the value Chr(34) & "," & Chr(34) to a variable named strReplacer; that means strReplacer will now be equal to this:

","

What was the point of that? Well, as we’ve already seen, we can’t search for and replace all commas. Therefore, what we’re going to do in line 2 is search for double quotes followed by a comma followed by another set of double quotes. Sounds crazy but if you take a look at line 1 in our text file you’ll see that this is actually the pattern used to separate items:

"a","b,c,d,e","f"

In other words, replacing all instances of “,” will yield this:

"a     b,c,d,e     f"

As you can see, that’s pretty close to what we want; we just need to do something about those extraneous double quotes. That’s what we do with our third line of code, which replaces any remaining double quote marks with nothing:

strLine = Replace(strLine, chr(34), "")

The net result? Just what the doctor ordered:

a     b,c,d,e     f

Again, not a foolproof method, but it will work in most cases. If you find that it doesn’t work in your case, then view the regular expressions webcast.


How Can I Create a CSV File?

Hey, Scripting Guy! How can I create a CSV file?

-- LF

Hey, LF. For those of you who aren’t up on your acronyms, CSV is short for comma-separated-values file, a text file in which individual elements are separated by commas. For example, say you have a CSV file consisting of user first names, last names, and job titles; that file might look something like this:

Ken,Myer,Accountant
Pilar,Ackerman,Vice-President
Carol,Philips,Research Specialist

Incidentally, at Microsoft we love acronyms. The other day, for example, the Scripting Guys were sent a three-sentence product description that contained 11 acronyms. Our personal favorite – used extensively around here – is OOF, which is somehow short for Out Of Office.

No wonder we felt the need to add a spellchecker to all our products.

But back to your question. How can you create a CSV file all your own? That’s easy: just use the FileSystemObject. After all, that’s what it’s here for.

Let’s take a look at a very simple demonstration script, then we’ll take a look at a more practical example. Here’s a sample script that writes the string A,B,C to a text file named Test.csv:

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objLogFile = objFSO.CreateTextFile("test.csv", _
    ForWriting, True)

objLogFile.Write "A,"
objLogFile.Write "B,"
objLogFile.Write "C"
objLogFile.Writeline

objLogFile.Close

We start out by defining a constant named ForWriting and setting the value to 2; any time you use the FileSystemObject, you need to use the appropriate constant, depending on whether you want to read from, write to, or append a file. We create an instance of the FileSystemObject, then use the CreateTextFile method to create a new text file named Test.cvs. (Notice that we did not specify a path; that means that the text file will be created in the same folder as the script. Had we wanted to, we could have specified a complete path, like C:\Scripts\Logfiles\Test.csv.)

When we call the CreateTextFile method, we simultaneously create an object reference to the new file; in our script, we named that object reference objLogFile (although we could have called it anything we wanted). With the object reference in hand, we can then use the Write method to write data to the file. Notice that we begin by writing the string A, (and, yes, we must manually include the commas when creating a CSV file). The Write method writes the specified data (in this case A,), and then leaves the cursor in place. Because of that, when we next call this method the string B, is tacked on right next to the A,. As a result, our text file looks like this:

A,B,

We call the Write method a third time, and write C. Because this marks the end of the line, we don’t append a comma after it; instead, we call the WriteLine method, which is equivalent to hitting ENTER on the keyboard. That gives us a text file that looks like this:

A,B,C

Note that there are no spaces between commas and the start of the next item. When one item ends, the next begins right away.

Now let’s take a look at a more practical example. This script uses WMI to retrieve service information, then uses the FileSystemObject to write that information to a text file named Service_List.csv:

Const ForAppending = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objLogFile = objFSO.CreateTextFile("service_list.csv", _
    ForWriting, True)

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colListOfServices = objWMIService.ExecQuery("Select * from Win32_Service")
For Each objService in colListOfServices
    objLogFile.Write objService.Name & ","
    objLogFile.Write objService.StartMode & ","
    objLogFile.Write objService.State
    objLogFile.Writeline
Next

objLogFile.Close

As you can see, we use the same exact technique used in our demonstration script. The biggest difference is that we aren’t using hard-coded values like A, B, and C. Instead, we’re using variables like objService.Name. That’s not a problem, we just use the variable as the parameter to the Write method. But because these are variables, we can’t enclose them and the following comma in double quotes. A line of code like this will not work:

objLogFile.Write "objService.Name,"

Instead, we specify the variable (objService.Name), and then use the ampersand to tack a comma (“,”) to the end. We do the same thing for the StartMode but not for State; that’s because State is the last item on each line. Consequently, we use WriteLine to press ENTER and start a new line in the file (the same way we sued WriteLine after the letter C in our first example). Because we do this within a For Each loop, we’ll end up writing this information for each service installed on the computer.

The net result is a text file that looks like this:

Alerter,Manual,Stopped
ALG,Manual,Stopped
AppMgmt,Manual,Stopped
aspnet_admin,Auto,Stopped

Although we could have cut down the number of lines of code slightly (by concatenating all our variables and what-not) writing the code in this fashion makes it pretty easy for you to see what’s going on. It also makes it easy for you to add new items to each line. Want to include the service PathName? Then just add a line of code like this:

objLogFile.Write objService.PathName & ","

One more thing: When you’re working with WMI, the preceding script will likely handle all your needs. When working with other scripting technologies, however, you might encounter a problem with data that includes commas. For example, suppose you have a text file of users, their office addresses, and their job titles. One user has an address of 2049, but another has an address of 2050, Suite A. Your subsequent text file looks like this:

Ken,Myer,2049,Accountant
Pilar,Ackerman,2050,Suite A,Vice-President

Yikes! We have a problem: our first line contains four fields (remember commas indicate the end of one field and the beginning of the next): Ken / Myer / 2049 / Accountant. Unfortunately, because of the comma in the address itself, our second line has five fields: Pilar / Ackerman / 2050 / Suite A / Vice-President. Uh-oh ….

How do you deal with embedded commas like this? The secret is to surround each field with double quotes; if your text file looks like this, the embedded commas are ignored:

"Ken","Myer","2049","Accountant"
"Pilar","Ackerman","2050,Suite A","Vice-President"

And how do you surround your fields with double quotes. Here’s another sample script that uses the function Chr(34) to write a double quote mark, writes a service property (such as objService.Name), uses Chr(34) to write another double quote mark, and only then tacks a comma on the end.

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objLogFile = objFSO.CreateTextFile("service_list.csv", _
    ForAppending, True)

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colListOfServices = objWMIService.ExecQuery("Select * from Win32_Service")
For Each objService in colListOfServices
    objLogFile.Write chr(34) & objService.Name & chr(34) & ","
    objLogFile.Write chr(34) & objService.StartMode & chr(34) & ","
    objLogFile.Write chr(34) & objService.State  & chr(34)
    objLogFile.Writeline
Next

objLogFile.Close

For more information, check out this portion of the Microsoft Windows 2000 Scripting Guide. And we already know your next question: how do you read a CSV file using a script? For the answer, take a look at this Scripting Clinic column on reading text files using ADO.


How Can I Delete Just the IP Addresses From Each Line in a Text File?

Hey, Scripting Guy! I have a file that has IP addresses and server names on each line. How can I delete the IP addresses, leaving just the server names?

-- RR

Hey, RR. In case you’re wondering, as we’re writing today’s column temperatures are still right around freezing and there’s still snow and ice everywhere you look. Admittedly, many of you aren’t feeling particularly sorry for the Scripting Guys. “It’s 33 degrees and you’re saying it’s too cold?!? Holy smokes, here in [insert place name] it’s 3 degrees and you don’t hear anyone whining and crying about the weather! Is everyone in Seattle a wimp?”

Well, here’s what we have to say about that: yes, yes we are. Each and every one of us.

In fact, not only are we all wimps, but some of us are downright weird as well. For example, the other morning the Scripting Guy who writes this column arrived at the bus stop to find several people dutifully waiting for the good old 245. One guy waiting was wearing a heavy sweatshirt with the hood pulled up over his stocking cap. He was also wearing tennis shoes with no socks and a pair of running shorts. As he stood there, holding a pair of gloves in his hands (no, not on his hands, in his hands) he said to no one in particular, “Sure is cold this morning, isn’t it?”

Yeah, sure is, isn’t it?

So how do weather wimps like us get through these gloomy winter days? Well we can’t speak for any of our fellow Seattleites, but the Scripting Guys spend their time writing scripts that can delete just the IP addresses from a file, leaving behind nothing but server names:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    arrItems = Split(strLine, ", ")
    strNewContent = strNewContent & arrItems(1) & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strNewContent
objFile.Close

Ah, yes: there’s nothing like a script to make you feel all warm and toasty, is there? We’ll explain how this script works in just a second. Before we do that, however, we should note that we’re assuming that the text file in question looks something like this:

192.168.1.1, atl-dc-01
192.168.1.2, atl-dc-02
192.168.1.3, atl-dc-03
192.168.1.4, atl-dc-04

What we want it to look like is this:

atl-dc-01
atl-dc-02
atl-dc-03
atl-dc-04

How are we going to do that? Here’s how.

To begin with, we define a pair of constants, ForReading (with a value of 1) and ForWriting (with a value of 2); we’ll use these constants when we open our text file. And yes, we need two constants because we’ll actually have to open the text file twice: once to read in the existing contents, and once to save the revised contents. That’s just the way you have to do things when working with text files.

After defining the constants we then use these two lines of code to create an instance of the Scripting.FileSystemObject and to open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

And now the fun begins.

For starters, we create a Do Until loop that runs until we’ve reached the end of the text file (or, as we Seattleites like to say, until the file’s AtEndOfStream property is True). Inside the loop we use the ReadLine method to read the first line of the text file and store it in a variable named strLine. That means that, the first time through the loop, strLine will be equal to this:

192.168.1.1, atl-dc-01

That’s all well and good, of course, except for one thing: that’s not what we want strLine to be equal to. Instead, we want it to be equal to this:

atl-dc-01

Consequently, in our next line of code we use the Split method to create an array (named arrItems), designating a comma followed by a blank space (, ) as the delimiter we want to split on. What will that do for us? That will give us an array consisting of these two items:

· 192.168.1.1

· atl-dc-01

As you can see, the second item (which has an index number of 1; remember, the first item in an array always has an index number of 0) just happens to be the name of the computer, the value we want saved in our text file. Because of that we use this line of code to grab the value of item 1 and store it in a variable named strNewContent:

strNewContent = strNewContent & arrItems(1) & vbCrLf

OK, you’re right: we’re actually storing more than just the value of that array item in strNewContent. Instead, we’re assigning strNewContent the existing value of strNewContent plus array item 1 plus a carriage return-linefeed (vbCrLf). Of course, the first time through the loop strNewContent won’t have a value; therefore, strNewContent will end up being equal to this:

atl-dc-01

We then loop around and repeat the process with the second line in the text file. That results in arrItems(1) being equal to atl-dc-02 and, when all is said and done, makes strNewContent equal to this:

atl-dc-01
atl-dc-02

And then we simply loop around and start all over again, repeating the process until we’ve read and modified each and every line in the text file. When we’re done strNewContent will be equal to this:

atl-dc-01
atl-dc-02
atl-dc-03
atl-dc-04

Which, remarkably enough, is exactly what we want our text file to look like.

The rest is easy. We close the file Test.txt and then immediately reopen it, this time for writing (using the constant ForWriting). We use the WriteLine method to write the value of strNewContent to the file, then close the file for the last time. At that point we can go back to whining and complaining about the weather.

Incidentally, as an alumnus of the University of Washington the Scripting Guy who writes this column feels he should add one final note. The crazy guy at the bus stop, the one who was wearing running shorts and no socks and yet was complaining about it being cold? His sweatshirt was emblazoned Washington State University. No further comment needed, if you know what we mean.


How Can I Delete Just the Last Line of a Text File?

Hey, Scripting Guy! How can I delete just the last line of a text file?

-- AD

Hey, AD. You know, it’s been a long, hard day here at Microsoft, one of those days when everyone and everything seems to be against you. Many people, when faced with a day like today, turn to the so-called comfort foods: meatloaf, beef stew, mashed potatoes and gravy, any food that’s familiar and reliable. When you’re a Scripting Guy faced with a day like today, you turn to the so-called comfort scripts: reading from and writing to text files. Familiar and reliable.

Note. Admittedly, we Scripting Guys would prefer to turn to comfort foods ourselves. However, we’re stuck with comfort scripts, at least until they start putting mashed potatoes and gravy into the vending machines around here.

It’s nowhere near as good as a meatloaf sandwich, but here’s a script that deletes just the last line of a text file:

Const ForReading = 1
Const ForWriting = 2

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

arrLines = Split(strContents, vbCrLf)

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)

For i = 0 to UBound(arrLines) - 1
    objFile.WriteLine arrLines(i)
Next

objFile.Close

Actually, no, we hadn’t planned on explaining how the script works; when you order a meatloaf sandwich no one ever explains how that’s made, do they?

Oh, fine; we’ll see what we can do. We start out by defining a pair of constants, ForReading and ForWriting; we’ll use these constants to specify read-only or write-only mode when opening the text file. As usual, we need to open this file twice. In just a second we’re going to open the file for reading, and read in the existing contents. We’ll close the file, modify the contents in memory, then reopen the file and save the modified contents (that is, the old contents minus the very last line). It’s a bit of a hassle, but we have little choice here: the FileSystemObject won’t let us open a file for both reading and writing. Instead, at any given time we’re limited to doing one of those operations or the other.

And, yes, that is a bit like eating the mashed potatoes, and only then being able to eat the gravy, isn’t it?

After defining the two constants we create an instance of the Scripting.FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Testtxt. With the file open for reading we then use this line of code to read the entire text file and store the contents in a variable named strContents:

strContents = objFile.ReadAll

With the entire file stashed safely away in memory we then call the Close method to (temporarily) close the file.

Now what? As it turns out, our next step is to use the Split function to split the contents of the file into an array name arrLines, with each item in the array representing a line in the text file. We do that by splitting on the carriage return-linefeed character (or, as VBScript knows this character, vbCrLf):

arrLines = Split(strContents, vbCrLf)

Why do we do that? Well, the FileSystemObject doesn’t really know much about text files; for example, it doesn’t know which line is the last line in a given file. In that respect, arrays are a little smarter than the FileSystemObject. After all, any array can tell what its last item is: you can determine the last item in an array simply by calling the Ubound function, which returns the index number (subscript) of that item. For example, if the Ubound function returns a 13, then the last item in the array must have an index number of 13; in turn, that would mean thet we’re dealing with arrLines(13).

Hold on; we’re getting to that. So why is it useful to know that which item is the last item in the array? Well, we don’t want the last item to be included in our revised text file. If we know that the last item is item 13, then we know that our revised text file should only include items 0 through 12 (remember, the first item in an array is always item 0). We want to grab items 0 through 12 and then stop; that effectively deletes the last line in the text file.

Good idea: maybe we should just show you what we mean. After creating the array we reopen the text file, this time for writing; as you know, information we write to the file will completely replace the existing contents of the file. We then set up a For Next loop that runs from 0 to the Ubound value of the array minus 1:

For i = 0 to UBound(arrLines) - 1

Boy, you have a lot of questions today, don’t you? Why Ubound minus 1? Well, the Ubound function identifies the last item in the array. As we know, we don’t want the last item; however, we do want all the items up to – but not including – the last item. If the last item in the array is item 13, taking the Ubound value (13) and subtracting 1 leaves us with 12, which turns out to be the very value we need in our For Next loop. (To get items 0 through 12 our loop needs to run from 0 to, well, 12.) That’s why we use Ubound minus 1.

Inside our loop, and on the first go-round, we use the WriteLine method to write the value of array item 0 (the first item in the array, represented by the counter variable i) to the text file:

objFile.WriteLine arrLines(i)

We then loop around and repeat the process with the counter variable i equal to 1, meaning that we’ll write the value of array item 1 to the text file. This continues until we write the value of item 12 (the next-to-last item in the array) and then exit the loop. At that point we close the file and call it a day, without ever writing the last item in the array to the text file. We took a somewhat roundabout path here, but the net effect is that we’ve deleted the last line in the text file (simply by rewriting all the lines except the last line).

So will this really work? Of course it will. Suppose our text file looks like this, with the letter E being the actual last line of the file (that is, there’s no blank line following it):

A
B
C
D
E

What do you get back when you run the script? This:

A
B
C
D

Note. So what if your text file does have a blank line at the end? One thing you could do is check for that possibility – and remove the blank line – before you convert the text file to an array. Fortunately, we have a Hey, Scripting Guy! column that tells you how to do that very thing.

And that’s how the script works. As for the meatloaf sandwich, take two pieces of bread, put a little mustard on both pieces, and then slap a hunk of meatloaf in the middle. Feel free to use this recipe any time. Just make sure you credit the Scripting Guys.


How Can I Delete the First Line and the Last Line in a Text File?

Hey, Scripting Guy! How can I delete the first line and the last line in a text file?

-- TF

Hey, TF. We don’t know if anyone has ever noticed, but Hey, Scripting Guy! tends to meander from time-to-time; there are those occasions when it seemingly takes forever for us to get around to answering the question of the day. Up until now we never really thought too much about that; so what if we don’t get right to the point and take care of business? However, after what happened Sunday morning, we’re beginning to re-think that approach.

So what did happen Sunday morning? Well, Sunday morning the Scripting Guy who writes this column ran to the store to buy doughnuts. Now, the Scripting Guy who writes this column appreciates the importance of picking out the correct doughnuts; after all, that’s likely the most important decision he’ll make this week. (Maybe the only decision he’ll make this week.) However, the man who reached the doughnut case seconds before the Scripting Guy who writes this column might have taken this careful consideration to the extreme.

After getting his doughnut box and picking up one of the blue doughnut-grabbing tissues, the man stood in front of the case for what had to be several minutes, carefully pondering his choices. He grabbed a single glazed doughnut, placed it in the box, then resumed pondering. After several minutes he grabbed a maple bar, put that in his box, then pondered some more. The Scripting Guy who writes this column was too polite to do what he wanted to do, which was grab the man, shake him a couple times, and tell him, “These are doughnuts, for crying out loud! It doesn’t matter; you can’t go wrong with any kind of doughnut! Grab 12 somethings and get out of my way!”

Ay-yi-yi.

This pattern continued for hours. Even worse, at one point the man changed his mind and put the glazed doughnut back! After taking several hours to pick out 5 doughnuts he was now back to having just 4! Awhile later he decided to rearrange the doughnuts in his box, moving the maple bar from the left side to the right and putting the two jelly doughnuts next to each other.

That’s right: ay-yi-yi.

Under any other circumstances the Scripting Guy who writes this column would have gotten tired of waiting and just given up. But these are doughnuts we’re talking about! So he waited. And waited.

Note. Of course, he could have done what these two women did, which was simply reach over and under the man and grab a few doughnuts of their own, at one point almost knocking the poor guy down in their quest to get a cinnamon twist. The Scripting Guy who writes this column decided he didn’t want doughnuts that bad.

At any rate, we realized that sometimes people appreciate it more if you just get to the point. And so we vowed that, from now on, Hey, Scripting Guy! would always get right to the point. No more meandering and messing around.

Although, considering the fact that we’re now 8 or 9 paragraphs into this column and all we’ve talked about are doughnuts, well ….

But, hey, you know what they say: good things come to those who wait. Sometimes that means doughnuts, and sometimes that means scripts that can remove the first line and the last line from a text file:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("c:\Scripts\Test.txt", ForReading)

strText = objTextFile.ReadAll
objTextFile.Close

arrLines = Split(strText, vbCrLf)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

For i = 1 to (Ubound(arrLines) - 1)
    objFile.WriteLine arrLines(i)
Next

objFile.Close

OK, let’s see if we can figure out how this thing works. We start out by defining a pair of constants, ForReading and ForWriting; we’ll need one of these constants each time we open the text file. And before you ask, yes, we’re going to have to open the text file twice. Because we can’t directly edit a text file using a script we need to open the file once in order to read from it. We’ll read the file, store the contents in memory, then “remove” the first and last lines from this virtual copy of the file. We’ll then open the file a second time (this time for writing) and replace the original contents with the virtual copy.

With that in mind, our next step is to create an instance of the Scripting.FileSystemObject and then open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("c:\Scripts\Test.txt", ForReading)

From there we use the ReadAll method to read the file and store the contents in a variable named strText. Once the file is read we then call the Close method and close the text file.

Let’s assume that this text file (and the value of the variable strText) looks like this:

This is line 1.
This is line 2.
This is line 3.
This is line 4.
This is line 5.

Our task now is to delete the first line and the last line. How are we going to do that? Well, for starters, we’re going to use the Split function and turn our virtual text file into an array:

arrLines = Split(strText, vbCrLf)

As you can see, we’re splitting the file on the carriage return-linefeed character (vbCrLf); that’s going to result in an array where each item in the array corresponds to a line in the text file. In turn, that makes this the first item in the array:

This is line 1.

It also makes this the last item in the array:

This is line 5.

Believe it or not, we’re now ready to write the revised contents back to the file. What’s that? You say we didn’t remove the first and last lines yet? That’s true, and, to tell you the truth, we have no intention of removing the first and last lines. (That’s why we said we would “remove” these lines.) But don’t worry; the first and last lines will not appear in the text file. Guaranteed.

Yes, it is kind of exciting, isn’t it: how will the Scripting Guys escape from this trap they seem to have set for themselves? Well, to begin with, we re-open the file C:\Scripts\Test.txt. But this time, we open the file for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

So what are we going to write to the file? We’re going to write the contents we read in from the text file – except, of course, for the first and last lines. That’s the reason we have this block of code:

For i = 1 to (Ubound(arrLines) - 1)
    objFile.WriteLine arrLines(i)
Next

What we’re doing here is setting up a For Next loop to loop through the items in the array arrLines. Wait, check that: to loop through most of the lines in the array arrLines. As you know, the first item in an array is always given the index number 0; that means that the second item in the array is given the index number 1. Well, notice where our For Next loop begins: it begins with item 1, the second item in the array. In other words, we’re skipping item 1 (and thus line 1) altogether.

You also know that the Ubound function identifies the last item in an array. OK, well, look at the endpoint of our For Next loop: Ubound(arrLines) – 1. See? The last item in our loop is the upper bound of the array (Ubound) minus 1. Or, to put it another way, the next-to-last item in the array. That means we’re skipping the very last item in the array as well. And that also means that we’re skipping the very last line in the text file.

So much for the For Next statement. Inside the loop we use the WriteLine method to write the value of each item in the array (except for the first item and the last item) back to the file C:\Scripts\Test.txt. What does that mean? That means that, when we’re all done, Test.txt is going to look like this:

This is line 2.
This is line 3.
This is line 4.

It’s our original file … minus the first and last lines. Pretty easy, wasn’t it?

As you know, we typically try to close each column with a punchline. Unfortunately, you’ve probably already guessed the punchline to today’s column. Sure enough, after selecting his doughnuts, the Scripting Guy who writes this column headed for the checkstand. Guess who was in line in front of him? And guess who couldn’t find his club card? And guess who started to hand the clerk a $10 bill and then decided he’d pay with exact change instead? And – well, you know the rest. Fortunately, the Scripting Guy who writes this column had picked up a dozen doughnuts; that ensured that he wouldn’t starve to death while he waited in line.


How Can I Eliminate Duplicate Names in a Text File?

Hey, Scripting Guy! I have a text file that contains a bunch of names. How can I read through that text file and eliminate all the duplicate names?

-- MW

Hey, MW. We’re assuming you have a text file that looks something like this:

Ken Myer
Dean Tsaltas
Jonathan Haas
Ken Myer
Dean Tsaltas
Syed Abbas
Gail Erickson
Carol Phillips
Dean Tsaltas
Dylan Miller
Kim Abercrombie
Dylan Miller

As you can see, there are a number of duplicate names in this list. For example, you have three Dean Tsaltas’. We actually know Dean Tsaltas and – take it from us – one Dean Tsaltas is enough for any organization! But how can you get rid of all those duplicate names?

Well, to tell you the truth there are several different ways. We decided to use the Script Runtime’s Dictionary object because we thought that, all things considered, it was the simplest and easiest approach. What we’re going to do is read in the names from the text file, one-by-one. We’ll read in the first name – Ken Myer – and then store that name in the Dictionary. We’ll then read in the second name but – before we add it to the Dictionary – we’ll check to see if the name is already in the Dictionary. If it’s not, we’ll add it. If it is in the Dictionary then we won’t add it and, instead, we’ll just go ahead and read in the third name. When we’re done, our Dictionary will contain a list of all the unique names, no duplicates.

Here’s a script that will echo back the unique names (we’ll modify this script in a minute so that it writes the unique names back to the text file):

Const ForReading = 1

Set objDictionary = CreateObject("Scripting.Dictionary")
Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile _
    ("c:\scripts\namelist.txt", ForReading)

Do Until objFile.AtEndOfStream
    strName = objFile.ReadLine
    If Not objDictionary.Exists(strName) Then
        objDictionary.Add strName, strName
    End If
Loop

objFile.Close

For Each strKey in objDictionary.Keys
    Wscript.Echo strKey
Next

We begin by defining a constant – For Reading – that we need when we open the text file. Next we create instances of two different objects: the Dictionary object and the FileSystemObject. Finally, we use the OpenTextFile method to open the file C:\Scripts\Namelist.txt for reading.

Now we’re ready to have some fun. What we do next is set up a Do Loop that enables us to read the text file line-by-line (and, by extension, name-by-name). Each time we read in a line, we store that value in a variable named strName. We then use this line of code to see if that particular name is already in the Dictionary:

If Not objDictionary.Exists(strName) Then

Yes, kind of clumsy syntax, but this can be read as “If the name stored in the variable strName does not exist in the Dictionary, then do the following line of code.” In that following line of code, we simply add that name to the Dictionary, using the value of strName for both the item value and item key. (For more information about the Dictionary object, see this section of the Microsoft Windows 2000 Scripting Guide.) If the name does exist in the Dictionary then we simply loop around and read in the next line. This continues until we’ve processed each line in the text file.

What does that give us? Well, it gives us a Dictionary that contains a set of keys, each key representing a unique name. If we want to see those names we can simply loop through the Keys collection and echo the value of each Dictionary key:

For Each strKey in objDictionary.Keys
    Wscript.Echo strKey
Next

Child’s play.

Of course, you didn’t necessarily want to see the unique names, you wanted to delete duplicate names from your text file. That’s easy enough. Our Dictionary object now contains the unique file names, with all the duplicate names deleted. To remove the duplicate names from the file itself all we have to do is open that file (for writing this time) and replace the existing contents with our Dictionary keys. Because the Dictionary keys represent the unique names writing those keys to the text file will give us a text file with unique names, no duplicates.

Here’s a modified script that writes the Dictionary keys back to the text file:

Const ForReading = 1
Const ForWriting = 2

Set objDictionary = CreateObject("Scripting.Dictionary")

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile _
    ("c:\scripts\namelist.txt", ForReading)

Do Until objFile.AtEndOfStream
    strName = objFile.ReadLine
    If Not objDictionary.Exists(strName) Then
        objDictionary.Add strName, strName
    End If
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile _
    ("c:\scripts\namelist.txt", ForWriting)

For Each strKey in objDictionary.Keys
    objFile.WriteLine strKey
Next

objFile.Close

If you run the script and then open the text file you should see something like this:

Ken Myer
Dean Tsaltas
Jonathan Haas
Syed Abbas
Gail Erickson
Carol Phillips
Dylan Miller
Kim Abercrombie

See? We’re down to one Dean Tsaltas. Now if we can just figure out a way to get rid of that Dean Tsaltas, then we’d have something. (Just kidding, Dean.)


How Can I Extract Text Embedded Between Target Phrases in a Text File?

Hey, Scripting Guy! How can I read a text file until I encounter a specified phrase, then write that information to a new text file? I then need to repeat this process, reading the text file to the next instance of the phrase and then writing that information to another new file. And so on.

-- EW

Hey, EW. You know, this has been a difficult year for the Scripting Guys. Back in February we said goodbye to Scripting Guy Peter Costantini, who left the team to take a position as a Program Manager here at Microsoft. Just a week or two ago we bid adieu to Scripting Guy Dean Tsaltas who, while still working at Microsoft, moved back to Halifax, Nova Scotia with the rest of his family. And now today we have the sad duty of saying goodbye to Scripting Guy Jean Ross.

Although Jean was a member of the team for a relatively short amount of time, there’s no way that we can understate her contributions both to the team and to the world of system administration scripting. As far as the Scripting Guy who writes this column is concerned, Jean Ross is, without question, the – sorry; could we put you on hold for a second here? Thanks.

Say that again? What do you mean you aren’t leaving the team? You have to leave the team; we wrote this moving tribute to you. Besides, if you don’t quit then we won’t have a column for today; we can’t very well publish a goodbye to Jean Ross if you aren’t leaving. You know, you’re really putting us in a very awkward position here; where’s your sense of team spirit?!

Um, excuse us folks; we seem to be having some … technical … difficulties here. While we sort out these issues why don’t we show you a script of some kind? What does this script do? Well, we’re not sure; something about reading a text file up to a specified point and writing that data to a text file, then doing the same thing up until the next specified point. But you guys are smart; you’ll figure it out.

Again, our apologies for the delay. We’ll be back with you as soon as we can. In the meantime:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll

objFile.Close

arrContents = Split(strContents, "#This is the end of the file.#")

i = 1

For Each strItem in arrContents
    strFileName = "C:\Scripts\NewFile_" & i & ".txt"
    Set objNewFile = objFSO.CreateTextFile(strFileName)
    objNewFile.Write strItem
    objNewFile.Close
    i = i + 1
Next

Before we explain how this script works, let’s take a look at our sample text file, a file that looks something like this:

Here is some text in our document.#This is the end of the file.#Here is
some more text in the document.#This is the end of the file.#Here is
the last bit of text in the document. #This is the end of the file.#

Note. Yes, a matter of fact Jean Ross did write this; it’s a rough draft for next month’s Sesame Script column. You can see why we’re going to miss her so much, can’t you?

Or at least we would miss her, if she’d ever actually leave.

If you look at this file closely, you’ll see that the phrase #This is the end of the file.# appears in a few different places. As you can probably guess, this is the target phrase, the one that separates one bit of information from another. In turn, that means that our script needs to do this:

· Read the text Here is some text in our document.

· After encountering the target phrase, save the preceding text to a text file (discarding the target phrase in the process).

· Read the text Here is some more text in the document.

· After encountering the target phrase, save that text to a text file.

· Read the text Here is the last bit of text in the document. and then save that to a third text file.

So how hard is that going to be? As it turns out, not very hard at all.

We start the script off by defining a constant named ForReading, assigning this constant the value 1; we’ll use ForReading when we set out to open the text file C:\Scripts\Test.txt. Speaking of which, we then use the following two lines of code to create an instance of the Scripting.FileSystemObject and open Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as the file is open, we use the ReadAll method to read in the entire contents of that file, storing the data in a variable named strContents. Once those contents are safely stashed away in memory we call the Close method and close Test.txt. For good.

All that serves as a prelude to this line of code:

arrContents = Split(strContents, "#This is the end of the file.#")

As we noted, our goal here is fairly straightforward: we need to identify – and save – all the text that comes between instances of our target phrase. Although there are several different ways we might be able to tease out this text, we decided that the simplest approach was to take the Split function and split the text file into an array named arrContents. Where exactly do we split the file? You got it: on each and every instance of the target phrase. (Not everyone is aware of this, but the Split function doesn’t limit you to splitting a string on a single character, say, a comma or a carriage return-linefeed.)

So what do we gain by this? Well, after we call the Split function arrContents will contain the following items:

· Here is some text in our document.

· Here is some more text in the document.

· Here is the last bit of text in the document.

And you’re absolutely right: those are the three separate bits of information we need to extract from the file. (That is, those items represent the text that falls between the target phrase.) The easiest way to get at that data was simply to call the Split function, something that discards the target phrase and stores the intervening text passages as items in an array.

The rest is easy. After assigning the value 1 to a counter variable named i, we set up a For Each loop that loops us through all the items in arrContents:

For Each strItem in arrContents

Inside that loop, we first use this line of code to construct a file path for our first text file:

strFileName = "C:\Scripts\NewFile_" & i & ".txt"

All we’re doing here is combining the string C:\Scripts\NewFile_ with the value of the counter variable and the string value .txt. That’s going to result in a file path like this:

C:\Scripts\NewFile_1.txt

Once we have the file path we can then call the CreateTextFile method to create a new text file:

Set objNewFile = objFSO.CreateTextFile(strFileName)

And what do you suppose we’re going to store in that text file? You got it: the value of the first item in the array arrContents. To get this data into the text file all we have to do is call the Write method, passing the variable strItem as the sole method parameter:

objNewFile.Write strItem

After that we close the file, increment the value of our counter variable by 1, and then repeat the process with the next item in the array. When all is said and done, our sample script will produce three separate text files, one for each item in the array:

C:\Scripts\NewFile_1.txt
C:\Scripts\NewFile_2.txt
C:\Scripts\NewFile_3.txt

And that, as they say, is that.

In the meantime we have good news for everyone: as it turns out, Scripting Guy Jean Ross isn’t leaving the team after all. (No, we tried paying her off; she took our money, then insisted on staying anyway.) And yes, that is good news for the scripting world; after all, if Jean was no longer a Scripting Guy then the scripting world would no longer reap the benefits of all the great things that Jean does.

Well, OK: all the great things that Jean undoubtedly will do.

Eventually.

Note. The odds are probably against this, but if there’s another set of Scripting Guys out there, and one of those Scripting Guys is named Jean Ross, and that Jean Ross is leaving the team, well, please let us know. We can make you one heck of a deal on a we’re-so-sad-to-see-Jean-go column.


How Can I Find and Replace Text in a Text File?

Hey, Scripting Guy! From the command line, how can I use a script to open a file and replace text; for example, how can I replace all instances of “Jim” with “James”?

-- JW

Hey, JW. As we’ve found out numerous times when dealing with text files, there is no obvious way to do this; that is, there is no ReplaceText command that can open a text file and find and replace text. Fortunately, this problem is typical of text file questions in one other respect: although there’s no obvious way to carry out the task, we can still find a way to get the job done.

Although we can’t directly search and replace text inside a text file, we can do the next best thing. We can: 1) open up a text file; 2) read the text into a variable; 3) do a search-and-replace on that variable; and 4) re-save the text file. We can even do all that from the command line, although we’ll hold off on that for a moment. Instead, let’s start with a simple script that carries out the search and replace:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Text.txt", ForReading)

strText = objFile.ReadAll
objFile.Close
strNewText = Replace(strText, "Jim ", "James ")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Text.txt", ForWriting)
objFile.WriteLine strNewText
objFile.Close

We start off by creating two constants (ForReading and ForWriting), which we’ll use for the two occasions when we’ll open our text file. (Yes, we said two occasions). We create an instance of the FileSystemObject, and then use the OpenTextFile method to open the file C:\Scripts\Text.txt for reading.

With the file open, we use the ReadAll method to read the contents of the entire file into the variable strText. We then close C:\Scripts\Text.txt even though we’ll almost immediately reopen it, this time for writing. Seems silly, yes, but that’s the way the FileSystemObject works: you can open a file for reading or you can open a file for writing, but you can’t perform both operations at the same time. (As you should know by now, the FileSystemObject works in mysterious ways.)

Having stored the contents of the file in the variable strText, we then use the VBScript Replace function to replace all instances of Jim with James. That’s what we do in this line of code:

strNewText = Replace(strText, "Jim ", "James ")

Notice that we’re looking for each instance of "Jim " (Jim followed by a blank space) and replacing those with "James " (James followed by a blank space). Though hardly foolproof, this gives our script a tiny bit of intelligence; if the script encounters the name Jimmy it won’t try to replace the Jim with James (resulting in Jamesmy). The new file we create – the one where each Jim has been replaced by James – is stored in memory in the variable strNewText.

Next we reopen our file (for writing), call the WriteLine method to write the contents of strNewText to the file, and then close the file a second time. The net effect? If we started off with a text file that looked like this:

Jim Jones
Mary Smith
Jim Doe
Jim Johnson
Mary Johnston

We’ll end up with a text file that looks like this:

James Jones
Mary Smith
James Doe
James Johnson
Mary Johnston

As for doing this all from the command-line, we simply need to modify the script so that it will accept – in this order – three command-line arguments: the name of the file to open; the text we want to search for; and the text we want to replace. Here’s a script that does just that. Note that we store the command-line arguments in the variables strFileName, strOldText, and strNewText, and we use those variables when opening and saving the text file and when calling the Replace function:

Const ForReading = 1
Const ForWriting = 2

strFileName = Wscript.Arguments(0)
strOldText = Wscript.Arguments(1)
strNewText = Wscript.Arguments(2)

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile(strFileName, ForReading)

strText = objFile.ReadAll
objFile.Close
strNewText = Replace(strText, strOldText, strNewText)

Set objFile = objFSO.OpenTextFile(strFileName, ForWriting)
objFile.WriteLine strNewText
objFile.Close

To use this revised script (which we’ll call replace.vbs) just type a command similar to this from the command prompt:

cscript replace.vbs "C:\Scripts\Text.txt" "Jim " "James "


How Can I Increment the Last Line in a Text File?

Hey, Scripting Guy! How can I increment the last line in a text file; that is, if the last line is 1, indicating we’ve done one backup, I need to increment it to 2, indicating that we’ve now done a second backup.

-- TH

Hey, TH. If you’ve ever been to a carnival then you’ve no doubt been tempted by some of the sideshow games. Throw a baseball and knock down a few milk cans? Toss a dime into a glass bowl? Break a balloon with a dart, for Pete’s sake?!? How hard could that be?

Well, if you’ve never been to a carnival here’s a word to the wise: those things are actually way harder than they look. Just because something sounds like it should be easy doesn’t mean it really is easy. You’d be surprised how hard – and how expensive – it can be to toss a rubber ring onto a glass bottle.

Note. If you’re bound and determined to try these games anyway, you might find some helpful hints here. All part of the Scripting Guys customer service!

We mention all this because your question would make the perfect carnival game. After all, it sounds easy enough: you just open the text file, go to the last line, and change it. How hard could that be? Well, due to the limitations of the FileSystemObject (the scripting technology used to read and write text files) it’s actually much harder than it probably should be. That doesn’t mean this can’t be done; it just means that the script will – at first glance – look a little more complicated than you might expect. Forewarned is forearmed, you know.

In other words, the script will look like this:

Const ForReading = 1
Const ForWriting = 2

Dim arrFileLines()
i = 0

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
     Redim Preserve arrFileLines(i)
     arrFileLines(i) = objFile.ReadLine
     i = i + 1
Loop

objFile.Close

intLastLine = Ubound(arrFileLines)
intNumber = arrFileLines(intlastLine)
intNumber = intNumber + 1

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)

For i = 0 to Ubound(arrFileLines) - 1
    objFile.WriteLine arrFileLines(i)
Next

objFile.WriteLine intNumber

objFile.Close

Before we explain how the script works, we’re assuming you have a text file that looks something like this:

A
B
C
D
E
31

Granted, it probably doesn’t look exactly like that, but the point is that you have a few lines of text (and, most likely, you aren’t sure how many lines of text) followed by the backup number. The next time you run the script you want the text file to look like this:

A
B
C
D
E
32

In other words, everything remains the same except for the backup number, which gets incremented from 31 to 32. Given the limitations of the FileSystemObject (having to open a file for reading or for writing, but not for both; being unable to go directly to the last line in a file; etc.) how do we go about doing that?

Well, we start by creating a pair of constants: ForReading and ForWriting. We’ll use these later in the script to open our text file in the appropriate mode. (As we noted a moment ago, you must open a text file for reading or for writing; you can’t perform both operations simultaneously.) We then define an array variable named arrFileLines; we’ll use this variable to store the existing contents of the text file. Why? We’ll explain that momentarily.

After opening the file C:\Scripts\Test.txt, we then use this block of code to read the file line-by-line and store the individual lines as items in the array arrFileLines:

Do Until objFile.AtEndOfStream
     Redim Preserve arrFileLines(i)
     arrFileLines(i) = objFile.ReadLine
     i = i + 1
Loop

We do this for two reasons. First, we need to be able to access – and change – the very last line in the file. The FileSystemObject doesn’t have a method that enables us to jump directly to the last line in the file; by storing the individual lines in an array, however, we cause VBScript to access the last item in the array (which will also be the last line in the file.) In addition, the FileSystemObject doesn’t allow us to change a single line in a text file; instead, we’re going to have to replace the existing contents of Test.txt with a brand-new file. That new file will include the first x lines (unchanged) in Test.txt plus a new, modified last line. By storing the existing contents in an array it will be easy for us to recall all the lines in the file except for the very last line.

That might not make a whole lot of sense right now, but it should become a bit clearer when we actually update the file. And, yes, there are a couple other ways we could have done this, but this way (believe it or not) seemed the simplest and most straightforward.

After reading the file we immediately close it, then use these three lines of code to figure out the current backup number and increment it by 1:

intLastLine = Ubound(arrFileLines)
intNumber = arrFileLines(intlastLine)
intNumber = intNumber + 1

What we’re doing here is using the Ubound function to determine the index number of the last item in the array. (Items in an array are assigned index numbers: the first item in the array is item 0, the second item in the array is item 1, and so on.) Once we know the index number for the last item (in this case it will be 5, because we have 6 items – or lines – in our array) we can then assign the value of the last item to the variable intNumber. That takes place here:

intNumber = arrFileLines(intlastLine)

The value of the last item in the array is 31; that’s because the last line in our text file is 31. We then use this line of code to add 1 to the existing value (which, in this example, will add 1 to 31):

intNumber = intNumber + 1

Make sense? All we’ve done so far is just up the backup number from 31 to 32. But, really, that’s all we need to do: we’re now ready to re-open the file C:\Scripts\Test.txt and replace the current contents with the revised contents. To do that we use this block of code to write all the existing lines except the last line to the file:

For i = 0 to Ubound(arrFileLines) - 1
    objFile.WriteLine arrFileLines(i)
Next

In other words, we’re going to write the following lines to the text file:

A
B
C
D
E

Why didn’t we write the last line? Well, remember, in our array the last line is this:

31

We want our new, incremented value (32) to be the new last line in the file. Therefore, we use this line of code to tack the value 32 (representing the current backup number) to the end of the text file:

objFile.WriteLine intNumber

We then close the file and we’re done. Our text file should now look like this, which just happens to be the way we wanted it to look:

A
B
C
D
E
32

And, yes, it’s a bit unfair that you can get a stuffed bear for tossing a dime into a bowl, yet you probably won’t get a stuffed bear for incrementing the last line in a text file. Life can be very cruel sometimes, can’t it?


How Can I Insert a Page Break in a Text File After Each Line Where the Only Character is the Number 1?

Hey, Scripting Guy! How can I open a text file and insert a page break after each line where the only character is the number 1?

-- JR

Hey, JR. OK, to begin with, there’s no doubt that the Scripting Guy who writes this column doesn’t keep up with the world of fashion; for example, he considers “getting dressed up” to mean going out without a baseball hat, something he does only for a wedding, a funeral, or a good old-fashioned drenching. And there’s no doubt that the Scripting Guy who writes this column has some … interesting … ideas when it comes to fashion; the Scripting Editor, who has had occasion to listen to some of these rants, can vouch for that. (Or maybe not. Usually when he starts rambling on about why men shouldn’t wear sandals she always remembers that she left a pot of water boiling on the stove and leaves.)

Still, picture this if you will. (Or if you dare). A brisk summer morning in Seattle; the Scripting Guy who writes this column is sitting in the stands waiting for the Scripting Son’s baseball game to start. (And because you’re all dying to know this, yes, the Scripting Son pitched three innings of relief and picked up the win in that game.) In walks a guy wearing:

· A fleece jacket.

· Blue jean cutoff shorts. (And cut off a little too short, if you ask us.)

· A cowboy hat.

· Cowboy boots.

So was there anything wrong with that? Admittedly, there probably isn’t anything wrong with that. But maybe there should be.

Oh, well; to each his own, right? Besides, what does that have to do with inserting page breaks in a text file? Darned if we know. But at least the following script does have something to do with inserting page breaks in a text file:

Const wdReplaceAll = 2

Set objWord = CreateObject("Word.Application")
objWord.Visible = True

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.txt")
Set objSelection = objWord.Selection
objSelection.Find.Text = "^p1^p"
objSelection.Find.Forward = TRUE
objSelection.Find.Replacement.Text = "^p1^p^m"

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

First things first. To begin with, you might have noticed that this script uses Microsoft Word to open the text file and to insert the page breaks. Why? Well, as far as we know, the only way to insert page breaks into a text file is to use an application like Word; the FileSystemObject can’t insert page breaks into a text file, and the text file wouldn’t know what to do with those page breaks anyway. (Try finding the Insert Page Break command in Notepad. That’s going to be a little difficult, to say the least.) Because of that we need to open the text file in Word, insert the page breaks, and then save that file as a Word document. (That’s a step we left out in our sample script, but we have at least one article that tells how to save a file as a Word document.) Admittedly we no longer have a text file at that point, but we don’t know any way to work around that.

All right. Having done first things first, let’s now do second things second and explain how the script actually works. For starters, we define a constant named wdReplaceAll and set the value to 2; we’ll explain what that’s for in just a second. We then create an instance of the Word.Application object and set the Visible property to True; that gives us a running instance of Microsoft Word that we can see on screen. After that we use these two lines of code to open the file C:\Scripts\Test.txt and create an instance of Word’s Selection object:

Set objDoc = objWord.Documents.Open("C:\Scripts\Test.txt")
Set objSelection = objWord.Selection

Why do we need an instance of Word’s Selection object? That’s easy: in order to do a search-and-replace operation (which is what we’re about to do) we need to use the Find object. And Find just happens to be a child object of the Selection object.

Still with us? Good. As it turns out, JR has a text file that looks something like this:

Here's some text.
1
Here’s some more text.
1
Here's even more text.
And more text.
1
And here's the last bit of text.

Notice the lines that have nothing but the number 1 on them? Our task is to insert a page break after each of those lines.

And just how do we propose to do that? We’re glad you asked that question. What we’re going to do is search for all the number 1s that happen to be on a line all by themselves. How can we tell if the number 1 is on a line by itself? That’s also easy: if we have a paragraph return followed by the number 1 followed by another paragraph return then we must have a line where the number 1 is sitting there all by its lonesome. (If that doesn’t make sense do this: create a new Word document, press ENTER, type the number 1, then press ENTER again. Voila!) The following line of code lets us specify search Text that consists of a paragraph return (using Word’s ^p syntax) followed by the number 1 followed by another paragraph return:

objSelection.Find.Text = "^p1^p"

All we’ve done here is define the text we want to search for. Once that’s done we then set the Forward property to True; that tells the script to begin the search at the current selection point and then move forward through the document. Because the selection point is currently at the very beginning of the document (the default location when you create an instance of the Selection object) this ensures that our script searches the entire document from start to finish.

After that we define our replacement text:

objSelection.Find.Replacement.Text = "^p1^p^m"

As you no doubt recall, we’re searching for the following bit of text: ^p1^p. We’re now stating that we want to replace that bit of text with this: ^p1^p^m. And yes, you’re absolutely right: the ^m is Word’s syntax for a manual page break. The net effect here is that we’re adding a page break after each line in the file where the number 1 appears by itself. And that’s good; after all, that’s exactly what we want to do.

All that’s left now is to call the Execute method and actually perform the search-and-replace:

objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll

We won’t discuss all the empty parameters required for this operation; for details see our Office Space article on finding and replacing text in a Word document. For now just note that all the commas are required; that’s to ensure that the constant wdReplaceAll is passed in the correct position. In addition, the constant wdReplaceAll is required to ensure that all instances of the target text get replaced; without specifying this value the script will replace the first instance of the target text and then call it good.

And that would not be good.

That should do it, JR. Give this baby a try and see what happens.

As for the Scripting Guy who writes this column, he’s busy rethinking some of his opinions on fashion. For example, after seeing a guy wearing short shorts and cowboy boots he’s beginning to think that maybe guys wearing sandals isn’t such a bad thing after all.

That said, however, he still firmly and resolutely believes that – what’s that? You just remembered that you left a pot of water boiling on the stove? Well, OK; you better go take care of that. We’ll hold this thought until you get back.


How Can I Insert Commas at Specified Locations in Each Line in a Text File?

Hey, Scripting Guy! How can I insert commas at specified locations in each line in a text file?

-- MM

Hey, MM. Seeing as how Father’s Day has just come and gone we couldn’t talk about inserting commas into text files without first relating a heartwarming Father’s Day tale of our own. On Father’s Day the Scripting Guy who writes this column went to the gym to play basketball with the Scripting Son. At one point the ball took a funny bounce off the rim and skipped out towards half court. “Hey, Dad,” said the Scripting Son. “Watch this.” Without turning around he grabbed the ball and flung it back over his head, ostensibly towards the basket.

Now, had the Scripting Son made the basket that would have been pretty impressive; nevertheless, it wouldn’t have been anywhere near as cool as what happened instead. As it turns out, the gym the duo plays at has a small bathroom at one end, and the last person to use this bathroom neglected to close the door. When the Scripting Son flung the ball over his head, the ball zoomed through the bathroom door, rattled off the sink a few times, and then landed in the toilet.

You couldn’t have asked for a better Father’s Day present. Or at least not a funnier one.

Note. For those of you who keep track of this sort of thing, that ranks as the Scripting Son’s second-greatest basketball achievement. Earlier this year he took an ill-advised, off-balance jump shot from behind the three-point line. The shot was way off-target; in fact, it sailed completely over the backboard … at which point it hit the wall, caromed off the supports holding up the basket, then somehow bounced back over top of the backboard and dropped down through the hoop. Three points for the Scripting Son.

Admittedly, it’s a bit difficult, if not downright impossible, to top a basketball landing in a toilet. But if anything can top a basketball in the toilet, it would have to be a script that inserts commas at specified locations in each line in a text file:

Const ForReading = 1
Const ForWriting = 2

arrCommas = Array(2,7,11,17,18,20)

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    intLength = Len(strLine)
    For Each strComma in arrCommas
        strLine = Left(strLine, strComma - 1) + "," + Mid(strLine, strComma, intLength)
    Next
    strText = strText & strLine & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strText
objFile.Close

So how does this all work? That’s a good question. Let’s see if we can figure out what’s going on here.

We actually start out simply enough, defining a pair of constants (ForReading and ForWriting) that we’ll need when it comes time to open our text file. (And yes, we need two constants because we have to open the text file twice: once to read in the existing contents and once to write the revised contents back to the file.) We then use this line of code to create an array named arrCommas:

arrCommas = Array(2,7,11,17,18,20)

Why did we assign these particular numbers to the array? That’s easy: these values represent the character positions where we want to insert our commas. For example, suppose our text file looks like this:

ABBBBCCCDDDDDEFFFFFFFFFFFFF
GHHHHIIIJJJJJKLLLLLLLLLLLLL
MNNNNOOOPPPPPQRRRRRRRRRRRRR

We want to insert commas in character positions 2, 7, 11, 17, 18, and 20. That means that, when the script finishes, the text file will look like this:

A,BBBB,CCC,DDDDD,,E,FFFFFFFFFFFFF
G,HHHH,III,JJJJJ,,K,LLLLLLLLLLLLL
M,NNNN,OOO,PPPPP,,Q,RRRRRRRRRRRRR

The only way to achieve that result is to put commas in the proper spots:

1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21
A | , | B | B | B | B | , | C | C | C | , | D | D | D | D | D | , | , | E | , | F

You get the idea.

The next part is also easy; we create an instance of the Scripting.FileSystemObject object and then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Now it starts to get a little tricky.

For starters, we set up a Do Until loop that runs until the file’s AtEndOfStream property is True (which simply means that we want to keep reading until there’s nothing left to read). Inside this loop we use the ReadLine method to read the first line from the file and store it in a variable named strLine:

strLine = objFile.ReadLine

OK, so maybe that wasn’t so tricky after all. But that’s about to change. Our next step is to use the Len function to determine the number of characters in the variable strLine:

intLength = Len(strLine)

Why do we do that? Just be patient; we’ll explain that in a second.

Once we know the length of the line we then set up a For Each loop, a loop that runs through all the items in the array arrCommas:

For Each strComma in arrCommas

And what do you suppose we do inside that loop? This is what we do inside that loop:

strLine = Left(strLine, strComma - 1) + "," + Mid(strLine, strComma, intLength)

As it turns out, this is the tricky part. What we’re doing here is inserting all the required commas into the required places. Why is that so tricky? Mainly due to the fact that there’s no command we can call that will simply insert commas in the designated spaces. Instead, we have to do this comma-by-comma, character-by-character. For example, the very first thing we need to do is insert a comma in character position 2. How are we going to do that? Well, first we need to grab all the characters up to character position 2 (in other words, we need to grab the first character). That’s what this piece of code is for:

Left(strLine, strComma - 1)

All we’re doing here is using the Left function to grab the first x number of characters from the variable strLine. What is x equal to? x is equal to the first value in the array (2) minus 1. And why minus 1? If you think about it for a second, you’ll see why we need to specify the minus 1. If we want our comma to hold down position 2 that means that we can only grab one character; if we took the first 2 characters then the comma would actually be in position 3. And who wants a comma in position 3? Maybe someone else does, but not us.

Therefore, we take the first 2 – 1 characters and stash them (or, rather it: that’s simply going to bet the letter A) in the variable strLine. We then use this bit of code to tack a comma onto the end of strLine:

+ ","

In turn, that makes the variable strLine equal to this:

A,

That’s nice; we now have a comma in character position 2. But what about the rest of the line? Well, that’s what this piece of code is for:

+ Mid(strLine, strComma, intLength)

Here we’re using the Mid function to grab the remaining characters in the line. When you use the Mid function you need to specify three things: the string value you are working with (the variable strLine); the starting position in the string; and the ending position in the string. In this case we want to start with character position 2, the second character in the string. Fortunately, that’s easy to do, because strComma just happens to equal 2. (Remember, we’re not dealing with the revised string, we’re dealing with the actual line of text read in from the text file, a line of text that doesn’t have a comma in character position2). As for the ending position, we want to grab all the remaining characters. How many characters is that? To tell you the truth, we don’t know. But that’s fine: after all, we’ve stored that value in the variable intLength. (That’s why we needed to determine the length of the string.)

What does all that mean? That means we’re going to grab the following set of characters:

BBBBCCCDDDDDEFFFFFFFFFFFFF

And when everything gets added together, that means that strLine will be assigned the following value:

A,BBBBCCCDDDDDEFFFFFFFFFFFFF

That’s really nothing special; it’s just the first line of text, albeit with a comma inserted in character position 2. Now we’re going to loop around and repeat this process with the next item in the array arrCommas. Because that item happens to be the number 7, we’ll end up inserting a comma in character position 7, making our string equal to this:

A,BBBB,CCCDDDDDEFFFFFFFFFFFFF

And then we’ll loop around and repeat this process with all the remaining items in the array, slowly but surely inserting all the commas in their appointed palces.

After all the commas have been inserted we then execute this line of code:

strText = strText & strLine & vbCrLf

That line simply takes the variable strText and assigns it the existing value of strText plus the recently-revised line (strLine) plus a carriage return-linefeed. From there we loop around, read in the second line of text, and start inserting commas in that line, eventually appending that value to the variable strText.

Sooner or later, we’ll finish reading all the lines in the text file. At that point we close the file and then reopen it, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

Once the file is open we use the Write method to overwrite the existing contents of Test.txt with the value of strText, then call the Close method and close the file once and for all:

objFile.Write strText
objFile.Close

The net result? A text file that looks like this:

A,BBBB,CCC,DDDDD,,E,FFFFFFFFFFFFF
G,HHHH,III,JJJJJ,,K,LLLLLLLLLLLLL
M,NNNN,OOO,PPPPP,,Q,RRRRRRRRRRRRR

A little tricky, perhaps, but still a task we can accomplish with just a few lines of code.

Incidentally, to the best of his recollection the Father’s Day game marked the first time the Scripting Guy who writes this column has ever seen a basketball land in a toilet. (Somewhat surprising to those who’ve heard about the weird competitions the Scripting Dad and Scripting Son have engaged in over the years.) As unusual as that might have been, however, the Scripting Guy who writes this column wasn’t really fazed by seeing a basketball land in the toilet; after all, his Microsoft career landed there a long, long time ago.

Note. If you’ve always assumed that the Scripting Guys were at the top of the Microsoft org chart, well ….


How Can I Insert Text Into a “Bookmark” in a Text File?

Hey, Scripting Guy! I saw your article about inserting text into a Microsoft Word bookmark. How can I do the same sort of thing in a text file?

-- LR

Hey, LR. You’ll have to forgive the Scripting Guy who writes this column if he seems a little less enthusiastic today than usual. As it turns out, he happened to see a commercial on TV right before he left for work this morning, and he’s been a little down-in-the-dumps ever since.

Note. Yes, for reasons the Scripting Guy who writes this column doesn’t fully understand, the TV in the Scripting Household must be on at all times. One time the Scripting Guy who writes this column was home by himself, and didn’t bother to turn the TV on. When the rest of the family returned home and saw that the TV was off, they immediately called 9-1-1, figuring that the Scripting Guy who writes this column must be dead. After all, if he wasn’t dead then why wouldn’t he have turned the TV on?

Anyway, in this commercial there’s a guy in a brand-new car, a car perched on the road. High in the air is a helicopter toting a similar new car. On the signal, the guy on the ground hits the gas pedal and starts racing down the road; meanwhile, the helicopter cuts the other car loose. The whole thing turns out to be a race: can the car on the ground accelerate quickly enough to avoid being hit by the car falling from the sky.

And, yes, it turns out that he can accelerate quickly enough to avoid getting squashed. Whew!

So why did this commercial make the Scripting Guy who writes this column so sad? Well, it wasn’t so much the commercial as it was the disclaimer accompanying that commercial:

Professional driver on a closed course. Do not attempt this at home.

Do not attempt this at home?!? Oh, great; now what is the Scripting Guy who writes this column supposed to do with his helicopter and the two new cars he just bought? If, like this Scripting Guy, you had hoped to spend the weekend dropping cars from helicopters, well, you might want to think about changing those plans.

Of course, that said, we aren’t convinced that car companies have the right to tell us what we can and can’t drop from our own helicopters. We’ll look into this and let you know what we find out.

In the meantime, you are more than welcome to insert text into a text file “bookmark” at home, at work, or anywhere else you might desire:

Const ForReading = 1
Const ForWriting = 2

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strText = objFile.ReadAll
objFile.Close

strText = Replace(strText, "[BOOKMARK #1]", Date)
strText = Replace(strText, "[BOOKMARK #2]", strUser)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strText
objFile.Close

Before we discuss how the script works we should note that this script won’t work unless you have a text file that includes the specified “bookmarks” (which, in this case, simply refer to readily-identifiable chunks of text). For this example, we’re using a simple little text file (C:\Scripts\Test.txt) that looks like this:

This text file was updated on [BOOKMARK #1] by [BOOKMARK #2].

Our script is going to replace the text [BOOKMARK #1] with the current date, then replace the text [BOOKMARK #2] with the name of the logged-on user. Note that the text we used for the bookmarks is arbitrary; the only caveat here is that you need to make sure that the text you use is separate and distinct from any other text found in the file. For example, if the word computer appears multiple times in your file then using computer as a bookmark name would be a bad idea. Why? Because the script will replace every instance of the word computer – not just the bookmark text – with the new text. That’s why a unique construction such as [COMPUTER NAME] is a better way to go.

As for the script itself, we start out by defining a pair of constants – ForReading and ForWriting – that we’ll use when working with the file. We then use these two lines of code to create an instance of the Wscript.Network object, get the logon name of the logged-on user, and then store that value in a variable named strUser:

Set objNetwork = CreateObject("Wscript.Network")
strUser = objNetwork.UserName

Once that’s done we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading (note the constant ForReading passed as a method parameter):

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

With the text file open we use the ReadAll method to read the entire contents of the file into a variable named strText; we then immediately close the file:

strText = objFile.ReadAll
objFile.Close

So why do we close the file, especially since we want to replace the bookmarks in that file? Well, that’s due to the eccentricities of the FileSystemObject: this object lets you read from a file or write to a file, but it doesn’t let you do both operations at the same time. Having read from the file, we have to close and reopen it before we can write to it.

However, before we do that we need to modify the value of strText. That’s what these two lines of code are for:

strText = Replace(strText, "[BOOKMARK #1]", Date)
strText = Replace(strText, "[BOOKMARK #2]", strUser)

Good question: what are we doing with these two lines of code? Well, in the first line we’re using the Replace function to search through strText and look for the value [BOOKMARK #1]. If we find that value, we’re going to replace {BOOKMARK #1] with the current date (hence the VBScript Date function passed as parameter 3). In line 2, we’re going to look for the text [BOOKMARK #2] and, if found, replace it with the name of the logged-on user. In both cases, the resulting value will be stored in the variable strText.

What that means, of course, is that strText will then contain a “virtual text file” in which the bookmark designators have been replaced by the specified values. Because of that we can then replace the real text file (C:\Scripts\Test.txt) with this modified version. That’s what this block of code is for:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strText
objFile.Close

When that’s done C:\Scripts\Test.txt will look like this:

This text file was updated on 2/15/2007 by kenmyer.

And there you have it.

As a public service, we should also note that there is another car commercial on TV these days, one which shows a car driving up and down a bunch of buildings. Before you get too excited, however, keep in mind that this commercial also includes a disclaimer, one that notes “This is a fantasy. Car cannot really drive on buildings.” Are they serious: a brand-new car and you can’t even drive on buildings with it? In that case, you can go ahead and remove the Scripting Guy who writes this column from the list of potential buyers. After all, he already has a car that can’t drive on buildings; why would he want another one?

Well, unless they’ll let him drop that car from a helicopter. Hmmm ….

Note. What, no mention of the 2007 Winter Scripting Games and how there’s still times to enter at least one event and win a Dr. Scripto Bobblehead doll or a copy of the book Windows PowerShell in Action? No, sorry, we aren’t even going to mention the Scripting Games today. After all, we don’t want people to think we’re pushy or anything.


How Can I Insert Text Into a Specific Column of a Text File?

Hey, Scripting Guy! How can I insert text into a specific column of a text file? I don’t want the text to be inserted at the beginning of a line or at the end of a line, but at a specific spot in the line.

-- D

Hey, D. You know, over the past two years we’ve answered a lot of questions about text files, and if there’s one thing we’ve learned in that time it’s this: we should never answer any questions about text files. Yes, we know that a lot of people are interested in text files, and we know that the scripts to read and write these files are generally short and easy to create. Unfortunately, though, those scripts are usually anything but elegant. Instead of doing something cool, like calling the InsertTextInThisSpot method, we always have to resort to crazy little workarounds. Sure, we end up with a script that works, but, like we said, that script is never very elegant. And we Scripting Guys want to be elegant!

Really? So you’re saying that the baseball hat and the Seattle Mariners 2005 Spring Training shirt we wear each day isn’t elegant? Interesting; that never occurred to us. And the old black sweatpants we wear on weekends? Well, what do you know?

OK, D, maybe we’re not quite as elegant as we thought we were. With that mind, we might as well go ahead and show you a script that can insert text into a specific spot in a line:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    intRight = Len(strLine) - 10
    strRight = Right(strLine, intRight)
    strLeft = Left(strLine, 10)
    strInsert = "This is inserted text"
    strText = strLeft & strInsert & strRight
    strContents = strContents & strText & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strContents

objFile.Close

Let’s start out by giving you a quick overview of what this script does. For the sake of argument, let’s assume your text file looks like this:

000000000  111111111
222222222  333333333
444444444  555555555
666666666  777777777
888888888  999999999

In other words, we have nine characters, two blank spaces, and then nine more characters. (Granted, that’s an odd-looking text file, but setting it up this way will make it easier for us determine whether or not the script worked.) What we want to do is insert some text in between the two blank spaces. Because the first blank space is character 10, we want the inserted text to start at character (or column) 11. If everything goes well that will give us a line that looks like this:

000000000 This is inserted text 111111111

Can we do that? We’re about to find out.

Our script starts out by defining a pair of constants, ForReading and ForWriting; we’ll use these constants to tell the script which mode (reading or writing) we want to use when opening the text file. This, by the way, is one of those less-than-elegant things we were talking about. For better or worse, we can’t read and write to a text file at the same time; instead we’re limited to reading from or writing to it. Therefore, we actually need to open this text file twice: once to read the existing contents and once to write the revised contents back to the file.

After defining our constants we use these two lines of code to create an instance of the Scripting.FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Seeing as how we opened the file for reading we figured we might as well start by reading in the contents. To do that we set up a Do loop that runs until we’ve reached the end of the file (that is, until the AtEndOfStream property is True). Inside that loop we use the ReadLine method to read the first line from the file and store that value in a variable named strLine:

strLine = objFile.ReadLine

What that means, of course, is that strLine is currently equal to this:

000000000  111111111

Cute, but not exactly what we want for the revised text file. But how are we going to insert our text into the middle of that line?

Here’s how. We start out by using this line of code:

intRight = Len(strLine) - 10

Why? Well, we need to construct a new line for the text file. That new line will consist of the first 10 characters in the existing line, the new text we want to insert, and then the remaining characters from the existing line. To get at these remaining characters we need to start at the end of the string and count backwards until we get to character 10. That’s fine, except for one thing: how far back are we supposed to count?

That’s what Len(strLine) – 10 tells us. The Len function tells us the total number of characters in the variable strLine; in this case that’s equal to 20. We then subtract 10 from that amount. Why? Because we don’t want to include the first 10 characters. Seeing as how 20 minus 10 equals 10, we now know that if we start at the end of the string and count backwards 10 characters we’ll end up exactly where we need to end up.

That’s exactly what we do here:

strRight = Right(strLine, intRight)

In this line of code we use the Right function to take the last 10 characters (10 being the value stored in the variabled intRight) from the end of the string and store them in the variable strRight. In this sample script, that means strRight will equal this:

111111111

We then use the Left function to take the first 10 characters in strLine and store them in the variable strLeft. That means, for the first line in the text file, strLeft will equal this:

000000000

Note that there’s a blank space at the end of strLeft and a blank space at the beginning of strRight.

And now, at long last, we can construct our new line. To do that, we store the text to be inserted into a variable named strInsert, then simply combine the values of strLeft, strInsert, and strRight (plus a carriage return-linefeed tacked on to the end). That’s what we do here:

strInsert = "This is inserted text"
strText = strLeft & strInsert & strRight

After all that, guess what the value of strText is? Good guess:

000000000 This is inserted text 111111111

Yes, we know: that’s a lot of work just to insert some text into a single line in a text file. Sadly, we’re not quite finished, though. Because we can’t write this line directly back to the text file (remember, it’s currently open for reading only) we need to construct a new text file in memory, and then, later on, write the complete contents of that virtual text file to C:\Scripts\Test.txt. Therefore, we use this line of code to add the value of our revised text line to a variable named strContents:

strContents = strContents & strText & vbCrLf

As we loop through the text file, we’ll continue appending values (lines of text) to strContents. When we’re through, strContents will be a virtual representation of what we want Test.txt to look like.

So what does happen when we’re through with the text file? Well, first we close it, then we immediately re-open it, this time for writing:

objFile.Close
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

Once the file is open we call the WriteLine method to write the value of strContents to the text file, and then close C:\Scripts\Test.txt for good. And what does that text file look like now? We’re glad you asked:

000000000 This is inserted text 111111111
222222222 This is inserted text 333333333
444444444 This is inserted text 555555555
666666666 This is inserted text 777777777
888888888 This is inserted text 999999999

Now that’s elegant.

And so’s the baseball hat, right?

Right?


How Can I Make Sure That No Line in a Text File Exceeds 261 Characters?

Hey, Scripting Guy! How can I make sure that no row in a text file exceeds 261 characters?

-- RK

Hey, RK. You know, this was a good day to ask a question; for once the Scripting Guy who writes this column is in an OK mood. (No, he’s never in a good mood, at least not when he’s at work.) Yesterday the Scripting Son absolutely had to have a sub sandwich for dinner. With that in mind the Scripting Dad dutifully stopped to pick up sandwiches on his way home, and had the opportunity to witness greatness in action: the sandwiches he ordered were made – no, were crafted – by the finest sandwich maker in the history of the world.

Typically when you order sandwiches from a fast food place you walk out thinking one of two things: 1) either the sandwich makers must have to pay for each ingredient themselves (why else would they put just two measly pickle slices on a foot-long sandwich?), or 2) these fast food employees are under doctor orders to never lift more than one green pepper strip at a time. Needless to say, the Scripting Dad usually asks the sandwich makers if they could tie a string around each sandwich: they have so little substance he’s afraid that, like a balloon, they might float away if they don’t include a string he can tie around his wrist.

Note to the Sandwich Makers of the world. If a patron asks for mustard it’s a pretty safe bet that they’d like to see mustard spread along the entire sandwich. One tiny squirt of mustard at the end of one piece of bread probably isn’t what they had in mind.

Yesterday was different, though. “You want olives?” said the sandwich maker as he proceeded to grab an entire handful of olives and scatter them across the bread. “Tomatoes? No problem,” he said, laying wall-to-wall tomatoes across the depth and breadth of the sandwich. “Vinegar?” he asked, and then promptly sprayed vinegar all over his creation (unlike some of his peers, who squirt a little bit of air out of the bottle and assume, for some reason, that that was what you wanted). To be honest, it was all so beautiful that the Scripting Guy who writes this column had to grab his food and hurry out of the restaurant; he was too embarrassed to have the sandwich maker see him cry.

Not that he should have been embarrassed, mind you. After all, few things in life are as beautiful and as stirring as a well-made sandwich.

As you might expect, this sandwich maker immediately became an inspiration to the Scripting Guy who writes this column. “No more shortcuts,” vowed the Scripting Guy. “No more slapping together scripts and calling them good, no more writing about baseball or basketball or sandwiches and passing that off as a scripting column. From this moment on I solemnly vow to make every script I create and every column I write a true work of art.”

Of course, that vow only lasted until he had to sit down to create his next script and write his next column; at that point he reverted back to his usual lazy self. But at least he did manage to come up with the following script:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strLine = Left(strLine, 9)
    strContents = strContents & strLine & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)
objFile.Write strContents

objFile.Close

A couple of quick notes before we begin. For one, RK, we got the impression from your email that what you wanted to do was discard any excess characters in a line. For example, suppose a line of code has 300 characters; as near as we could tell, in a case like that you want to grab just the first 261 characters and then get rid of characters 262 through 300. Consequently, that’s exactly what this sample script does. If that’s not what you had in mind, just let us know.

Note. Well, no, we won’t readdress the question. We’re just assuming that the sandwich maker will do it for us.

Second, it’s difficult for us to display lines with 261 characters on our Web pages; in turn, that makes it difficult for us to show you before and after pictures of the text file. Therefore, we cheated a little bit and created a script that ensures that no line in the text file has more than 9 characters. But that shouldn’t be a problem; to change that just look for the number 9 in the script and replace it with any number you want, including 261.

Now, let’s get down to business. Let’s assume that you have a text file that looks like this, a text file with lines of varying lengths:

123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
123456789ABCDEFGH
12345
12345678
123456789ABC
123456789ABCDEFGHIJKLMNOPQRS

What we need to do is ensure that none of these lines have more than 9 characters (they can have fewer than 9 characters; just not more). Here’s how we do that.

To begin with, we define a pair of constants – ForReading and ForWriting – that we’ll use when opening the text file. (As usual, we’ll need to open the file on two separate occasions: once to read what’s currently there, and once to replace the existing contents with the modified contents.) We create an instance of the Scripting.FileSystemObject, then use this line of code to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

So far, so good.

Our next chore is to examine the file line-by-line, truncating any lines that have more than 9 characters. To do that we set up a Do Until loop that runs until we reach the end of the file (that is, until the AtEndOfStream property is True). Here’s what we do inside that loop:

strLine = objFile.ReadLine
strLine = Left(strLine, 9)
strContents = strContents
& strLine & vbCrLf

To begin with, we use the ReadLine method to read the first line in the file and store that value in a variable named strLine. We then use the following line of code (and VBScript’s Left function) to grab just the first 9 characters in the string and assign those characters to the variable strLine:

strLine = Left(strLine, 9)

We know what you’re thinking: what if we only have, say, 5 characters in the line? No problem; in that case VBScript will simply use whatever characters there are and leave it at that. No error will occur if you ask for the first 9 characters and the line only has 5 characters.

At this point it’s safe to assume that the first line in the text file has, at most, 9 characters. In an ideal world, we would now go ahead and replace line 1 in the text file with our modified line. Unfortunately, albeit with the possible exception of one local sub sandwich place, this is not an ideal world. As it turns out, the FileSystemObject doesn’t allow us to replace individual lines in a text file. Because of that we need to construct a new text file in memory and then, when the time comes, replace the entire contents of Test.txt with this new text file.

All of which explains the purpose of this line of code:

strContents = strContents & strLine & vbCrLf

What we’re doing here is assigning our first modified line to a variable named strContents. (Technically, strContents gets assigned the existing value of strContents plus the modified line plus a carriage return-linefeed.) Each time we run through the loop we’ll modify a different line in the text file and then append that line to strContents. When we’re done, strContents will contain the modified contents of our text file, with no individual line having more than 9 characters.

Note. You’re right: in some cases we won’t actually have to modify a line; because some lines have 9 or fewer characters they’re fine just the way they are. In those cases, then, we’ll just append the existing line to strContents. But it’s the same basic idea.

After we’ve read through the entire text file we next use the Close method to close Test.txt. We have to do that because the file is currently open for reading; before we can write to the file we have to reopen it for writing. Which is exactly what we do here:

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)

With the file reopened, this time for writing, we can now use the Write method to replace the existing contents with the value of the variable strContents. After that all we have to do is close the file and we’re done:

objFile.Write strContents

objFile.Close

Assuming that everything went off without a hitch Test.txt should now look like this:

123456789
123456789
12345
12345678
123456789
123456789

Which is pretty much how we wanted it to look.

So there you have it, RK. It might not be a work of art (or maybe it is, if you’ve seen what passes for art these days) but it should do what you need it to. Besides, you know what they say: some people are really good at their jobs; the rest become Scripting Guys.


How Can I Modify a Text File and Then Extract Selected Lines From the Modified Contents?

Hey, Scripting Guy! How can I write a script that modifies the contents of a text file and then copies specified lines from the modified contents to a new file?

-- EH

Hey, EH. There’s no way to do that. Now go away and leave us alone.

Wait, sorry; we didn’t mean it. As it turns out, the Scripting Guy who writes this column is in a terrible mood this morning. Admittedly, he’s in a terrible mood pretty much every morning, but at least today he has good reason to be grumpy. He’s writing this column on March 1, and when he awoke this morning his house and everything around it was covered with a good two inches of snow. Snow, mind you, and on March 1st! The Scripting Son has already started high school baseball practice and there are two inches of snow on the ground! The Scripting Guy who writes this column fully intends to sue someone over this. He just has to figure out who he can sue over this.

And yes, we know: people in, say, New York, who recently dealt with several feet of snow, don’t feel too sorry for Seattleites who have to deal with a few piddling inches of snow, snow that’s already beginning to melt even as we speak. But that’s OK. After all, we don’t need anyone’s pity; we have more than enough self-pity to go around. In the past few months we’ve had ice storms; power failures (leaving the Scripting Guy who writes this column without electricity for 6 long, cold, and very dark days); more ice storms; and now snow on March 1st. What did we ever do to deserve all that?

OK, we can’t argue with you there. But surely the entire Seattle area shouldn’t have to pay for the sins of one Scripting Guy.

Oh, well. At any rate, we apologize for starting this column out on such a rude note. Tell you what, EH: is there some way we can make this up to you? What’s that? Show you how to write a script that modifies the contents of a text file and then copies specified lines from the modified contents to a new file? Hmmm; we never would of have thought of that. OK, EH; here you go:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

strContents = Replace(strContents, "~", "~" & vbCrLf)

arrLines =Split(strContents, vbCrLf)

For Each strLine in arrLines
    If Left(strLine, 2) = "N9" or Left(strLine, 2) = "B4" Then
        strNewFile = strNewFile & strLine & vbCrLf
    End If
Next

Set objFile = objFSO.CreateTextFile("C:\Scripts\Test2.txt")
objFile.Write strNewFile
objFile.Close

And now can we explain how the script works? How in the world are we supposed to know how the script works?!? Now go away and leave us alone.

Sorry; guess we aren’t quite back to our usual jolly and carefree selves here. Sure, we can explain how this script works. As you can see, we start off by creating a constant named ForReading and setting the value to 1; we’ll use this constant when we open our text file. In fact, we’ll use this constant in the following block of code, code that creates an instance of the Scripting.FileSystemObject and then uses the OpenTextFile method to open the file C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As you noted in your email, EH, you have a crazy text file that looks something like this, with everything on one big long line:

*070219*1347*U*00401*000000852*0*P*>~GS*QO*CMACGM*USALTLANP*20070219*1347*85
2*X*004010~ST*315*0001~B4***CT*20070219*0840*1703*CAXU*983290*L*4510~N9*BM*SZ2
711711~N9*BN*SHZ438713~N9*SN*C4025491~Q2*9227027*****166***PX311E***L*CSCL
DALIAN~R4*L*K*57078*YANTIAN~DTM*140*20070119*1200~R4*D*D*1703*SAVANNAH, GA~

Note. It’s probably none of our business, but are you a spy or something, EH? You’re right; maybe it’s best if we don’t know that.

You’d like to read in this text file and do two things. First, you’d like to put a carriage return-linefeed after each tilde character (~); that will give you lines that look like this:

*070219*1347*U*00401*000000852*0*P*>~
GS*QO*CMACGM*USALTLANP*20070219*1347*852*X*004010~
ST*315*0001~
B4***CT*20070219*0840*1703*CAXU*983290*L*4510~
N9*BM*SZ2711711~
N9*BN*SHZ438713~
N9*SN*C4025491~
Q2*9227027*****166***PX311E***L*CSCL DALIAN~
R4*L*K*57078*YANTIAN~
DTM*140*20070119*1200~
R4*D*D*1703*SAVANNAH, GA~

After that you’d then like to identify any lines that begin with N9 or B4 and copy those lines to a separate text file. Sounds crazy, doesn’t it? But if it’s crazy you want, well, you came to the right place.

To begin with, we use the ReadAll method to read the entire file into memory and store the contents in a variable named strContents:

strContents = objFile.ReadAll

We then immediately close the file C:\Scripts\Test.txt. Why? Because we don’t need it anymore; we’ll do all our work with the variable strContents instead.

For starters, let’s put a carriage return-linefeed after each tilde in the file. How are we going to do that? Like this:

strContents = Replace(strContents, "~", "~" & vbCrLf)

As you can see, we simply use the Replace function to replace each tilde character with two characters: a tilde and a carriage return-linefeed (as indicated by the VBScript constant vbCrLf). The net effect: the tildes remain exactly where they are, only there’s now a carriage return-linefeed after each one.

Of course, something else has happened here as well: our text file (or at least the virtual copy of that file) now has multiple lines thanks to the carriage return-linefeed. And because it does have multiple lines we can go ahead and extract all the lines that begin with N9 or B4.

In order to do that we first use the Split function to split the variable strContents on the carriage return-linefeed character:

arrLines =Split(strContents, vbCrLf)

That’s going to result in an array (arrLines) in which each element represents a line in the text file. In turn, that means we can now “read” each line in the file simply by setting up a For Each loop that walks through all the items in the array:

For Each strLine in arrLines

So what are we going to do inside that loop? Well, for each array item we’re going to check to see if the first two characters are either N9 or B4; that’s what this line of code, and the Left function, are for:

If Left(strLine, 2) = "N9" or Left(strLine, 2) = "B4" Then

Let’s suppose we find one of the target lines. In that case, we simply take that value and append it (plus a carriage return-linefeed character) to a variable named strNewFile:

strNewFile = strNewFile & strLine & vbCrLf

And then we loop around and repeat this process with the next item in the array.

Once we’ve finished looping through the array we then execute this block of code:

Set objFile = objFSO.CreateTextFile("C:\Scripts\Test2.txt")
objFile.Write strNewFile
objFile.Close

All we’re doing here is creating a new file named C:\Scripts\Test2.txt; that’s what we do with the CreateTextFile method. We use the Write method to write the value of strNewFile to Test2.txt, then close the file and call it good. If we then open Test2.txt we should see this:

B4***CT*20070219*0840*1703*CAXU*983290*L*4510~
N9*BM*SZ2711711~
N9*BN*SHZ438713~
N9*SN*C4025491~

Which, crazy as it might seem, is exactly what we wanted to see.

Now, if you’ll excuse us, we’re going to go back to being grumpy and feeling sorry for ourselves. Admittedly, right now the sun is peeking through the clouds, and the snow is rapidly melting. But we’re not going to fall for that old trick; the sun coming out simply means that we’ll have a nice clear view of the next disaster to strike the Seattle area. We’re betting on frogs or locusts; it’s been awhile since we’ve had a plague of frogs or locusts around here. We’ll let you know.


How Can I Parse a Tab-Delimited File and Then Save That as a Comma-Separated Values File?

Hey, Scripting Guy! How can I parse a tab-delimited file, and then save the resulting information as a comma-separated values file?

-- DM

Hey, DM. You know, yesterday morning the Scripting Guy who writes this column had to scrape the ice off his windshield; today it’s much warmer – 44 at last report – although it is raining. Of course, you know what that means: the Seattle-area baseball season is in full swing!

We mention that simply because it provides a sneaky way for us to also mention that the Script Son took the mound for his high school team yesterday afternoon. The results? Five shutout innings, one hit (on a checked-swing), no walks, eight strikeouts. Not bad for his first outing of the season, although it’s a shame that he gave up that one hit; after all, that’s one more hit than the Scripting Dad gave up during his entire high school career.

Note. What’s that? How many times did the Scripting Dad actually pitch during his high school career? Well, that’s hard to say; after all, that was a long time ago, and pitching records from that era are spotty at best. Off the top of our heads, we’d have to say the number of times he pitched was definitely greater than – or, at least, equal to – zero.

But, hey, this is supposed to be about the Scripting Son. Let’s let him have his moment and not concern ourselves with the Scripting Dad.

At any rate, it’s always a treat to watch your son mow down the opposition. He blew his fast ball right by several batters, and completely froze several more with his curve.

Although, come to think of it, maybe the curveball didn’t freeze the hitters as much as the weather did. The Scripting Dad, for example, was wearing long underwear, a long-sleeved shirt, a hooded sweatshirt, a coat, and a pair of gloves. If that doesn’t scream “baseball season” we don’t know what does.

Of course, in years past the start of baseball season meant that the Scripting Guy who writes this column disappears and isn’t heard from again until August; that’s because he finds baseball far more interesting than work. (Hard to believe, but he does.) Right now, however, he has to admit that he finds work far warmer than he finds baseball. Which means that, rather than disappearing, he’s going to sit in his nice warm office and see if he can figure out how to parse a tab-delimited file, and then save the resulting information as a comma-separated values (CSV) file.

At least until it’s time to leave work early and head for the next game.

To begin with, DM has a text file that looks something like this, with fields separated by tabs:

Cre Rec    Name=Jack    Address=5 XYZ Drive    Phone=555-4567
Cre Rec    Name=Jill    Address=7 XYZ Drive    Phone=555-6547
Cre Rec    Name=Jake    Address=9 XYZ Drive    Phone=555-9876

And sure, that is a very nice text file. But it’s nowhere near as nice as this CSV file, which is the file DM would like to have:

Name,Address,Phone
"Jack","5 XYZ Drive","555-4567"
"Jill","7 XYZ Drive","555-6547"
"Jake","9 XYZ Drive","555-9876"

The question for today is this: how can DM get from point A (the tab-delimited file) to point B (the CSV file)? Here’s how:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForReading)

Do Until objFile.AtEndOfStream
strLine = objFile.ReadLine
arrFields = Split(strLine, vbTab)
arrName = Split(arrFields(1), "=")
strName = arrName(1)
arrAddress = Split(arrFields(2), "=")
strAddress = arrAddress(1)
arrPhone = Split(arrFields(3), "=")
strPhone = arrPhone(1)
strNewContent = strNewContent & Chr(34) & strName
& Chr(34) &  "," & Chr(34) & strAddress
& Chr(34) & _
"," & Chr(34) & strPhone & Chr(34) & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.CreateTextFile("C:\Scripts\Test.csv")

objFile.WriteLine "Name,Address,Phone"
objFile.Write strNewContent

objFile.Close

As you can see, we start out by defining a constant named ForReading and setting the value to 1; we’ll use this constant when we set out to open our tab-delimited file. We then use these two lines of code to create an instance of the Scripting.FileSystemObject and to open the file C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

So now what do we do?

Oh, right: that’s our job to figure that out, isn’t it? OK, well, here’s one thing we can do: we can set up a Do Until loop that reads the tab-delimited file line-by-line. In other words, a Do Until loop that runs until the file’s AtEndOfStream property is True:

Do Until objFile.AtEndOfStream

Inside that loop we use this line of code to read the first line in the file and store it in a variable named strLine:

strLine = objFile.ReadLine

What does that mean? That means that strLine is equal to this:

Cre RecName=JackAddress=5 XYZ DrivePhone=555-4567

Of course, that doesn’t do us much good; somehow we need to tease out the individual values for Name, Address, and Phone. Is that too much to ask for? Of course it is. But let’s give it a try anyway.

To begin with, we use the Split function within the following line of code to split the value of strLine into an array:

arrFields = Split(strLine, vbTab)

By splitting the value on the tab character (using the VBScript constant vbTab) we end up with an array named arrFields that consists of the following items:

· Cre Rec

· Name=Jack

· Address=5 XYZ Drive

· Phone=555-4567

OK, now we’re getting somewhere. As you can see, the user’s name (Jack) is part of the second item in the array. (And, remember, because the first item in an array has an index number 0 that means the second item in the array has an index number of 1). The only problem, of course, is that the name is prefaced with Name=. So why don’t we just get rid of that prefix:

arrName = Split(arrFields(1), "=")
strName = arrName(1)

See what we’re doing here? In the first line, we’re again using the Split function to create an array; this time we’re splitting the value of array item 1 (Name=Jack) on the equals sign (=). What does that give us? That gives us a little two-item array named arrName, an array that looks like this:

· Name

· Jack

Well, what do you know: the second item in our mini-array (index number 1) just happens to be the user name. That means we can grab the name simply by assigning the value of array item 1 to the variable strName, which is what we do in our second line of code.

Get the idea? We then repeat the process with index numbers 2 (address) and 3 (phone number), like so:

arrAddress = Split(arrFields(2), "=")
strAddress = arrAddress(1)
arrPhone = Split(arrFields(3), "=")
strPhone = arrPhone(1)

We now have three variables – strName, strAddress, and strPhone – that contain information parsed from the text file. That means that our next step is to begin constructing the CSV file. That’s what this monstrosity is for:

strNewContent = strNewContent
& Chr(34) & strName
& Chr(34) &  "," & Chr(34) & strAddress
& Chr(34) & _
"," & Chr(34) & strPhone
& Chr(34) & vbCrLf

Don’t let this line of code deter you: its bark is far worse than its bite. All we’re doing here is assigning a value to a variable named strNewContent. What value are we assigning to strNewContent? Well, we’re assigning the existing value of the variable plus the following:

· Double quotes (that’s what the Chr(34) is for).

· The value of the variable strName.

· More double quotes.

· A comma (“,”).

· Double quotes.

· The value of the variable strAddress.

· Double quotes.

· Another comma.

· Double quotes.

· The value of the variable strPhone.

· Double quotes.

· A carriage return-linefeed character (vbCrLf).

Why such a complicated construction? What we’re trying to do is guard against any items that might include a comma. For example, suppose a user has the name Ken Myer, Jr. If we simply write that name as-is to a CSV file, the CSV file will assume that we have two values: Ken Myer and Jr. That’s because the comma is used as the delimiter. In turn, a simple thing like that can cause our CSV file (and any scripts/applications that need to read that file) to go completely haywire. To prevent that from happening we enclose each value in double quotes, like so:

"Ken Myer, Jr."

In a CSV file, items enclosed in double quotes are treated as a single value, even if the value includes commas.

See? We’re doing this for your own good!

After that we then loop around and repeat the process with the next line in the tab-delimited file.

Once we’ve read and processed the entire file we’re ready to create the CVS file. To do that we first close the file Test.txt, then use this line of code to create a new text file named C:\Scripts\Test.csv:

Set objFile = objFSO.CreateTextFile("C:\Scripts\Test.csv")

Because DM needed a header line the first thing we do to our new file is use this line of code to write a header to the thing:

objFile.WriteLine "Name,Address,Phone"

And then all we have to do is use the Write method to write the value of strNewContent to the file. (And then use the Close method to close Test.csv.) At that point we – just like the hitters that had to go up against the Scripting Son – are finished.

Hope that helps. DM. It’s a little complicated, but not if you take a deep breath and work your way through it. And we know that a lot of people are faced with similar tasks. We hope this helps all of you as well.

Meanwhile, we – what’s that? Oh, right: you noticed that small patch of blue sky off in the distance. If the weather’s improving, does that mean that baseball season is already over?

No, far from it: if the weather is improving that simply means that the Scripting Son doesn’t have a game tonight. But don’t worry: the forecast for tomorrow’s game includes a temperature of 49 degrees and rain. Now that’s baseball weather.


How Can I Pick Out and Save Specific Lines in a Text File?

Hey, Scripting Guy! I’d like to be able to read through a text file, select the lines that begin with a particular word (like Failure), and then save only those lines back to the same text file. Is there any way to do that?

-- AC

Hey, AC. Hey, AC. For simplicity’s sake, we’re assuming you have a text file that looks similar to this:

Success – Operation succeeded 10/1/2004.
Success – Operation succeeded 10/2/2004.
Failure – Operation failed 10/3/2004.
Success – Operation succeeded 10/4/2004.
Failure – Operation failed 10/5/2004.
Success – Operation succeeded 10/6/2004.
Failure – Operation failed 10/7/2004.
Failure – Operation failed 10/8/2004.

You’d like to have a script read through the file, toss out all the lines that begin with Success, and then save the file, a file which will then hold only information about the operations that failed. In other words, you want the revised file to look like this:

Failure – Operation failed 10/3/2004.
Failure – Operation failed 10/5/2004.
Failure – Operation failed 10/7/2004.
Failure – Operation failed 10/8/2004.

Can you do this with a script? Of course you can:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("test.log", ForReading)

Do Until objTextFile.AtEndOfStream
    strLine = objTextFile.ReadLine
    If Left(strLine, 7) = "Failure" Then
        strNewText = strNewText & strLine & vbCrLf
    End If
Loop

objTextFile.Close
Set objTextFile = objFSO.OpenTextFile _
    ("test.log", ForWriting)
objTextFile.Write(strNewText)
objTextFile.Close

This script looks a tad bit complicated, but that’s because there’s no way to directly edit a text file using a script. Instead, we have to open the text file, read in the current contents, and then close the file. We then do our “editing” in memory, re-open the text file, replace the current contents with our new data, and then close the file again. And that’s exactly what this script does.

After defining a couple of constants (ForReading and ForWriting, constants we’ll need to open the text file), the script opens the file test.log for reading. We then create a Do loop that runs until we reach the end of the text file; in other words, until we are at the end of the text stream.

And what happens inside that loop? We begin by using the ReadLine method to read the current line of the text file; that line of text gets stored in the variable strLine. We then check to see if the first 7 characters of the line happen to be Failure; that’s what the command If Left(strLine, 7) = “Failure” does. If the first seven characters are anything but Failure, then we simply loop back around and read the next line in the file.

But what if the first seven characters are Failure? In that case, we have another variable – strNewText – that we use to store the data we want to save. The line of code strNewText = strNewText & strLine & vbCrLf just takes whatever happens to be in strNewText at the moment, append the value of strLine, and then add a carriage return-linefeed to the end (vbCrLf). This builds up a new dataset in memory; by the time we finish reading the entire file, strNewText is equal to this:

Failure – Operation failed 10/3/2004.
Failure – Operation failed 10/5/2004.
Failure – Operation failed 10/7/2004.
Failure – Operation failed 10/8/2004.

In other words, all we’ve done is read the file and keep a list of all the lines beginning with the word Failure; any lines beginning with anything else have been ignored.

Now that we have our new dataset, we need to close the text file and then reopen it, this time using the constant ForWriting. (Yes, we know, but that’s the way the FileSystemObject works: you have to open it for reading, then close it and reopen it for writing.) With the file open we use the Write method to replace the existing contents of test.log with the value of our variable strNewText. We then close the file, which saves the change we just made. The net effect? Test.log now contains only a list of operations that failed:

Failure – Operation failed 10/3/2004.
Failure – Operation failed 10/5/2004.
Failure – Operation failed 10/7/2004.
Failure – Operation failed 10/8/2004.

A tiny bit cumbersome, but it works just fine.

Of course, often times the word you are looking for isn’t found at the beginning of the line, but is instead found somewhere in the middle of the line. For example, your log file might look like this:

10/1/2004     Success – Operation succeeded.
10/2/2004     Success – Operation succeeded.
10/3/2004     Failure – Operation failed.
10/4/2004     Success – Operation succeeded.
10/5/2004     Failure – Operation failed.
10/6/2004     Success – Operation succeeded.
10/7/2004     Failure – Operation failed.
10/8/2004     Failure – Operation failed.

If that’s the case, then checking to see the value of the first 7 characters in each line won’t do you much good; you need to check to see if the word Failure appears anywhere within the line. But that’s all right; you can just use the VBScript function InStr. With InStr you pass it two parameters: the string to search (the variable strLine) and the item to search for the word Failure). InStr will respond by telling you the character position at which the word Failure begins. For example, in this line the word Failure begins at character 15, so InStr returns a 15:

10/5/2004     Failure – Operation failed.

If the search term can’t be found in the string, then IntStr returns a 0. Therefore, we just use InStr to search each line for the word Failure. If InStr is greater than 0, that means the word was found, and we add the line to the variable strNewText. Here’s a revised Do loop that searches for the value Failure anywhere within the line:

Do Until objTextFile.AtEndOfStream
    strLine = objTextFile.ReadLine
    intFailure = InStr(strLine, "Failure")
    If intFailure > 0 Then
        strNewText = strNewText & strLine & vbCrLf
    End If
Loop

Could we have used this approach earlier? Sure. But when we know sure that the term we’re looking for will be found at the beginning of the line, we think Left is a better choice (and the Right function is best if we know for sure that the term in question appears at the end of the line). That’s because it’s at least theoretically possible to “fool” the InStr function. For example, InStr will peg this line as marking a failure, even though it actually represents a successful operation:

Success – Failure troubleshooter successfully loaded 10/1/2004.

A somewhat unlikely occurrence, sure. But why take any chances?

P.S. We know we’ll get a bunch of letters from people saying, “What about regular expressions?” Yes, regular expressions are very powerful, but they’re also a bit too complex to explain in this column. We’ll take up regular expressions sometime soon, but somewhere other than the Hey, Scripting Guy! column.


How Can I Reformat Numbers Using a Script?

Hey, Scripting Guy! I have a text file containing latitudes and longitudes. The numbers look like this: 36000000. I need to format these numbers so they look like this: 36.000000. How can I reformat numbers using a script?

-- BE

Hey, BE. You know, if you have a question about numbers then you came to the right place. After all, one of the Scripting Guys is the proud owner of a degree in advanced mathematics. And the other Scripting Guys … well, never mind about the other Scripting Guys. Did we mention that one of the Scripting Guys is the proud owner of a degree in advanced mathematics?

Fortunately, you don’t need a degree in advanced mathematics to solve this problem. (And fortunately we don’t need one either, or this might be a really short column today.) As you noted, BE, you have a couple of text files containing latitude and longitude entries similar to this:

36000000
-076000000
92000000
105000000

What you want to do is open the text file, reformat the numbers, and then save the reformatted text file. When all is said and done you’d like your text file to look like this:

36.000000
-76.000000
92.000000
105.000000

Can we accomplish this feat using a script? Piece of cake:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Do Until objFile.AtEndOfStream
    intLine = objFile.Readline
    intLine = intLine/ 1000000
    intLine = FormatNumber(intLine, 6)
    strText = strText & intLine & vbCrLf
Loop

objFile.Close
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)

objFile.Write strText
objFile.Close

And don’t worry: we’ll explain how this all works. After all, that’s what we’re here for, right?

As you can see, the script starts out simple enough. We begin by defining a pair of constants – ForReading and ForWriting – which we’ll use when working with the text file itself. We create an instance of the FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading. (As you might well know, you can open a text file for reading or you can open a text file for writing, but you can’t perform both operations at the same time.)

That brings us to this block of code:

Do Until objFile.AtEndOfStream
    intLine = objFile.Readline
    intLine = intLine/ 1000000
    intLine = FormatNumber(intLine, 6)
    strText = strText & intLine & vbCrLf
Loop

This is where all the excitement takes place. To begin with, we set up a Do Until loop to read through the file line-by-line, then continue reading until we’ve read every last line in the file (that is, when the AtEndOfStream property is True). Inside that loop we use the ReadLine method to read the first line of the file and store that value (in our sample file, 36000000) in a variable named intLine.

The next step is easy. We want to take the number 36000000 and turn it into 36.000000. How do we do that? For starters, we divide the value by 1000000:

intLine = intLine/ 1000000

That’s going to give us – in this case – the value 36. To display that value with 6 decimal points all we have to do is call on the FormatNumber function:

intLine = FormatNumber(intLine, 6)

All we’re doing here is assigning a new value to the variable intLine. What’s that new value going to be? Well, it’s going to be the result of running the existing value of intLine (36) through the FormatNumber function, asking FormatNumber to display the result using 6 decimal places. (That’s what the parameter 6 is for: it indicates the number of decimal places we want displayed.) The end result: after turning 36000000 into 36, we’ve now turned 36 into 36.000000. Which is the very thing we set out to do in the first place.

After converting the first line (first number) in the text file we then add the new value plus a carriage return-linefeed (vbCrLf) to a variable named strText:

strText = strText & intLine & vbCrLf

As you probably figured out, strText is simply a variable that keeps track of our reformatted numbers. After we’ve read through all the lines in the text file the value of strText will look like this:

36.000000
-76.000000
92.000000
105.000000

If you’re thinking, “Say, that looks a lot like the way we want the numbers in the text file to be formatted,” well, give yourself a gold star: this looks exactly like the way we want the numbers in the text file to be formatted.

Because of that we can achieve our end goal simply by saving the value of strText to the text file. To do that we first close the file C:\Scripts\Test.txt. Why? Remember, we opened the file for reading; in order to write to it, we have to close the file and then reopen it for writing. That’s what we do here:

objFile.Close
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)

We use the Write method to write the value of strText to the text file and then close the file one last time. The net result: our text file now looks like this:

36.000000
-76.000000
92.000000
105.000000

Not bad for a bunch of Scripting Guys with limited math skills, is it?

Note. Despite our self-deprecation we should point out that one of the Scripting Guys completed his entire income tax return without using a calculator! Granted, you don’t really need a calculator when working with tiny numbers like his salary, but, still, we thought it was impressive nonetheless. (Editor’s Note: Before the IRS decides to audit all of us, we’ll save them the trouble and point out that it was Greg.)


How Can I Remove All Duplicate Lines From a Text File?

Hey, Scripting Guy! How can I remove all duplicate lines from a text file?

-- SW

Hey, SW. You know, to be a Scripting Guy means to embark on a never-ending quest to find the ultimate solution to a given problem. (Or at least that’s what we tell our manager when he asks why we never actually seem to finish anything: “But boss, never-ending quests take time!”) That’s why we were glad to see your question. Awhile back we answered a similar question, one about removing duplicate names from a text file. The solution we came up with was simple enough, and it worked just fine; we just weren’t convinced that it was the best solution. And now, thanks to your question, we can take another shot at this. As to whether this is a better/faster/easier solution than the one we offered before, well, we’ll leave that up to you.

To begin with, we assume you have a text file where each line represents a separate record. It’s unlikely, but maybe your file looks like this:

This is one of the lines in the text file.
This is one of the lines in the text file.
This is another line in the text file.
This is one of the lines in the text file.
This is yet another line in the text file.
This is another line in the text file.
This is another line in the text file.
This is one of the lines in the text file.

You want a script that can weed out all the duplicate lines and provide you with output similar to this:

This is one of the lines in the text file.
This is another line in the text file.
This is yet another line in the text file.

SW, you came to the right place:

Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H0001

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

strPathToTextFile = "C:\Scripts\"
strFile = "Test.txt"

objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
      "Data Source=" & strPathtoTextFile & ";" & _
          "Extended Properties=""text;HDR=NO;FMT=Delimited"""

objRecordSet.Open "Select DISTINCT * FROM " & strFile, _
    objConnection, adOpenStatic, adLockOptimistic, adCmdText

Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields.Item(0).Value
    objRecordSet.MoveNext
Loop

We find this script kind of interesting because we’re using ActiveX Data Objects (ADO) and treating this text file as if it were a database. We won’t spend a lot of time detailing how you treat a text file as if it were a database; if you’d like to learn more about that we have a Scripting Clinic column covering that topic in detail. For now, suffice to say that we’re working with the text file C:\Scripts\Test.txt, something we indicate by assigning the appropriate values to the variables strPathToTextFile and strFile:

strPathToTextFile = "C:\Scripts\"
strFile = "Test.txt"

So how does that enable us to eliminate duplicate lines? Well, there’s a kind of database query known as Select DISTINCT; what Select DISTINCT allows you to do is select all the distinct (or unique) records in a table. Suppose you had a simple database with these entries:

Red
Red
Blue
Red

If you use a Select DISTINCT query, you’ll get back a recordset consisting only of the unique records:

Red
Blue

No doubt you’re thinking, “Wow: getting back the unique records is pretty much the same thing as eliminating duplicate records.” And we’ll admit that – whoa, wait a second: you’re absolutely right. Our text file is constructed like a database table, with each line in the text file representing a single field in a single record. If we run a Select DISTINCT query against this text file, we’ll get back only the unique lines. In fact, we’ll get back a recordset that looks like this:

This is one of the lines in the text file.
This is another line in the text file.
This is yet another line in the text file.

That’s just exactly the information we wanted to get back. Good thing you pointed that out to us!

After retrieving our recordset we then use this code to echo the unique lines back to the screen:

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item(0).Value
    objRecordset.MoveNext
Loop

If we wanted to, we could also use the FileSystemObject to open the text file and replace the existing contents with only the unique lines; that would have the effect of removing all duplicate lines from the text file. (It would be cool if we could use some sort of Update query to do that, but when it comes to text files ADO is read-only.)

So is this the last word on removing duplicate items – be they names or entire lines – from a text file? Hey, who knows: after all, never-ending quests take time! (Actually, we find that they take about 2-to-3 days. After that we get bored and move on to something else.)


How Can I Remove All the Blank Lines from a Text File?

Hey, Scripting Guy! How can I remove all the blank lines from a text file?

-- RE

Hey, RE. You know, back in the year 2002, when the Scripting Guys were still just lowercase scripting guys, we began working on the Microsoft Windows 2000 Scripting Guide. In putting together the book we faced enormous resistance from people who thought it was a mistake to even mention text files. Why? Because nobody uses text files any more, and it was silly for us to waste our time on a dead technology.

Note. Believe it or not, we also faced enormous resistance to including a chapter on VBScript, even though this was a book designed to teach people how to write scripts using VBScript. But that’s another story.

What’s the moral to all this? It’s now 2006, and the first question being tackled in the Hey, Scripting Guy! column is one dealing with text files. Not bad for a dead technology that nobody uses, huh?

We’re assuming that you have a text file that looks something like this:

Line 1

Line 2

Line 3

What you’d like is for the text file to look like this:

Line 1
Line 2
Line 3

Can you do that using a script? Of course you can:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.Readline
    strLine = Trim(strLine)
    If Len(strLine) > 0 Then
        strNewContents = strNewContents & strLine & vbCrLf
    End If
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strNewContents
objFile.Close

Let’s talk about the basic idea before we get into the nitty-gritty details of how the script works. As you probably know, manipulating text files often involves coming up with work-arounds. Today’s script is no exception. Because we can’t directly edit a text file we have to do this instead: First, we read in the text file line-by-line. As we read each line we check to see if the line is blank. If it is, we discard it; if it’s not, we add that line (along with any other non-blank lines) to a variable named strNewContents. When we’re done reading the file we close it, then immediately reopen it for writing. (One of the vagaries of working with text files: you can read from a file or you can write to a file, but you can’t do both at the same time.) We then write the value of strNewContents to the file. The net result: we end up with a file that has no blank lines in it.

Got all that? OK. Now let’s walk through the script step-by-step.

We start off by defining a pair of constants named ForReading and ForWriting; we’ll use these constants to specify the appropriate mode when opening our text file. We then use these two lines of code to create an instance of the Scripting.FileSystemObject and then open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Next we have this block of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.Readline
    strLine = Trim(strLine)
    If Len(strLine) > 0 Then
        strNewContents = strNewContents & strLine & vbCrLf
    End If
Loop

What we’re doing here is reading the file line-by-line, starting from the beginning and ending, well, at the end. (How do we know we’ve reached the end of the file? When the AtEndOfStream property is True.)

Inside this loop we use the ReadLine method to read the first line of the text file and store it in a variable named strLine. We then use this line of code – and the Trim function – to remove any blank spaces from the beginning and/or the end of the line:

strLine = Trim(strLine)

Why do we do that? Well, suppose you have a bunch of lines that consist of a single blank space. We’re assuming that you’re counting those as blank lines and want them deleted. If you don’t want those lines deleted, then just remove this line of code from the script:

strLine = Trim(strLine)

That brings us to here:

If Len(strLine) > 0 Then

What we’re doing now is using the Len function to determine the number of characters in the line. If Len is greater than 0 then this is not a blank line; consequently, we add the line (plus a carriage return-linefeed) to a variable named strNewContents:

strNewContents = strNewContents & strLine & vbCrLf

And yes, we add this line to whatever currently happens to be in the variable strNewContents. That’s why we concatenate the existing value of strNewContents and the variable strLine and a carriage return-linefeed (using the VBScript constant vbCrLf).

But what if Len is not greater than 0? If that’s the case then we have a blank line and we don’t add the line to strNewContents. We then loop around and repeat the process for the next line in the text file.

After closing the file C:\Scripts\Test.txt we then reopen it (this time for writing) and call the Write method to write the value of strNewContents to the file. Why? That’s easy: because strNewContents consists of all the lines in the original text file except for the blank lines; those were never added to strNewContents. So where does that leave us? You guessed it:

Line 1
Line 2
Line 3

A little cumbersome, but, like we said, not bad for a “dead” technology.


How Can I Remove All the Double Quote Marks in a Text File?

Hey, Scripting Guy! How can I remove all the double quote marks in a text file?

-- DD

Hey, DD. As we’ve noted before, we could probably answer a text file question every day in this column. (Why don’t we? Well, for one thing, Hey, Text File Guy!just doesn’t have quite the same ring as Hey, Scripting Guy! If you know what we mean.) This seemed like a good question for us to answer, however, and for at least two reasons.

For one, it’s very common to find individual fields within a text file to be bracketed by double quote marks; for example, many applications that save data as a comma-separated-values (CSV) file save that data in this format:

"Ken Myer","Research Director","North American Research Lab"
"Pilar Ackerman","Associate Vice-President","European Sales"

There’s actually a good reason why data is often saved this way. For example, suppose Pilar Ackerman actually worked for this division:

Sales, European

If the individual fields weren’t enclosed in double quote marks the CSV file would look like this:

Ken Myer,Research Director,North American Research Lab
Pilar Ackerman,Associate Vice-President,Sales, European

As you can see, Ken Myer’s record contains three fields:

· Ken Myer

· Research Director

· North American Research Lab

By contrast, Pilar Ackerman’s record appears to contain four fields:

· Pilar Ackerman

· Associate Vice-President

· Sales

· European

That’s why you’ll often see the double quotes surrounding individual fields.

But while these double quotes are often necessary, any time they aren’t necessary they create a problem for script writers. For example, suppose you want to use the VBScript Split function to split each line in the file into its constituent parts. What the heck do you split on? In a case like that, you’ll find it much easier to work with the text file if you first remove all the double quotes.

For today’s column we’re going to assume that you’ve already determined that you can remove all the double quotes in the file without affecting the number of fields found on each line. Of course, doing so leads to a second problem: although we can use the VBScript Replace function to replace each double quote with nothing, how do we indicate this in code? Here’s a hint: this line of code will not work:

strContents = Replace(strContents, """, "")

In other words, trying to enclose double quotes within double quotes just leads to an “Unterminated string” error.

So what do we do about that? Well, there are a couple different ways to work around this issue, but the one we like best uses the Chr function. As you probably know, anything you can type on the keyboard has an ASCII equivalent value; for example, the uppercase A has an ASCII value of 65. The Chr function converts an ASCII value to the actual character; give Chr an ASCII value and it will return the character. Thus we can search for and replace the ASCII value 34 (which corresponds to the double quote mark) using code like this:

strContents = Replace(strContents, Chr(34), "")

Note. For a list of characters and their ASCII equivalents, see the VBScript documentation on MSDN. One nice thing about using the Chr function is that it makes it easy to search for “weird” characters like carriage returns, tabs, blank spaces and non-printing characters.

Confused? Maybe seeing the entire script – and the subsequent explanation – will help:

Const
ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)
strContents = objFile.ReadAll
objFile.Close

strContents = Replace(strContents, Chr(34), "")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strContents
objFile.Close

We begin by defining two constants – ForReading and ForWriting – that we’ll use when working with the text file. We create an instance of the FileSystemObject and then call the OpenTextFile method to open the file C:\Scripts\Test.txt. After the file is open we use the ReadAll method to read the entire file and store the contents in a variable name strContents. With the contents safely stored in memory we then close the file.

Why do we close the file? Well, we can’t use the FileSystemObject to directly modify the text file itself; that’s why we opened the file and read the contents into memory. What we’ll end up doing is changing the contents in memory and then writing this revised information back to the file. Because a file can be open for reading or for writing (but not both) we need to close Test.txt then open it again, this time for writing.

Next we use this line of code to remove all the double quotes from the file contents stored in memory:

strContents = Replace(strContents, Chr(34), "")

All we’re doing here is using the Replace function to search the variable strContents and look for all the characters with an ASCII value of 34; as we’ve already noted, that will be all the double quote marks. And what are we replacing these quote marks with? Nothing, as represented by an empty string (“”).

After modifying the file contents in memory we then re-open our text file and use the WriteLine method to replace the existing file with the new, modified contents. Upon closing the file we’ll have exactly the same text file as we had before, minus the double quote marks.


How Can I Remove Any Line in a Text File That Contains a Specified String Value?

Hey, Scripting Guy! How can I remove any line in a text file that contains the string f0?

-- CJ

Hey, CJ. Before we begin, we have a question for you: are you by any chance a serious player of online video games like EverQuest or World of Warcraft? Admittedly, none of the Scripting Guys play these games; we still find Pong to be a little over-stimulating for our tastes. However, we are absolutely fascinated by the underground economy that has sprung up around these “massively multiplayer online role-playing games.” For example, suppose you start to play EverQuest and you suddenly realize, “Dang, I need a 77 monk and a 55 druid.” What are you supposed to do then?

Now, if you’re stodgy, old-fashioned squares like the Scripting Guys, you’d probably just continue to play the game and try to earn a 77 monk and a 55 druid. Apparently, though, actually playing the game they spend all their time playing is too much effort for a lot people; instead, they just jet over to Ebay, where you can buy video game characters for the low, low price of $115:

70 monk with 720 aa's, very good character. Has gloves of coalesced flame, bone staff of wickedness, fiery staff of zha, many other great items. I can show him in game, he is currently on the cazic thule server. There is also a 55 druid, 53 ranger, 35 paladin, 35 enchanter, 33 berserker. Account comes with 260k and a few items worth over 100k. Once payment is received i will give the station name, password, and secret question.

And yes, that’s $115 in real money, not some sort of virtual EverQuest money.

Here’s another ad we stumbled upon:

This auction is for the time and energy spent to play this game and acquire the levels of the characters in the game. Only a sheet of paper containing information is being sold. [Italics added.] 75 Cleric on Luclin server. Original owner. Has many clickies that aren't included in the magelo. AoN / Spectre Mask / DE Mask / Wood Elf Mask and many other great clickies. 13k hp / 14.6k mana UNBUFFED http://eq.magelo.com/profile/727280

The asking price for the “sheet of paper containing information?” A mere $400. Also in real money.

Let’s face it: somehow, somewhere, the Scripting Guys became a bunch of old fogeys. $400 for a piece of paper? Not unless that piece of paper is a $1,000 bill.

And even then we’d have to think about it.

As we noted, the underground economy surrounding online video games has become huge; in 2002, one researcher estimated that EverQuest was the 77th richest “nation” on the face of the Earth. Admittedly, the Scripting Guys don’t fully understand why people would pay huge amounts of real money just to acquire new characters or new equipment for a video game. But then again, it’s not up to us to judge the motives behind the behaviors of other people; all we want to do is to get a piece of the action. Therefore, we have decided to offer everyone the deal of a lifetime: a Level 589 A5 script, one that has never been included in the magelo. The price for this script, which deletes all the lines in a text file that include the string f0? During this limited-time offering, you can get this script for just $3,985.

Plus shipping and handling.

Now, we know what you’re thinking, you’re thinking, “Wow, $3,985; that’s a lot of money to pay for a Level 589 A5 EverQuest script, especially when you consider the fact that there’s no such thing, and, even if there was, this script would be totally useless to anyone playing EverQuest.” Granted, that does sound a bit pricey, even to the Scripting Guys. (Although, as we noted, this has never been included in the magelo. Ever.) But we have a feeling you’ll change your mind after you learn how the script actually works.

To begin with, let’s pretend that you’re a 55 druid and you have a text file (C:\Scripts\Test.txt) that looks like this:

This line should stay.
This line (f0) should be deleted.
This line should also be deleted. (f0)
This line should not be deleted.
This line should remain.
f0: This last line should be deleted.

You’re absolutely right about that: no 55 druid could afford to be caught dead with a text file like this one, not with all those lines of text that contain the string value f0. But how are you, a mere 55 druid, supposed to delete each and every line that contains that string value, and only those lines that contain that string value?

Well, if you’re smart, and if you have $3,985, you’ll use a Level 589 A5 script like this one:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    If InStr(strLine, "f0") = 0 Then
        strNewContents = strNewContents & strLine & vbCrLf
    End If
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strNewContents

objFile.Close

And yes, we’ll explain how this script works; for $3,985, that’s the least we can do. As you can see, we start out by defining a pair of constants, ForReading and ForWriting; we’ll need these two constants when we open the text file. (For better or worse, we have to open the file twice: once to read the current contents of the file into memory, and a second time to write the modified contents back to the file.) After defining our two constants we create an instance of the Scripting.FileSystemObject object, then use the following line of code to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

You’re right: that probably is worth $3,985 right there. But we’re not done yet; far from it. Instead, once our file is open for reading we execute this block of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    If InStr(strLine, "f0") = 0 Then
        strNewContents = strNewContents & strLine & vbCrLf
    End If
Loop

Good question: what are we doing here? (No need to apologize; we wouldn’t expect a 55 druid to know what that block of code is for.) For starters, what we’re doing here is setting up a Do Until loop designed to run until we reach the end of the file. (Or, for all you 33 berserkers out there, until the file’s AtEndOfStream property is True.) Inside this loop we’re going to read the file line-by-line; that’s what this line of code (and the ReadLine method) is for:

strLine = objFile.ReadLine

As you can probably figure out for yourself, the first time through the loop we’re going to read the first line in the text file and store that value in the variable strLine. (In case you’ve forgotten, the first line in the text file is this: This line should stay.)

What we need to do now is determine whether or not the string value f0 appears anywhere within that line of text. How do we do that? Why, by using VBScript’s InStr function, of course:

If InStr(strLine, "f0") = 0 Then

The InStr function looks at a string value (like the variable strLine) and reports back the character positions where the target text (f0) is found. For example, if strLine was equal to 123f0, InStr would report back the value 4. Why? Because the target text begins in the fourth character position:

Position 1 | Position 2 | Position 3 | Position 4 | Position 5
1 | 2 | 3 | f | 0

And what if the target text can’t be found, as is the case with our first line of text? No problem; in that case, InStr returns a 0.

The fact that InStr returns a 0 if the target text can’t be found turns out to be pretty useful, at least for all us 55 druids. If InStr returns anything but a 0 that means that the target text was found somewhere in the string strValue; in turn, that means that this is a line of text that we need to discard. Because of that, we don’t do anything if we get back a value other than 0; we just ignore that line of text altogether. However, if we do get back a 0 (meaning that the target text could not be found) then we execute this line of code:

strNewContents = strNewContents & strLine & vbCrLf

Again, this should be pretty straightforward: all we’re doing here is assigning a value to a variable named strNewContents, a value made up of the following:

· The current value of strNewContents (the current value, the first time through the loop, being an empty string).

· The line of text we just read in from the text file (represented by the variable strLine).

· A carriage return-linefeed (represented by the VBScript constant vbCrLf).

And then we loop around and repeat the process with the next line in the text file.

By the time we’ve reached the end of the file (OK, OK: the end of the stream) the variable strNewContents will be equal to the following, which just happens to be the contents of the text file minus any lines that contain the string f0:

This line should stay.
This line should not be deleted.
This line should remain.

That’s great but, for the moment anyway, the text file itself remains unchanged. But we can fix that. After closing the file (text files can be opened for reading or for writing, but not for both, at least not at the same time) we then immediately reopen the thing, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

At that point all we have to do is call the Write method and write the modified contents (the value of strNewContents) to the text file:

objFile.Write strNewContents

After that we close the text file one last time and we’re done.

That should do it, CJ; just send us a check for $3,985 and we’ll call it good. Oh, and in case you’re wondering, the answer is no, at this time the Scripting Guys do not offer tours of virtual worlds like EverQuest.

But don’t fret; amazing enough, there are travel agencies that do offer guided tours of virtual worlds like EverQuest.

Maybe we Scripting Guys need to give Pong another chance. We can’t afford to get any more fogeyish than we already are.


How Can I Remove Double Double Quotes From a Text File?

Hey, Scripting Guy! I have a comma-separated values file that has data similar to this: data1",""data2"","data3",""data4"". As you can see, some of the items are enclosed by two sets of double quote marks (like ""data2""). How can I remove one of those sets of double quote marks, leaving the item looking like this: “data2”?

-- RH

Hey, RH. Yes, it’s true: Google™ is busy sending cease-and-desist letters to various news organizations and the like, asking them to refrain from using “google” as a verb. It’s still OK to say, “I did a Google Internet search for x.” However, in their mind it’s not OK to say, “I googled x.”

We don’t know whether Google will succeed in their quest; after all, Xerox®, Kleenex®, and Jell-O® all tried – not very successfully – to keep their brand names from becoming generic terms for photocopying, tissue, and gelatin, respectively. After giving this considerable thought, however, the Scripting Guys have decided that we would like to help Google out as best we can. Therefore, we are officially declaring that it’s OK to use “scripting guysed” as a replacement verb for “googled.” If you don’t like saying, “I did a Google Internet search for x,” then feel free to say “I scripting guysed x.” Consider it our little gift to the world.

Note. We should hasten to add that we’re not giving away everything, however. For example, don’t even think about writing a daily column in which the answer never seems to have anything to do with the question. That’s a Scripting Guys exclusive.

OK, now, where were we? Oh, that’s right: RH has a CSV file with data similar to this:

"data1",""data2"","data3",""data4""

What RH would like to have is a CSV file with data similar to this, with just a single set of double quotes surrounding each item:

"data1","data2","data3","data4"

Can the Scripting Guys provide a solution to RH’s problem? Let’s put it this way: when your name is synonymous with searching the Internet, hey, you can do almost anything:

Const ForReading = 1
Const ForWriting = 2

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForReading)

strContents = objFile.ReadAll

objFile.Close

strFindText = Chr(34) & Chr(34)
strReplaceText = Chr(34)

strContents = Replace(strContents,
strFindText, strReplaceText)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForWriting)

objFile.WriteLine strContents

objFile.Close

Now, admittedly, you could scripting guys the Internet and search for information about the various techniques we’ve used in this script. But no need to do that; as long as we’re here we’ll go ahead and explain how the script works.

To begin with, we define a pair of constants – ForReading and ForWriting – which we’ll use when opening the text file. (And yes, we need two constants for this; that’s because we’ll need to open the file two different times, and in two different modes.) Next we create an instance of the Scripting.FileSystemObject, then use this line of code to open the file C:\Scripts\Test.txt for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Once the file is open we then use the ReadAll method to read in the entire file and store the contents in a variable named strContents:

strContents = objFile.ReadAll

And yes, there is a good reason why we do that: the FileSystemObject doesn’t allow us to directly manipulate the text file itself. Because of that, we need to read the file and store the contents in memory; we can then do a search-and-replace operation on that copy of the file. After we’ve made the required changes we’ll reopen the file – this time for writing – and overwrite the existing contents with the new, modified contents currently in memory.

Got that? Good. If that’s the case, then you’ll understand why, after reading from the file, we immediately close it: with the file open for reading, we can no longer do anything with it. Instead, we have to close it, then reopen it for writing. (Yes, that is kind of silly. But that’s the way the FileSystemObject works.)

Now we’re ready to do a little searching and replacing; what we’re going to do is replace any set of double quotes – "" – with a set of single quotes: ". To do that, however, we’re going to use the Chr function to indicate the ASCII value of the characters we’re searching for. We do that to help prevent anyone who reads our script from getting a headache; because double quotes are a reserved character in VBScript the only way to search for double double quotes is to store those double double quotes inside another pair of double double quotes. Something like this:

""""""

That’s way too many double quotes for the Scripting Guys to deal with. Therefore, we sidestep the problem of embedding quotes within quotes within quotes by, instead, searching for consecutive instances of Chr(34):

strFindText = Chr(34) & Chr(34)

As you might have guessed, Chr(34) is equivalent to the double quote mark: ". Consequently, Chr(34) & Chr(34) will be equal to the very thing we’re searching for: "".

Of course, we don’t just want to find a pair of double quote marks; we want to replace each of these pairs with a single set of double quote marks. Therefore, in addition to defining the target text, we also need to define the replacement text. Needless to say, that happens to be a single instance of Chr(34):

strReplaceText = Chr(34)

Now, at long last, we can call the Replace function, passing this function – in order – the string value we want to search (the variable strContents); the target text we want to search for (strFindText); and the text we want to use to replace the target text (strReplaceText). That all sounds very complicated, but it really requires just a single line of code:

strContents = Replace(strContents, strFindText, strReplaceText)

Execute this line of code, and the variable strContents will contain a corrected copy of the text file, a copy that has all the doubled-up double quotes removed. Once we have that we can reopen the file Test.txt, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

The rest is easy: we call the WriteLine method to replace the existing contents of the file with the value stored in strContents, and then close the file.

That’s really all there is to it. We’re just going to walk over to the photo copy machine and scripting guys this column, then maybe have a dish of strawberry Scripting Guys (of course with whipped cream on it). See you tomorrow.


How Can I Remove the First Five Lines From All the Log Files in a Folder?

Hey, Scripting Guy! I have about 400 log files in a folder. I need to go in and remove the first 5 lines from each of those log files. How can I write a script to do that?

-- JW

Hey, JW. Before we begin we need to issue a clarification. A few days ago we reported that the Scripting Guy who writes this column had thrown a basketball the length of the court and made a basket; we dutifully termed this the greatest athletic feat in history. As it turns out, we were wrong: what this same Scripting Guy did last night was far more impressive than simply making a full-court shot in basketball.

Interestingly enough, this latest achievement occurred while the Scripting Guy who writes this column was making dinner. The Scripting Stove is none-too-big, which means that, any time you have multiple pots and pans bubbling away, those pots and pans (especially the big ones) sit very close to one another. On top of that, the Scripting Guy who writes this column is none-too-bright; among other things, that means that he has a tendency to leave the handles of these pots and pans pointing out into the middle of the kitchen (as opposed to pointing them away from all the foot traffic).

At any rate, somewhere along the line the Scripting Cook bumped into one of those handles, bumping it with enough force to send it skittering across the stove. Considering the fact that the stove is relatively small, that meant this pot bumped into a second pot, and suddenly both pots – and their contents – were headed off the stove and onto the floor.

At that moment, in a feat worthy of Superman (or, at the very least, Aquaman) the Scripting Guy who writes this column snatched one pot out of mid-air with his left-hand, then somehow managed to grab the handle of the second pot just as it started to slide off the stove. In a split second the problem was solved, and without spilling a drop.

But, really, that was no big deal: all in a day’s work for the Scripting Guy who writes this column.

Speaking of a day’s work, maybe it’s time we put in just that. JW needs a script that can remove the first five lines from every log file in a folder. Can we do that? No problem; it’s as easy as boiling water.

Note. Boiling water is easy. It’s keeping the pot of boiling water from crashing to the floor that’s hard.

Let’s start out by showing you a script that can remove the first five lines from a single text file; we’ll then see if we can figure out how to modify that script so that it works against all the log files in a folder.

Here’s our initial script:

Const ForReading = 1
Const ForWriting = 2

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

i = 1

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    If i > 5 Then
        strContents = strContents & strLine & vbCrLf
    End If
    i = i + 1
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.Write strContents

objFile.Close

As you can see, we start out by defining a pair of constants (ForReading and ForWriting), constants we’ll need when opening the text file. We then use these two lines of code to create an instance of the Scripting.FileSystemObject object and open the file C:\Scripts\Test.txt for reading:

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Got that? Good. Now, we’re assuming that our text file looks something like this:

1
2
3
4
5
A
B
C
D
E
F
G

As it turns out, the first 5 lines are just header information; we need to delete those 5 lines and leave just the good stuff: ABCDEFG. But how are we going to do that?

We’ll tell you how. (After all, that is our job.) To begin with, we assign the value 1 to a counter variable named i. What are we going to do with this counter variable? You’re about to find out.

After initializing the counter variable we set up a Do Until loop designed to run until we reach the end of the text file (that is, until the file’s AtEndOfStream property is True). Inside that loop, we use the ReadLine method to read the first line in the file, storing that value in a variable named strLine:

strLine = objFile.ReadLine

You know, you’re right: we don’t want the first line in the file, do we? For that matter, we don’t want the next 4 lines, either. Fortunately we’ve already thought of that:

If i > 5 Then

What we’re doing here is checking to see if our counter variable has a value greater than 5; as you might expect, we’re using the counter variable to keep track of which line we just read in from the text file. The first time through the loop we’re reading in line 1; it should come as no surprise, then, that the value of i is also 1.

So what happens if i is less than 5? In that case, we must be dealing with one of the first 5 lines in the text file. We don’t care about those lines so, in essence, we throw them away; we simply increment our counter variable by 1 (making i equal to 2), then loop around and repeat the entire process with line 2 in the text file. Because 2 isn’t greater than 5 we’ll end up ignoring line 2, then loop around and try again with line 3.

Sooner or later we’ll hit line 6; when we do, i will be greater than 5. In that case (and for every subsequent line), we take the value of strLine and assign it to a variable named strContents:

strContents = strContents & strLine & vbCrLf

Actually, that’s only partly true, isn’t it? What we really assign to strContents is the current value of strContents plus the value of strLine plus a carriage return-linefeed (vbCrLf). The net effect? Each time we read in a line from the text file we tack that line onto the end of strContents.

After we’ve read each line we close the file, then immediately reopen it, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

At this point we call the Write method to write the value of strContents to the file, then close the file for good. What will Test.txt look like now? It’ll look pretty much like it did before, except for one thing: the first 5 lines in the original file won’t be there any more. In other words, Test.txt will now look like this:

A
B
C
D
E
F
G

How ‘bout that?

Of course, that only helps us with the file C:\Scripts\Test.txt; how are we going to perform this same task for all the log files in a folder? Like this, of course:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

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 objLogFile In colFiles
    If objLogFile.Extension = "log" Then
        strFile = objLogFile.Name
        Set objFile = objFSO.OpenTextFile(strFile, ForReading)

        i = 1

        Do Until objFile.AtEndOfStream
            strLine = objFile.ReadLine
            If i > 5 Then
                strContents = strContents & strLine & vbCrLf
            End If
            i = i + 1
        Loop

        objFile.Close

        Set objFile = objFSO.OpenTextFile(strFile, ForWriting)
        objFile.Write strContents

        objFile.Close
      End If
Next

If this looks somewhat similar to the script we just showed you, there’s a good reason for that: it’s pretty much the same script, just tricked out a bit so that it can work against all the log files in C:\Scripts. As you can see, we start out by defining our constants then create an instance of the Scripting.FileSystemObject. After we do that, we connect to the WMI service on the local computer, then use this query to retrieve a collection of all the files in the folder C:\Scripts:

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

What are we going to do with this collection? Well, to begin with, we’re going to set up a For Each loop to walk through each item (that is, each file) in the collection. For each file we’ll then check to see if the Extension is equal to log (without the dot):

If objLogFile.Extension = "log" Then

Note. You might not have to do this. For example, if you want to open all the files in the folder, regardless of file extension, then leave this part of the code out.

If we are dealing with a log file we then assign the file Name (which is equivalent to the file path) to a variable named strFile:

strFile = objLogFile.Name

And now we will use the first script we showed you. The only difference? Instead of hard-coding the file name into our method calls we’ll use the variable strFile to reference the file we want to work with. For example:

Set objFile = objFSO.OpenTextFile(strFile, ForReading)

And guess what? This is going to open the first log file in the folder and remove the first 5 lines from that file. The script will then loop around, open the second log file, and remove the first 5 lines. This will continue until we’ve opened up every log file in C:\Scripts and removed the first 5 lines.

Give it a try and see for yourself.

Before we go we should issue one other clarification. Remember the Scripting Guy who writes this column? Well, as it turns out, his successes might be fewer and farther between then he lets on. Take the kitchen, for example. A more typical cooking session for him would be the one he had a few years ago, when he was making spaghetti. As it will sometimes do, a dab of the spaghetti sauce bubbled up and landed on top of the stove. The Scripting Guy who writes this column dutifully grabbed a dishrag and began wiping up the spilled sauce. As he did so, he apparently drug the dishrag over the burner; a second or two later, he realized that the dishrag he was holding in his hand was on fire.

Which, in case you’re wondering, is not the recommended state for a dishrag.

And then there was the time he dumped noodles into the colander, only then realizing that he hadn’t actually put the colander into the sink yet. And, of course, there was the time that – well, trust us: this could go on forever.

And ever.


How Can I Remove the Last Carriage Return-Linefeed in a Text File?

Hey, Scripting Guy! How can I remove the last carriage return-linefeed in a text file?

-- LEK

Hey, LEK. From the rest of your email you note that you’re using the FileSystemObject to read a text file, then using the contents of that text file as a query in another application. Unfortunately, though, the text file invariably has a carriage return-linefeed at the end, and that carriage return-linefeed is causing problems when you try passing the contents of the file as a query.

What does all that mean? Well, consider the simple little text file below. We opened the file and then pressed Ctrl+End to position the cursor at the end of the file. Note that the cursor does not appear immediately after the period in line 1; instead, the cursor appears at the beginning of line 2. That’s because there’s a carriage return-linefeed immediately following the period (as if someone typed in the first line and then pressed ENTER). You might not see anything on the screen, but it’s there and it’s creating problems:

So how are we going to get rid of that pesky little carriage return-linefeed? Here’s how:

· We’re going to open the file and read the contents of that file into a variable named strFile.

· We’re going to grab the last two characters stored in that variable. Why the last two characters? Well, the carriage return-linefeed actually consists of two separate characters: the carriage return (with an ASCII value of 13) and the linefeed (with an ASCII value of 10). Although we could probably get by with stripping away just the linefeed character we’re going to do things nice and neat and get rid of both characters.

· We’re going to check and see if the last characters have an ASCII value of 13 and an ASCII value of 10; an easy way to do that is to see if they are equal to the VBScript constant vbCrLf (which happens to be a carriage return-linefeed). If those last two characters are equal to vbCrLf, we’ll chop the two characters off the string and save the new value (minus the carriage return-linefeed) back to the original text file. If they don’t, then we won’t do anything at all; in that case there’s no reason to do anything.

Got all that? Here’s a script that carries out all those tasks:

Const
ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\scripts\test.txt", ForReading)
strFile = objFile.ReadAll
objFile.Close

intLength = Len(strFile)
strEnd = Right(strFile, 2)

If strEnd = vbCrLf Then
strFile = Left(strFile, intLength - 2)
Set objFile =
objFSO.OpenTextFile("C:\scripts\test.txt", ForWriting)
objFile.Write strFile
objFile.Close
End If

We start off by defining a pair of constants – ForReading and ForWriting – that we’ll use when we work with the text file. We create an instance of the FileSystemObject and use the OpenTextFile method to open the file C:\Scripts\Test.txt. We then use the ReadAll method to read in the entire contents of the text file and store that information in a variable named strFile. After all that we immediately close the text file. (Why? Well, the FileSystemObject allows you to open a file for reading or for writing. Because we have to read the file we had to open it for reading; if we later need to make changes to the file we’ll have to reopen it for writing. We’re done reading the file so we might as well close it.)

Next we use the Len function to determine the length of the variable strFile (that is, how many characters are in the string). This value – which we’ll use in a minute – is stored in the variable intLength.

So far so good, right? Now we need to determine whether or not the existing file has a carriage return-linefeed on the end. As we noted earlier, this means checking to see if the last two characters in the file consist of a carriage return character and a linefeed character. Thus we use the Right function to grab the two characters at the very end of the string and store these in the variable strEnd. That’s what we do here:

strEnd = Right(strFile, 2)

We can then check to see if strEnd is equal to vbCrLf. If it is, we use this line of code to grab all the characters except the two at the very end:

strFile = Left(strFile, intLength - 2)

Remember when we used the Len function to determine how many characters are in the string? Well, now we use that value to take the first length – 2 characters; for example, if our string has 37 characters, we’ll take only the first 35 (37 – 2). That will effectively chop the carriage return-linefeed off the end of the string.

We then reopen our text file – this time for writing – and replace the existing contents with the new string value. That will leave us with our original text file, minus the trailing carriage return-linefeed. And that’s just exactly what you were hoping to end up with.


How Can I Replace Selected Lines in an XML File?

Hey, Scripting Guy! How can I read lines 56 to 62 in one XML file, and replace those with lines 56 to 62 taken from another XML file?

-- JA

Hey, JA. First of all, we’d like to thank you for your question; in addition, we’d like to welcome you and everyone else to today’s Open House in honor of the 501st Hey, Scripting Guy! column. We know that – sorry, could we put you on hold for a second?

Hmmm …. The Scripting Editor has pointed out that we just had an Open House in honor of the 500th column and that there is no way we’re having another one, not this soon anyway. To tell you the truth, that doesn’t make much sense to us: if 500 columns are considered noteworthy, well, then wouldn’t 501 columns be even better? But we aren’t going to argue with her; we don’t really want her turning us into newts.

Like she did to poor Dean.

Editor’s Note: The fact that Dean bears a striking resemblance to a newt has no connection to any special powers the Scripting Editor may or may not have.

So now what do we do? Seeing as how we aren’t going to have another big party today, we might as well try to come up with a script that can replace selected lines in an XML file. A script like this one:

Const ForReading = 1
Const ForWriting = 2

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.xml", ForReading)

For i = 1 to 55
    objFile.SkipLine
Next

For i = 1 to 7
    strText = strText & objFile.ReadLine & vbCrLf
Next

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\New_test.xml", ForReading)

For i = 1 to 55
    strNewText = strNewText & objFile.ReadLine & vbCrLf
Next

strNewText = strNewText & strText

For i = 1 to 7
    objFile.SkipLine
Next

Do Until objFile.AtEndOfStream
    strNewText = strNewText & objFile.ReadLine & vbCrLf
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\New_test.xml", ForWriting)

objFile.WriteLine strNewText

objFile.Close

The first thing you might have noticed about this script is that we don’t use any fancy XML coding; instead we use the FileSystemObject and treat this XML file as if it was just another text file. Why do we do that? That’s simple: an XML file is just another text file. Obviously there are times when you need to use the XML object model to manipulate an XLM file; this isn’t one of those times.

So what do we do with this XML file? Well, we start off by defining a pair of constants: ForReading and ForWriting. We’ll use these two constants to tell the FileSystemObject which mode (for reading or for writing) we want to use when opening the files. After defining the two constants we create an instance of the Scripting.FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Test.xml, the file that contains the text we want to copy.

Pretty easy so far, right? Of course, at this point it gets just a teeny-tiny bit tricky. We’ve already decided that we don’t want lines 1 through 55 of this file; we want only lines 56 through 62. Therefore, we set up a For Next loop that runs 55 times; inside that loop, we use the SkipLine method to simply skip past the line in the text file:

For i = 1 to 55
    objFile.SkipLine
Next

What does that do for us? That enables us to skip past the first 55 lines, leaving us exactly where we want to be: line 56. Having reached the promised land (or at least the promised line), we then set up another For Next loop, this one running from 1 to 7:

For i = 1 to 7

Why 1 to 7? That’s easy: our goal now is to copy 7 lines out of the file: lines 56, 57, 58, 59, 60, 61, and 62. Thus a loop that runs from 1 to 7.

Inside that loop we execute this line of code:

strText = strText & objFile.ReadLine & vbCrLf

What we’re doing here is storing those 7 lines of text into a variable named strText. To do that we set the value of strText to the existing value of strText plus the current line in the text file (which we can retrieve using the ReadLine method), plus a carriage return-linefeed. For example, suppose lines 56 and 57 in the XML file consist of the following:

<computer>
    <name>

The first time through the loop strText will be assigned the following:

· The current value of strText (which is nothing).

· The value of line 56 (<computer>).

· A carriage return-linefeed.

· The second time through the loop strText will be assigned the following:

· The current value of strText (<computer>, plus a carriage return-linefeed).

· The value of line 57 ( <name>).

· A carriage return-linefeed.

Etc., etc.

After we’ve read lines 56-62 we’re finished with Test.xml; therefore, we use this line of code to close the file and get on with our lives:

objFile.Close

So what exactly are we going to do with the rest of our lives? Well, the first thing we’re going to do is open the file C:\Scripts\New_test.xml; this is the file where we want to insert the lines we just read in. Notice, however, that we don’t open the file for writing; instead, we open it for reading:

Set objFile = objFSO.OpenTextFile("C:\Scripts\New_test.xml", ForReading)

What sort of madness is that? Well, unfortunately, we can’t simply replace text in a text file; the FileSystemObject won’t allow that. Therefore, what we need to do is construct a replacement file in memory: we need to read lines 1 through 55 in the current file, add the seven lines we copied from the first file, skip lines 56-62, and then add in the rest of New_test.xml. Confusing? You bet. But let’s walk through it and see if we can explain how that all works.

Reading lines 1 through 55 are easy: we just use this block of code, stashing the results in a variable named strNewText:

For i = 1 to 55
    strNewText = strNewText & objFile.ReadLine & vbCrLf
Next

Like we said, that was easy; we just did something very similar a few minutes ago. Once we have lines 1 through 55 safely stored away we can use this code to append the lines we copied from our first XML file:

strNewText = strNewText & strText

That gives us – in memory – a 62-line text file: the first 55 lines having been read in from New_test.xml, the last 7 lines having been copied from Test.xml. What we need to do now is grab the rest of New_test.xml, beginning with line 63 and continuing to the end of the file.

Why “beginning with line 63?” Well, remember, lines 56 through 62 are already in place; that’s what we copied from Test.xml. We need to skip lines 56 to 62 in New_test.xml, something we do using this block of code:

For i = 1 to 7
    objFile.SkipLine
Next

And then to get line 63 on through the end of the file we use this code:

Do Until objFile.AtEndOfStream
    strNewText = strNewText & objFile.ReadLine & vbCrLf
Loop

Make sense? If not, walk yourself through the script line-by-line. It might take a time or two, but it should become clear soon enough.

Once we’ve read through the rest of New_test.xml we close the file; that’s because the file was originally opened for reading, which means we have to close the thing and then immediately reopen it for writing. (It sounds silly to us, too. But it is what it is.) We use these two lines of code to close and reopen the file:

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\New_test.xml", ForWriting)

All we have to do now is write the contents of the variable strNewText to the file, close the file one last time, and then call it good.

Granted, it seems a little convoluted, but it works. And, again, the same basic approach works on any text file, not just on XML files.

We hope that helps, JA. And we hope that you – and everyone else – will come back tomorrow. As you probably know, tomorrow will be the 502nd Hey, Scripting Guy! column, and we have a huge celebration planned.

Well, as long as you-know-who doesn’t find out, that is.

Editor’s Note: Has anyone ever noticed that Greg has always kind of resembled a toad….


How Can I Replace Single Quote Marks in a Text File?

Hey, Scripting Guy! I saw your column on replacing text in a text file, and that’s almost the solution I’m looking for. However, the text I need to replace is the single quote mark, and I can’t figure out how to do that. How do I replace single quote marks in a text file?

-- JB

Hey, JB. You know, this is shaping up to be another very hectic day for the Scripting Guys; after all, we have meetings in the morning as well as meetings in the afternoon. That would be bad enough, but these meetings also involve such heavy-duty issues as our budget for the coming year and the new system that we will eventually have to use in order to manage the Script Center. And yet, as busy as he is, there’s still one thing that remains first and foremost in the mind of the Scripting Guy who writes this column: what should he have for dinner tonight?

Maybe chicken casserole; the Scripting Family hasn’t had chicken casserole in awhile. And maybe “candied carrots,” you know, the ones with the butter and the brown sugar glaze on them. Or maybe ….

To be honest, JB, this could take awhile; after all, important decisions like this should never be made lightly. While you wait, feel free to read a magazine. If you don’t happen to have a magazine handy, then feel free to read the following script, one that replaces all the single quote marks in a text file. In fact, not only does it replace the single quote marks, but it replaces them with double quote marks!

OK, sure, that sounds impossible. But the proof, as they say, is in the pudding.

Hmmm, pudding might be good, too ….

Here’s the script:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strText = objFile.ReadAll
objFile.Close

strOldText = Chr(39)
strNewText = Chr(34)

strNewText = Replace(strText, strOldText, strNewText)

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strNewText
objFile.Close

What we’re assuming here is that we have a text file that looks something like this:

'Ken Myer','Finance','Senior Analyst'
'Pilar Ackerman','Human Resources','Manager'
'Jonathan Haas','Transportation','Director'

All we want to do is replace all the single quote marks with double quote marks. As JB has discovered, however, that’s not quite as easy as it sounds. Why not? We’ll explain that in a moment. Before we do that, however, let’s talk about the first part of the script.

As you can see, we kick things off by defining a pair of constants (ForReading and ForWriting); we’ll use these constants when we open our text file. (And yes, we do have to open the file twice: once to read in the existing contents, then once more to write out the new, revised contents.) After defining the constants we create an instance of the Scripting.FileSystemObject, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

What do we do with the file once it’s open? For starters, we use the ReadAll method to read the entire file and store the contents in a variable named strText. And then we immediately turn around and close the file.

Note. Yes, that does seem sort of silly, doesn’t it? However, the FileSystemObject will only let us open a file and read from it, or open a file and write to it; we can’t carry out both operations at the same time. (Strange but true.) Having read from the file we have no choice but to close the thing before we’ll be allowed to write to it.

In a typical search-and-replace script this would be the point where we would call the VBScript Replace function and replace the single quote marks with double quote marks. We’re guessing that JB has tried to do just that, using a line of code similar to this:

strNewText = Replace(strText, "'", """)

Now, syntactically, that looks OK; after all, we’ve called the Replace function followed by three parameters:

· strText, the text we want to do the search-and-replace operation on.

· "'", the single quote mark (embedded between a pair of double quote marks), representing the text to be replaced.

· """, the double quote mark (embedded between a pair of double quote marks), representing the replacement text.

Like we said, that looks OK. But here’s what happens when we try to use this code:

C:\Scripts\test.vbs(13, 40) Microsoft VBScript compilation error: Unterminated string constant

Yikes! What’s that all about?

As you might have guessed, the problem is due to the fact that both the single quote mark and the double quote mark have special meanings in VBScript. The single quote mark, to name one use, indicates a comment added to a script:

' This is a comment

Meanwhile, the double quote mark is used – among other things – to enclose string values:

x = "This is a string value."

Because these characters have special meanings, all we’ve managed to do so far is to confuse VBScript. For example, a human being might look at three consecutive double quote marks and think, “Oh, this is just a double quote mark surrounded by two other double quote marks.” Unfortunately, though, VBScript doesn’t see it that way; instead, VBScript desperately tries to interpret that statement using its built-in parsing rules. One of those rules says that, for every opening double quote mark, there must be a corresponding ending quote; in other words, double quote marks must travel in pairs. When VBScript sees an odd number (three) of double quote marks it assumes that one of the pairs is missing a quote mark.

Is that a problem? You bet it is: because of the syntax error the script won’t even compile, let alone run.

This – or some variation of it – is the situation JB has run into. So how do we work around the issue? There are actually a couple different ways to do that, but we chose this approach:

strOldText = Chr(39)
strNewText = Chr(34)

What we’re doing here is using the Chr function to assign values to a couple of variables (strOldText and strNewText). If you provide Chr with an integer value (such as 39) it will report back the character that corresponds to that value. Guess what character has an ASCII value of 39? You got it: the single quote mark. As for ASCII value 34, that – well, needless to say, you’re way ahead of us: yes, Chr(34) is the double quote mark.

See what we’re doing here? In the first line we’ve assigned the single quote mark to the variable strOldText; this enables us to use a reference to the single quote mark without using the character itself. We then do the same thing with the double quote mark. Why? Because now we can call the Replace function and use variable names rather than problematic statements like """:

strNewText = Replace(strText, strOldText, strNewText)

This statement is syntactically correct. Equally important, it will replace all the single quote marks with double quote marks.

Note. So how did we know that Chr(39) was the single quote mark and Chr(34) was the double quote mark? Believe it or not that was something we just plain knew. (Sometimes the Scripting Guys do know what they’re talking about ... sort of) But you don’t have to know that Chr(39) is the single quote mark; instead you can get ASCII values any time you need them simply by looking in the VBScript Language Reference.

The rest of the script practically writes itself. After reopening Test.txt (this time for writing) we call the Write method and replace the existing contents of the file with our newly-revised contents (the ones stored in the variable strNewText). We then close the text file and go back to planning tonight’s dinner. Does garlic bread sound good to anyone else?

Incidentally, when all is said and done our text file will look like this:

"Ken Myer","Finance","Senior Analyst"
"Pilar Ackerman","Human Resources","Manager"
"Jonathan Haas","Transportation","Director"

Which is exactly the way we want it to look.

And now that today’s column is done, and dinner is planned, it’s time to head off to our first meeting, the one about budgets. Last year, for the first time ever, the Scripting Guys actually got a budget; we took that money and immediately spent the whole thing on Dr. Scripto Bobblehead dolls. That should make this year’s budget meeting an interesting one, to say the least.


How Can I Replace the First Five Characters in a Text File With the First Five Characters in the Computer Name?

Hey, Scripting Guy! How can I open a text file and replace the first 5 characters with the first 5 characters of the local computer name?

-- EC

Hey, EC. You know, you might find this hard to believe (his fellow Scripting Guys absolutely refuse to believe it), but sometimes the Scripting Guy who writes this column is all talk and no action. For example, a few months ago he told everyone that he was planning on going back to college and getting a second Master’s degree (taking advantage of an email that promised he could get this degree in just two weeks). But was he really planning on going back to college? To tell you the truth, no: he’s way past the point where he ever wants to work hard or learn anything ever again.

Or so he thought. But then, last night, his life changed forever.

You might find this even harder to believe, but there was a time, long ago, when the Scripting Guy who writes this column was actually fairly smart; in fact, after he took his SAT test he was inundated with offers to attend universities all over the country. Harvard, Yale, Stanford, you name it. In the end, he opted to stay in the state of Washington, partly due to the costs involved, but largely because of the ho-hum factor. No offense to Harvard or Yale, but, to tell you the truth, places like that didn’t sound very exciting. As far as the Scripting Guy who writes this column was concerned, it didn’t matter: if you’ve seen one university you’ve seen them all.

Or so he thought. But then, last night, the Scripting Son was flipping through channels on the TV when he landed on the Food Network. It was there that the Scripting Guy who writes this column finally figured out what he was meant to do with his life: graduate from Dunkin’ Donuts University. That’s right, Dunkin’ Donuts University (founded in 1966), a school where the curriculum is based entirely on making – and eating – donuts. Just try to find courses on donut-making and donut-eating at one of those hoity-toity Ivy League colleges.

Well, OK, maybe Princeton. But other than that ….

There is a problem, however. Back when the Scripting Guy who writes this column was being inundated with invitations to attend various colleges and universities he did not receive an invitation from Dunkin’ Donuts U. Does that mean he isn’t qualified to attend Dunkin’ Donuts University? Considering the number of donuts he’s eaten in his life he expected, if anything, to be over-qualified. On the other hand, however, Dunkin’ Donuts is probably looking for smart, hard-working, and responsible people.

Dang! There’s always a catch, isn’t there?

Unless …. Hey, admissions committee at Dunkin’ Donuts University. How would you feel about a smart, hard-working and responsible guy who can write a script that can replace the first 5 characters of a text file with the name of the local computer?

Well, OK. Then how about this same script written by a not-so-smart, lazy, and somewhat-irresponsible guy:

Const ForReading = 1
Const ForWriting = 2

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

strContents = objFile.ReadAll
objFile.Close

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName
strComputer = Left(strComputer, 5)

intlength = Len(strContents)
strRemainder = Right(strContents, intLength - 5)
strNewContents = strComputer & strRemainder

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strNewContents

objFile.Close

On the off-chance that the admissions committee thinks the Scripting Guy who writes this column did nothing more than download this script off the Internet (like he did his master’s thesis) let’s take a few minutes to explain how this baby works. To begin with, we have a text file named C:\Scripts\Test.txt, a text file that (for our example purposes) looks like this:

XXXXX This is our text file. We want to replace the first five
characters (the Xs) with the first five characters of the
local computer's name.

As you can see, and as you can read for yourself, the first five characters in the text file are nothing but Xs. Our job is to replace those five characters with the first five characters in the name of the local computer.

To do that, we start out by defining a pair of constants, ForReading and ForWriting; we’ll need to use these two constants when we open Test.txt. (And yes, for better or worse we need to open the text file twice: once to read in the existing contents, the second time to replace those contents with our new, modified contents.) After defining the two constants we then use these two lines of code to create an instance of the Scripting.FileSystemObject and to open the text file for reading:

Set objFSo = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

As soon as we have the file open we use the ReadAll method to read the contents of the file (storing that information in a variable named strContents), then we close Test.txt:

strContents = objFile.ReadAll
objFile.Close

Now what?

Funny you should ask that. (We hope that the admissions committee will take note of our prompt response to a customer inquiry.) Our next step is to run this block of code:

Set objNetwork = CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName

Here we’re creating an instance of the Wscript.Network object, then assigning the value of the ComputerName property to the variable strComputer. It probably doesn’t come as any great surprise that the value of the ComputerName property just happens to be the name of the computer itself. Of course, we don’t actually want the entire computer name; all we want are the first five characters. With that in mind we then use the Left function to grab those first five characters and store them in the variable strComputer:

strComputer = Left(strComputer, 5)

The net result? If we happen to be running this script on a computer named GIZMONIC (which we are) then strComputer will be equal to this:

GIZMO

We now have the first five characters in the computer name. Our next task is to replace the first five characters in the text file with these computer name characters. Depending on the kind of information in your text file you might be able to do this using a search-and-replace operation; that will work as long as the first five characters in the text fill will always be XXXXX, and as long as it’s OK to replace any other instances of XXXXX with GIZMO. We can’t be sure that those conditions will always hold true, so we decided to replace the first five characters using this approach:

intlength = Len(strContents)
strRemainder = Right(strContents, intLength - 5)
strNewContents = strComputer & strRemainder

In the first line of this code block we use the Len method to count the number of characters in strContents; if you’re keeping score at home, that’s 149. As you know, we want to replace only the first five characters in this string; we want to keep characters 6 through 149 exactly as they are. That’s what our second line of code is for; in that line we use the Right function to start at the end of the string and work our way backwards, scooping up characters as we go. How many characters do we scoop up? We want to grab a number equal to the length of the string minus 5. (149 – 5 equals 144, which means that we want to start at the end of the string and, working from right-to-left, grab the first 144 characters.) In turn, our variable strRemainder will be assigned everything except the first five characters in the string (because we stopped grabbing characters before we got to those).

Or, to put it another way, strRemainder ends up equal to this:

This is our text file. We want to replace the first five
characters (the Xs) with the first five characters of the
local computer's name.

As you can see, that’s the entire string except for the first five characters.

Finally, in line 3 we combine the variables strComputer and strRemainder, then assign that value to a variable named strNewContents. That makes this new variable equal to the following:

GIZMO This is our text file. We want to replace the first five
characters (the Xs) with the first five characters of the
local computer's name.

Well, what do you know: we’ve replaced the first five characters in the text file (XXXXX) with the first five characters in the computer name (GIZMO).

All that’s left now is to reopen the file Test.txt (this time for writing), use the WriteLine method to overwrite the old contents with our new, modified contents, and then close the file:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)
objFile.WriteLine strNewContents

objFile.Close

That should do it.

We hope that helps, EC. (And we hope that impresses you, Dunkin’ Donuts University admissions committee.) We’d like to stay and take questions from the audience, but we need to hurry down to the local bakery and pick up a couple dozen donuts. After all, it’s never too early to start studying for our entrance exam.

At least not when you’re hoping to attend Dunkin’ Donuts University.


How Can I Reverse All the Lines in a Text File?

Hey, Scripting Guy! How can I “rotate” a text file 180 degrees? I need the last line in the file to become the first line, the first line in the file to become the last line, and all the lines in between to switch places as well.

-- BJM

Hey, BJM. You know, when most people think of the Scripting Guys they think, “OK, sure, those guys are absolute geniuses when it comes to system administration scripting. But that just means they’re a bunch of geeks; I bet they aren’t well-rounded intellectuals who know the slightest thing about anything besides system administration scripting.” But you want to know the truth? The truth is, we Scripting Guys are – what’s that? Who said that the Scripting Guys are absolute geniuses when it comes to system administration scripting? Well, you know, there was that one guy, the one who always wears the blue shirt. Oh, and the girl in the green baseball hat; she said we were absolute something-or-others (probably “absolute geniuses,” although it was kind of hard to hear her over all the screaming). And, well, needless to say, far more people than we could possibly list in today’s column.

Far more.

Anyway, the point is that … wait a second here … the point is that …hold on a second…. The point is that the Scripting Guys are well-rounded intellectuals, that we are people who know about things other than system administration scripting. For example, the moment we saw BJM’s question, the one about making the last line in a text file the first line and the first line in a text file the last line, well, we immediately thought of the parable of the vineyards: “So the last will be first and the first will be last.” That sounds pretty well-rounded to us.

Of course, we didn’t exactly know what to do with that phrase, nor did we have any idea how we could relate that parable to system administration scripting. In other words, we had a tiny, isolated piece of knowledge that had no practical application whatsoever. But if that’s not a definition of an intellectual, well, we don’t what is!

So we admit it: we’re not sure what the parable of the vineyards has to do with rotating the lines in a text file. (Although we do think tossing a famous quote into a column every now and then sure makes it sound like we know what we’re talking about.) On the other hand, we are sure that the following script has a bit more relevance to the job of reversing all the lines in a text file.

Well, pretty sure, anyway. Here’s the script:

Const ForReading = 1
Const ForWriting = 2

Dim arrLines()
i = 0

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    Redim Preserve arrLines(i)
    arrLines(i) = objFile.ReadLine
    i = i + 1
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

For i = Ubound(arrLines) to LBound(arrLines) Step -1
    objFile.WriteLine arrLines(i)
Next

objFile.Close

As you can see, we start out by defining a pair of constants (ForReading and ForWriting), constants we’ll need when we open the text file to be “rotated.” After defining the two constants we then use this line of code to declare a dynamic array named arrLines:

Dim arrLines()

Once the array has been declared we set the value of a counter variable i to 0.

Note. What’s that? What’s a “dynamic array?” Well, in VBScript, a plain old array is an array of a fixed size; in advance, you must specify that the array will hold, at most, 10 or 12 or 15 or whatever number of items. By contrast, a dynamic array is, well, a bit more dynamic: it can end up being any size, and can be resized on-the-fly to accommodate new items. We need a dynamic array here for one simple reason: we’re going to use the array to store all the lines we read in from the text file and, because we have no idea how many lines are actually in the text file, well, that means that we can’t specify the size of the array in advance. That leaves just one option: a dynamic array.

Our next step is to create an instance of the Scripting.FileSystemObject object, then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading; that’s what we do with these two lines of code:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

In turn, that brings us to the following section of the script:

Do Until objFile.AtEndOfStream
    Redim Preserve arrLines(i)
    arrLines(i) = objFile.ReadLine
    i = i + 1
Loop

What we’re doing here is setting up a Do Until loop that runs until we’ve read the entire text file (that is, until the file’s AtEndOfStream property is True). Inside the loop, the very first thing we do is execute this line of code:

Redim Preserve arrLines(i)

As it turns out, the Redim Preserve statement is the key to resizing our dynamic array. The Redim statement is used to change the size of the array; meanwhile, the Preserve statement tells the script to be sure and preserve any existing data in the array. That, by the way, is very important. Suppose we left out the Preserve statement and executed this line of code instead:

Redim arrLines(i)

What would happen in that case? Well, in that case our array would be resized. However, any existing items in the array would be deleted, and we’d be left with an empty array of size i.

Which reminds us: notice that we use the counter variable i to indicate the size of the array. Because i is currently equal to 0, that means that, the first time through the loop, we’ll end up with an array named arrLines that has a size of 0. (Which means that the array can hold one item: an array always holds one more item than the size would seem to indicate. That’s because the first item in an array is always given the index number 0.)

So what happens once we have an array of size 0? Well, our next step is to use the ReadLine method to read the first line in the text file, then assign that value to item 0 in the array. That’s what we do here:

arrLines(i) = objFile.ReadLine

We then increment the value of i by 1. That means that, the second time through the loop, i will be equal to 1. In turn, that means we’ll resize the array to a size of 1 (meaning the array can now hold 2 items), then we’ll read the next line in the text file and assign that value to the second item in the array (the item with the index number 1). This process then continues until we’ve read and processed each and every line in the text file, at which point we automatically exit the Do Until loop. As soon as that happens we use the Close method to close the file C:\Scripts\Test.txt.

So what does all this mean? This means that the array arrLines now contains the individual lines in the text file, lines that we’ll assume look like this:

E. This is line 5.
D. This is line 4.
C. This is line 3.
B. This is line 2.
A. This is line 1.

In other words, arrLines is now the spitting image of the file C:\Scripts\Test.txt.

At this point we’re ready to rotate the text file. As it turns out, VBScript doesn’t include a function for rotating a text file; however, VBScript does include a procedure for reading an array from the bottom to the top. That’s the approach we’re going to use to reverse the lines in the file.

To that end, our next step is to reopen the file C:\Scripts\Test.txt, this time for writing:

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)

As soon as the file has been reopened, we execute this block of code:

For i = Ubound(arrLines) to LBound(arrLines) Step -1
    objFile.WriteLine arrLines(i)
Next

What we’re doing here is reading the array from the bottom to the top: we’re starting with the Ubound item (the last item in the array) and then finishing with the Lbound item (the first item in the array). In other words, our For Next loop is starting with item 4 (remember, in a 5-item array like ours the very last item has an index number of 4) and finishing with item 0 (because the first item in an array always has the index number 0). We use the statement Step -1 to ensure that our loop runs backwards: item 4, item 3, item 2, item 1, item 0.

Inside the loop, we simply use the WriteLine method to write the value of the current array item (item 4 the first time through the loop) to the text file. This continues until we’ve accessed each item in the array, at which point we exit the loop, close the file, and then take off to pursue more intellectual pastimes.

Like watch yet another edition of Baseball Tonight on ESPN.

So what’s the net effect of all this? This is the net effect:

A. This is line 1.
B. This is line 2.
C. This is line 3.
D. This is line 4.
E. This is line 5.

As you can see, we’ve successfully rotated our text file: the first is now last, the last is now first, and everything in between has been shuffled around accordingly. Which is just the way we wanted things to be shuffled.

There you have it, BJM. Incidentally, we know that you had another question about text files. But don’t worry; we’ll address that question in the near future. For now, however, we need to clarify one thing: upon double-checking our sources, it turns out that the parable of the vineyards does not include the phrase “So the last will be first and the first will be last.” Instead, the correct translation is actually “So the last will be first and the Scripting Guys will be last.” Which, needless to say, is something we’ve known for a long time.

Or at least since last year’s performance review.


How Can I Save Output to a Text File?

Hey, Scripting Guy! Is there an easy way to save my script output to a text file rather than displaying it on screen?

-- KP, Ogden, UT

Hey, KP. If you always want your script to save data to a text file rather than display it on screen, then you’re probably better off using the FileSystemObject and its file-writing capabilities. However, it appears as though you sometimes want to save data to a text file and sometimes want to display that data in a command window. So can you create a multi-purpose script like that?

Well, you could, but an easier way is to use the command shell redirection characters when you start the script. For example, you might typically start your script like this:

cscript myscript.vbs

That runs the script, and causes any Wscript.Echo commands to display their output in the command window.

However, you could also start your script like this, using the > command shell redirection command:

cscript myscript.vbs > c:\scripts\log.txt

In that case, nothing will be echoed to the screen; instead, all your Wscript.Echo commands will be written to the text file C:\Scripts\Log.txt.

In the preceding example, should C:\Scripts\Log.txt already exist it will be overwritten with any new information generated by the script. If you’d prefer to have new information appended to C:\Scripts\Log.txt, then start the script using this command:

cscript myscript.vbs >> c:\scripts\log.txt


How Can I Sort the Contents of a Text File?

Hey, Scripting Guy! I have a text file that contains a list of computer names. How can I sort that file alphabetically?

-- LR

Hey, LR. If we wanted to take the easy way out we’d just tell you, “Sorry, you can’t do that.” And we could get away with that because none of Microsoft’s scripting technologies feature a way to open up and then sort a text file. But, hey, when have the Scripting Guys ever taken the easy way out?

Oh, right: we were hoping everyone had forgotten about those times. But this time we’re going to give you a workaround.

Although there’s no straightforward way to sort a text file, we can achieve the same net result by doing the following: 1) Use the FileSystemObject to read the file into memory; 2) Sort the file alphabetically in memory; 3) Replace the existing contents of the file with the sorted data we have in memory. A slightly roundabout way of doing things, but you’ll end up with a text file sorted alphabetically, which is all you really care about.

To carry out this task we’ve chosen to use a disconnected recordset. There are other ways we could do this (say, a bubble sort), but a disconnected recordset gives us more flexibility and is much easier to explain, especially for those of you who have some experience with database programming. We’ll give you a quick overview of disconnected recordsets here; if you want a more detailed explanation of how they work, you might want to check out the Scripting Week 2 webcast Things the Scripting Guys Never Told You. (As an added bonus, that webcast will also show you how you can use a bubble sort to sort single-field data.)

Before we begin, we assume you have a text file that looks like this, with one computer name per line:

red-ws-02
atl-ws-01
sf-ws-02
atl-ws-02
atl-ws-03
red-ws-02
sf-ws-01

How can we read in these computer names, sort them, and then write the sorted list back to the text file? Why, we can use a script that looks just like this one:

Const adVarChar = 200
Const MaxCharacters = 255
Const ForReading = 1
Const ForWriting = 2

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "ComputerName", adVarChar, MaxCharacters
DataList.Open

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Computers.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    DataList.AddNew
    DataList("ComputerName") = strLine
    DataList.Update
Loop

objFile.Close

DataList.Sort = "ComputerName"

DataList.MoveFirst
Do Until DataList.EOF
    strText = strText & DataList.Fields.Item("ComputerName") & vbCrLf
    DataList.MoveNext
Loop

Set objFile = objFSO.OpenTextFile("C:\Scripts\Computers.txt", ForWriting)

objFile.WriteLine strText
objFile.Close

We begin by defining a series of constants needed to create our disconnected recordset. (Think of a disconnected recordset as a database that exists only in memory, and is not tied to a physical database stored on a disk drive.) We then use this chunk of code to create a disconnected recordset that consists of a single field (ComputerName):

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "ComputerName", adVarChar, MaxCharacters
DataList.Open

Next we use the FileSystemObject to open the text file C:\Scripts\Computers.txt. At this point we’re ready to start populating our recordset. What we’re going to do is read the text file line-by-line. Each time we read in a line, we’ll use the AddNew method to add a new record to the recordset. We’ll set the value of the ComputerName field to the line we just read in from the text file (remember, each line in the text file represents a single computer name), and then use the Update method to save the record to the recordset. We’ll continue doing that until we’ve read in each line of the text file, at which point we’ll close the file.

Yes, that sounds like a lot but, as you can see, all that requires only a few lines of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    DataList.AddNew
    DataList("ComputerName") = strLine
    DataList.Update
Loop

objFile.Close

Next we need to sort our recordset. This is one of the great benefits of using a disconnected recordset rather than a bubble sort or some other manual sorting algorithm. Sorting our recordset takes only a single line of code:

DataList.Sort = "ComputerName"

Now that we have a sorted recordset we need to write the data back to our text file. The easiest way to do this is to walk through the recordset, grab each record, and stash the entire recordset in a variable. That’s what we do here:

DataList.MoveFirst
Do Until DataList.EOF
    strText = strText & DataList.Fields.Item("ComputerName") & vbCrLf
    DataList.MoveNext
Loop

We’re simply taking a look at each record and then storing the value of the ComputerName field in the variable strText. Note how we do this: we set the value of strText to whatever happens to be in strText plus the value of the current ComputerName field plus a carriage return line feed (that’s what the vbCrLf constant represents). What we’ll end up with is, in memory, a variable named strText that holds this data:

atl-ws-01
atl-ws-02
atl-ws-03
red-ws-02
red-ws-02
sf-ws-01
sf-ws-02

Finally, we reopen our text file (for writing this time) and use the WriteLine method to replace the existing contents with the value of strText. And because the value of strText just happens to be our sorted list of computer names, we’ve managed to alphabetically sort the contents of C:\Scripts\Computers.txt.


How Can I Write a Script That Will Open a Text File and Shuffle the Words Found in That File?

Hey, Scripting Guy! My daughter has a flash-card style program that gets its list of words from a text file. How can I write a script that will open that file and automatically shuffle the list of words?

-- SN

Hey, SN. You know, for the most part we try to keep this column focused on practical system administration tasks: we tell you how to set default printers, how to disable services, and how to map network drives. Every now and then, however, we like to tackle a question that just sounds interesting, regardless of how useful the end result might be. Do system administrators routinely need to shuffle a list of words in a text file? Probably not. But, hey, all work and no play makes Jack a dull boy, right?

Actually, Jack’s kind of dull even when he does play. But that’s a different story.

With that in mind, let’s take a look at a script that can shuffle the words in a text file. Like we said, this might not be the most practical of tasks, but it is a bit of a challenge, and it does require us to use a few interesting little scripting techniques. And you never know when techniques like those might come in handy.

To begin with, let’s assume you have a text file that looks something like this, with all the words in alphabetical order:

Apple
Banana
Carrot
Dog
Fish
Elephant
Giraffe
Horse

How can we shuffle these words around? By using a script just like this one:

Const ForReading = 1
Const ForWriting = 2

Set objDictionary = CreateObject("Scripting.Dictionary")
Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("c:\scripts\words.txt", ForReading)

i = -1

Do Until objFile.AtEndOfStream
    strLine = objFile.Readline
    objDictionary.Add strLine, strLine
    i = i + 1
Loop

objFile.Close

Dim arrWords()
Redim arrWords(i)
intWordsLeft = i

z = 0

Do While intWordsLeft >= 0
    Randomize
    rndWord = Int((intWordsLeft - 0 + 1) * Rnd + 0)
    intWordsLeft = intWordsLeft - 1
    colItems = objDictionary.Items
    strText = colItems(rndWord)
    arrWords(z) = strText
    z = z + 1
    objDictionary.Remove(strText)
Loop

Set objFile = objFSO.OpenTextFile("c:\scripts\words.txt", ForWriting)

For Each strItem in arrWords
    objFile.WriteLine strItem
Next

objFile.Close

Well of course it’s crazy-looking; it’s a crazy task (at least for system administrators). But, believe it or not, there’s some logic here, and we’ll walk you through that logic, step-by-step.

The first part of the script is actually pretty easy. We define a pair of constants – ForReading and ForWriting – that we’ll use when working with the text file. We then create a pair of objects: Scripting.Dictionary and Scripting.FileSystemObject. We’ll use the Dictionary object as a temporary storehouse for the words we read in from the text file; we’ll use the FileSystemObject to actually interact with that text file.

Following that we use the OpenTextFile method to open the file C:\Scripts\Words.txt for reading. (Note the use of the constant ForReading.) Next we create a counter variable named i and set the value to -1; we’re going to use this variable to keep track of the number of words in the file. Why do we start with i equal to -1 rather than 0? Well, we’re going to use i to set up an array, and because the first item in an array is always 0 (rather than 1) we need to start off at -1. When we read in our first word i will be set to 0, and – weird as it sounds – an array with a size of 0 means that there’s one item in that array.

Hey, we just report the news, we don’t make it up.

That brings us to this block of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.Readline
    objDictionary.Add strLine, strLine
    i = i + 1
Loop

What we’re doing here is reading the file line-by-line. For each line (and thus each word) in the file, we assign the value to a variable named strLine; we then use the Add method to add this value to our Dictionary object, in turn incrementing the value of i by 1. When we finish reading the file, all our words will be stored in the Dictionary object, and i will be equal to 7, which just happens to be the number of words in the file minus 1. (Why? Because an array with 8 items in it has an array size of 7.)

Don’t worry; this will all make sense at the end.

We hope.

After closing the file we initialize an array named arrWords, setting it to size i (representing the number of words in the text file minus 1). We also assign the value of i to a variable named intWordsLeft, which will let us know how many words remain to be shuffled. Finally, we set the value of variable z to 0; we’ll use z to populate an array with the shuffled words. Basically what we’re going to do is randomly grab a word from the Dictionary and then add that word to the array. Because the words are taken out of the Dictionary in random order, that will result in them being “shuffled” (stored in a different order) in the array.

Now comes the fun part. We set up a Do loop that runs until we’ve used up all the words in the Dictionary object. We then use these two lines of code to randomly pick a number between 0 and the number of items in the Dictionary object (or at least the actual number of items minus 1, because the first item in the Dictionary objects is also item 0):

Randomize
rndWord = Int((intWordsLeft - 0 + 1) * Rnd + 0)

After that we decrement the value of intWordsLeft by 1; that’s to keep track of the fact that we now have one less word to work with than we did before.

So what do we need this random number for? Well, what we’re going to do now is use this value to randomly grab a word from the Dictionary. We do that by creating a collection of Dictionary items and then storing the value of the randomly-selected item number in a variable named strText:

colItems = objDictionary.Items
strText = colItems(rndWord)

In other words, Banana is currently item 1 in our Dictionary. Suppose we got a 1 when we generated a random number. That means we’ll grab the value of item 1 out of the Dictionary; in turn, that means that the word Banana gets stored in the variable strText.

Does that make sense? Once we have randomly extracted a word from our Dictionary we need to store that value somewhere. To do that we use the array arrWords, simply making strText the first item in the array:

Words(z) = strText

How do we know this should be the first item? Because we’re assigning the value to item z, and item z is equal to 0. After doing that, we immediately increment z by 1, meaning z will be equal to 1. That means that, the next time through the loop, we’ll assign the retrieved value to the second item in the array.

Having used the word Banana our next step is to remove that word from the Dictionary; otherwise we could end up using it a second time. To remove this word we simply call the Dictionary object’s Remove method, passing the variable strText as the item to be removed:

objDictionary.Remove(strText)

When all is said and done, the array arrWords will consist of a shuffled list of words taken from the text file:

Banana
Elephant
Giraffe
Apple
Fish
Carrot
Horse
Dog

Nice, huh? All we do then is reopen the file Words.txt (this time for writing), and replace the existing contents with the shuffled list arrWords:

Set objFile = objFSO.OpenTextFile("c:\scripts\words.txt", ForWriting)

For Each strItem in arrWords
    objFile.WriteLine strItem
Next

objFile.Close

The next time your daughter (or anyone’s daughter, for that matter) runs the educational program, she’ll be presented with the words in random fashion.

Incidentally, there’s no truth to the rumor that we tested this script on the text of this column. The words in a Hey, Scripting Guy! column aren’t chosen randomly; each word is the result of hours of painstaking research and craftsmanship.

In fact, it’s only after we do all that careful craftsmanship that our editor randomly shuffles all the words. You’d be amazed how good this column is before it gets edited! (Editor’s Note: You would amazed be. use wouldn’t But word the I “good.”)


How Can I Write Double Quotes to a Text File?

Hey, Scripting Guy! I’m using the FileSystemObject to create an XML file. However, I need to put double quote marks around some of the items in that file. How do I do that?

-- JP

Hey, JP. According to your email, you’re trying to write the following line to your XML file:

<? xml version = "1.0" encoding = "UTF-8" ?>

However, you’re getting hung up on the double quotes that surround 1.0 and UTF-8. Double quotes can be a major nuisance when trying to write data to text files (and an XML file, of course, is just a text file); that’s because double quotes are used to, among other things, indicate where strings begin and end. A line of code like this is bound to end in failure:

objFile.WriteLine "<? xml version = "1.0" encoding = "UTF-8" ?>"

Why? Well, like we said, double quotes indicate the beginning and ending of strings. As far as our script is concerned, your string actually consists of this: "<? xml version = ". In other words, the string consists of everything embedded inside the first set of double quotes. Thus the script reads along, sees "<? xml version = " and assumes that’s the end of the string. However, you have some additional text following that string: 1.0" encoding = "UTF-8" ?>". That’s not a valid scripting command, and VBScript has no idea what that means. As a result, it simply throws up its hands in despair, and the script fails.

To get around this we need to find a different way to specify that double quotes should be written to the text file. There are a couple different ways to do this, but we’re going to focus on just one (although we’ll briefly show you the other at the end of this column): we’re going to use VBScript’s Chr function to represent double quotes. As you probably know, all the characters you can type on the keyboard are represented by an ASCII value; for example, double quotes have an ASCII value of 34. If we want to insert double quotes into a file we can do this by specifying Chr(34); the Chr function takes the ASCII value – 34 – and converts it to an actual character (in this case double quotes).

Trust us; it’s much easier than it sounds. Here’s a script that writes the desired line to the file C:\Scripts\Test.xml:

Set objFSO =
CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\test.xml")

objFile.WriteLine "<? xml version = " & chr(34) &
"1.0" & chr(34) & _
" encoding = " & chr(34) &
"UTF-8" & chr(34) & " ?>"

objFile.Close

We begin by creating an instance of the FileSystemObject and then calling the CreateTextFile method to create the file C:\Scripts\Test.xml. That brings us to the line of code that writes the desired line to the file:

objFile.WriteLine "<? xml version = " & chr(34) & "1.0" & chr(34) & _
    " encoding = " & chr(34) & "UTF-8" & chr(34) & " ?>"

Yes, it looks weird, but it’s really not that bad. All we’re doing is calling the WriteLine method, and asking WriteLine to do the following.

First, we need it to compose the string to be written, starting with the characters <? xml version = . (Note the space after the equal sign.) After that, tack on the character with the ASCII value of 34. That happens to be this: ". Our string starts out looking like this:

<? xml version = "

Of course, we’re not done yet. Next, we add the characters 1.0 followed by another set of double quotes. That gives us a string that looks like this:

<? xml version = "1.0"

Can you see where this is all headed? We add the phrase encoding = (again, note the blank space; we need that to ensure that our completed string is spaced properly), followed in rapid-fire succession by double quotes, the characters UTF-8 and another set of double quotes. In other words:

<? xml version = "1.0" encoding = "UTF-8"

Now we just need to add the final chunk of characters: ?>". With the string fully composed, WriteLine will write the following to the XML file:

<? xml version = "1.0" encoding = "UTF-8" ?>

Make sense? All we did is take the string we want to write and surround it with double quotes:

"<? xml version = "1.0" encoding = "UTF-8" ?>"

As we know, that’s not valid VBScript syntax, so we then replace each set of embedded quotes with & Chr(34) & (and making sure that all the other characters are surrounded by double quotes). For example, here we’ve replaced the double quotes surrounding 1.0:

"<? xml version = " & Chr(34) & "1.0" & Chr(34) & " encoding = "UTF-8" ?>"

And here we’ve replaced the double quotes surround UTF-8:

"<? xml version = " & Chr(34) & "1.0" & Chr(34) & " encoding = " & Chr(34) & "UTF-8" & Chr(34) & " ?>"

To test this construction we can echo the finished product to the screen:

Wscript.Echo "<? xml version = " & Chr(34) & "1.0" & Chr(34) & " encoding = " & _
    Chr(34) & "UTF-8" & Chr(34) & " ?>"

Give it a try and see what happens.

We mentioned a second method of writing quotes to a text file. You can perform this same trick by doubling up your quote marks:

Wscript.Echo "<? xml version = ""1.0"" encoding = ""UTF-8"" ?>"

This works just fine and, in this example, is easier than using Chr(34). The only reason we don’t recommend doubling up your quote marks is that, later on, you can get yourself into situations that look like this, with single and double quote marks intermingled (something that can happen quite a bit when writing more-complicated WMI queries):

""'" test "'""

It’s next-to-impossible to look at code like that and figure out what’s what. We like Chr(34) because it makes the scripts much easier to read. But if you prefer double double quotes, this script will also work:

Set objFSO =
CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("C:\Scripts\test.xml")

objFile.WriteLine "<? xml version = ""1.0""
encoding = ""UTF-8"" ?>"

objFile.Close


How Can I Add a Site to Internet Explorer’s Restricted Sites Zone?

Hey, Scripting Guy! How can I add a site to Internet Explorer’s Restricted Sites zone on a remote computer?

-- NS

Hey, NS. When it comes to scripting, Internet Explorer has a sort of dual personality. If you want to program the application itself, well, you came to the right place: Internet Explorer’s document object model is wide open, and lends itself to doing all sorts of interesting things, things like creating a graphical user interface for your scripts. (For examples, see the new HTA Developers Center.)

When it comes to managing Internet Explorer, however, it’s a different story, especially if you’re hoping to manage multiple computers across the enterprise. There are a number of Group Policy options for managing Internet Explorer, but very few scripting options for managing Internet Explorer; Internet Explorer doesn’t have a full-fledged WMI provider nor does it expose a COM object that enables you to configure administrative settings. At first glance script writers would appear to be out of luck.

Ah, but first glances can be deceiving. As it turns out, most of Internet Explorer’s configuration settings are stored in the registry, and stored in a manner accessible to scripting. We can’t use a WMI provider to add a site to the Restricted Sites zone and we can’t use an Internet Explorer COM object to add a site to the Restricted Sites zone. But that’s OK: instead we’ll just use a WMI script to modify the registry and add the site that way.

Zone information for Internet Explorer can be found in the following portions of the registry:

· HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\

· HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\

As you might expect, information found in HKEY_CURRENT_USER applies only to the current user (and can vary from user to user) while information found in HKEY_LOCAL_MACHINE applies to everyone who logs on to that computer. You can add a Restricted Site to either location; the one consideration is whether you want that restriction to apply to everyone who uses the computer or only the current user.

So how do we add a site to a security zone? Well, we’re going to need to do the following:

· Create a registry key. For example, to add the fabrikam.com site to a security zone for only the current user, you need to create a registry key named fabrikam.com under HKEY_CURRENT_USER\ Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\.

· Create a DWORD registry value. The name of the registry value represents the protocols being managed from that site. Because we are talking about the Restricted Sites zone you will likely want to name the registry value *; that blocks all protocols from the site. Alternatively, you could name the value, say, http, which would block only the HTTP protocol.

· Assign the registry value the appropriate Internet Explorer security zone. Each Internet Explorer security zone is represented by a number. The Restricted Sites zone is number 4; here’s a list including other zones:

Zone | Value
Intranet | 1
Trusted Sites | 2
Internet | 3
Restricted Sites | 4

In the registry a restricted site looks something like this:

What do you mean you’re bored? We were just about to show you a script that adds fabrikam.com to the Restricted Sites zone:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\" _
    & "ZoneMap\Domains\fabrikam.com"
objReg.CreateKey HKEY_CURRENT_USER,strKeyPath

strValueName = "*"
dwValue = 4
objReg.SetDWORDValue HKEY_CURRENT_USER,strKeyPath,strValueName,dwValue

The script begins by creating a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we do this to indicate which portion of the registry we want to modify. We then connect to the WMI service and to the standard registry provider (root\default:StdRegProv).

Next we specify the registry path for our new registry key; note the fabrikam.com tacked on the end:

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\" _
    & "ZoneMap\Domains\fabrikam.com"

After specifying the path we then call the CreateKey method to create the new registry key.

Now that we have a fabrikam.com registry key we need to add a registry value. In this case we create a registry value named * with a value of 4; that will add fabrikam.com to the Restricted Zones site and will block all Internet protocols. We then call the SetDWORDValue method to create our new registry value. If you fire up Internet Explorer and check the Restricted Sites zone you should see fabrikam.com in the list of restricted sites:

Note that our sample script is designed to work on the local computer, but it can easily be modified to work on a remote computer; just assign the variable strComputer the name or IP address of that remote machine.


How Can I Add a Web Site to the Trusted Sites Zone?

Hey, Scripting Guy! How can I add a Web site to the Trusted Sites zone in Internet Explorer?

-- NR

Hey, NR. As it turns out, trusted sites are actually stored in the registry; consequently, adding a Web site is simply a matter of creating and configuring a new registry key. For example, suppose you want to trust Microsoft.com. (And, really, who doesn’t want to trust Microsoft?) Here’s a script that adds Microsoft.com to the list of trusted sites:

On Error Resume Next

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")
strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\" _
    & "ZoneMap\Domains\microsoft.com"

objReg.CreateKey HKEY_CURRENT_USER, strKeyPath

strValueName = "http"
dwValue = 2

objReg.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

Let’s talk about what’s going on here. We begin by creating a constant HKEY_CURRENT_USER and setting it to the value &H80000001. This constant will be used to access the HKEY_CURRENT_USER portion of the registry and configure Microsoft.com as a trusted site for only the logged-on user. What if you wanted to configure Microsoft.com as a trusted site for anyone logging on to the computer? In that case, substitute the constant HKEY_LOCAL_MACHINE for HKEY_CURRENT_USER, and assign HKEY_LOCAL_MACHINE the value &H80000002.

Next we connect to the WMI service and, more specifically, to the Standard Registry Provider. We then assign the following registry path to the variable strKeyPath:

Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\microsoft.com

Note the tail-end of the path: that’s where we put microsoft.com, the name of the Web site to be added to the trusted sites. We then call the CreateKey method to create a new registry key (microsoft.com) inside Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains.

Still with us? Having created the registry key, we simply need to create and configure a single registry value. In our sample script, we assign the value http to the variable strValueName. This will be the name of our new registry value; it also indicates which Internet protocols will be trusted from Microsoft.com. If we want to trust only the ftp protocol, then we’d assign strValueName the value ftp; if we want to trust all Internet protocols, then we’d assign strValueName the value * (a single asterisk).

We then assign the value 2 to the variable dwValue. In the world of Internet Explorer, the 2 represents the Trusted Sites zone. You could also use the value 1 to assign a site to the Intranet Sites zone; the value 3 to assign a site to the Internet Sites zone; or the value 4 to assign a site to the Restricted Sites zone.

Finally we use the SetDWORDValue method to create our new registry value. Fire up Internet Explorer, click on Tools, click on Internet Options, and then, on the Security tab, select Trusted Sites and click the Sites button. You should see Microsoft.com among the trusted sites.


How Can I Assign Multiple Home Pages to Internet Explorer 7.0?

Hey, Scripting Guy! How can I configure Internet Explorer 7 so it uses multiple home pages?

-- PI

Hey, PI. Just a second; before we begin, is anyone from Microsoft reading today’s column? Don’t be shy; raise your hand if you’re from Microsoft. No one, not a single Microsoft employee is reading today’s column? Excellent.

What difference does it make if someone from Microsoft reads today’s column? Well, all things considered, the Scripting Guy who writes this column would just as soon that no one he works with knows the horrible truth: the Scripting Guy who writes this column doesn’t actually use Internet Explorer 7. That’s right; he still uses Internet Explorer 6. Why? No real reason; he just likes Internet Explorer 6 better.

Like we said, though, that’s just between us. (Although it could be worse; he could be using Firefox or some other non-Microsoft browser.)

Anyway, because he still uses Internet Explorer 6 (and drives a Model T, and listens to music on a gramophone, and …) the Scripting Guy who writes this column didn’t even know that you could configure Internet Explorer 7 to use multiple home pages. As it turns out, however, you can. Which actually makes sense: multiple tabs means that you can open up multiples pages at once. So why not multiple home pages, pages that automatically load up, each on a separate tab, every time you start Internet Explorer?

And the best part of all? Writing a script to assign multiple home pages to Internet Explorer 7 is as easy as, well, as easy as this:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Internet Explorer\Main"
ValueName = "Start Page"
strValue = "http://www.microsoft.com/technet/scriptcenter/default.mspx"

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

ValueName = "Secondary Start Pages"

strValue1 = "http://www.microsoft.com/technet/scriptcenter/resources/qanda/default.mspx"
strValue2 = "http://www.microsoft.com/technet/scriptcenter/resources/begin/default.mspx"
arrValues = Array(strValue1, strValue2)

objReg.SetMultiStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, arrValues

As you can probably tell from glancing at the code, in order to configure multiple home pages for Internet Explorer 7 we need to modify the registry; in particular, we need to modify the following two registry values:

· HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Start Page. This is the spot where we configure the primary home page, the one that shows up in the topmost tab in Internet Explorer. If this registry location sounds familiar to you, it should: this is the same registry value used to configure the home page in previous versions of Internet Explorer.

· HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Secondary Start Pages. This is a brand-new registry value, and just happens to be the spot where we configure the rest of our home pages. (Or, as Internet Explorer 7 terms them, “secondary home pages.”)This registry value doesn’t exist by default; if it did, then you’d already have multiple home pages. But don’t worry; creating this new registry value is nowhere near as difficult as you might think it would be. (Well, unless, of course, you don’t think it would be difficult at all.)

OK, so much for what we’re going to do; let’s talk about how we’re going to do it. To begin with, we define a constant named HKEY_CURRENT_USER and assign it the value &H80000001; we’ll use this constant to tell the script which registry hive we want to work with. (You’re right: we do want to work with the HKEY_CURRENT_USER hive.That’s because the Internet Explorer home page is configured on a per-user basis.) We then connect to the WMI service on the local computer, taking care to bind to the StdRegProv class which, unlike most of the WMI classes we work with, is found in the root\default namespace:

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Note. Could we run this same script against a remote computer? Of course we can; what fun would it be if we couldn’t? To configure multiple home pages on a remote computer, just assign the name of that computer to the variable strComputer, like so:

strComputer = "atl-ws-01"

After we connect to the WMI service, our next task is to assign values to three variables:

· strKeyPath. This is the path (within the HKEY_CURRENT_USER hive) to the registry value we want to modify. In this case, that’s SOFTWARE\Microsoft\Internet Explorer\Main.

· ValueName. The name of the registry value to be modified. For this script, that’s Start Page.

· strValue. The value to be assigned as the primary home page. Needless to say, we opted for the page that all kind and decent people use as their home page: http://www.microsoft.com/technet/scriptcenter/default.mspx.

So then how do we actually make the Script Center home page the Internet Explorer home page? Like this:

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

As you can see, all we’re doing here is calling the SetStringValue method, passing the registry hive (the constant HKEY_CURRENT_USER), the registry path (strKeyPath), the registry value name (ValueName), and our new home page (strValue) as the method parameters.

Well, that was easy, wasn’t it? Now let’s see what we can do about those secondary home pages.

As we noted earlier, the URLs for secondary home pages (all of them) are stored in a registry value named Secondary Start Pages. That means the first thing we need to do is change the value of the variable ValueName:

ValueName = "Secondary Start Pages"

Note. How come we don’t change the value of the variable strKeyPath? There’s a very good reason for that: we don’t need to. That’s because Secondary Start Pages also resides in the registry key SOFTWARE\Microsoft\Internet Explorer\Main.

At this point, things get a tiny bit tricky. (But just a tiny bit.) The Secondary Start Pages registry value needs to have the data type REG_MULTI_SZ; that’s because this key (if needed) has to be able to hold multiple values. Fortunately, that’s no big deal; that simply means that any value we assign to this registry key must be passed as an array. That explains what this block of code is for:

strValue1 = "http://www.microsoft.com/technet/scriptcenter/resources/qanda/default.mspx"
strValue2 = "http://www.microsoft.com/technet/scriptcenter/resources/begin/default.mspx"
arrValues = Array(strValue1, strValue2)

In our first line of code, we’re assigning the URL for the Hey, Scripting Guy! column to a variable named strValue1. Why? Because we want Hey, Scripting Guy! to be one of our secondary home pages. In line 2, we assign the URL for the Sesame Script column to a variable named strValue2. Why? Because – oh, right. Guess you didn’t need our help to figure that out, did you?

Note. Can you create more than 2 secondary home pages? Yes you can, although, to be honest, we’re not sure what the limit is, or even if there is a limit.

That brings us to line 3, where we call VBScript’s Array function and add our two variables (strValue1 and strValue2) to an array named arrValues.

After we’ve done that we’re ready to call the SetMultiStringValue method and assign these two URLs to the registry value Secondary Start Pages. That’s what we do here:

objReg.SetMultiStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, arrValues

What’s that? Didn’t we say that, by default, the registry value Secondary Start Pages doesn’t exist? Yes, we did say that. But, as we also said, that’s no big deal. If this registry value exists then SetMultiStringValue will simply overwrite it; if the registry value doesn’t exit then SetMultiStringValue will first create it, and then assign it a new value. Talk about a win-win situation, huh?

At any rate, that should do it, PI. We’re sure that today’s column has generated even more questions about working with Internet Explorer 7; for example, no doubt some of you are wondering how to delete these secondary start pages, or maybe how to delete one of the secondary start pages (while leaving the other alone). We’ll address some of these questions in the very near future, albeit not today. But that’s all right; after all, you need some time to play around with the script that assigns multiple home pages. And the Scripting Guy who writes this column needs some time to actually install and start using Internet Explorer 7.

Although, if anyone asks, he’s been using it all along.


How Can I Change the Internet Explorer Home Page?

Hey, Scripting Guy! Can I change the Internet Explorer home page by using a script?

-- AH

Hey, AH. It’s Monday morning, and after a hard weekend of sitting around watching football, we decided to take it easy this morning. Yes, you can change the Internet Explorer home page by using a script; all you have to do is write a WMI script that modifies the HKCU\ SOFTWARE\Microsoft\Internet Explorer\Main\Start Page registry value:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Internet Explorer\Main"
ValueName = "Start Page"
strValue = "http://www.microsoft.com/technet/scriptcenter/default.mspx"
objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

Boy, when we said we were going to take it easy this morning, we weren’t kidding, were we? As you can see, there’s not much to this script. We start by defining the constant HKEY_CURRENT_USER, setting the value to &H80000001; as we’ve noted in previous columns, this value tells our script to work with the HKCU portion of the registry. We then connect to the WMI service; note that the class used to modify registry values – StdRegProv – is in the root\default namespace (which makes it different from the hundreds of WMI scripts you’re probably most familiar with, almost all of which connect to the root\cimv2 namespace).

After that, we assign the registry path and the registry value to a pair of variables (strKeyPath and ValueName, respectively). We then assign our new home page -- http://www.microsoft.com/technet/scriptcenter/default.mspx -- to the variable strValue. Once we get all these variables set, we then call the SetStringValue method to actually change the registry and, in turn, change the Internet Explorer home page. (As you no doubt figured out, each time Internet Explorer starts up, it checks HKCU\ SOFTWARE\Microsoft\Internet Explorer\Main\Start Page to determine the home page.)

The end result: a script that sets the home page of the current user to the TechNet Script Center. (Yeah, we know: that is kind of silly. After all, who doesn’t already have their home page set to the TechNet Script Center?)

Incidentally, Internet Explorer makes extensive use of the registry; in turn, that means you can easily write scripts that configure Internet Explorer settings. If you just can’t wait to do this, check out the Tweakomatic utility, which includes scores of scripts useful in managing Internet Explorer. Or, just sit tight for a couple weeks and wait until we get all those scripts added to the Script Repository.

Why don’t we just add all those scripts today? Didn’t we mention that it was Monday? We’ll get them up soon.

By the way, if all you want to do is determine the current home page configured for a user, try this script:

On Error Resume Next

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")
strKeyPath = "SOFTWARE\Microsoft\Internet Explorer\Main"
ValueName = "Start Page"
    objReg.GetStringValue HKEY_CURRENT_USER, strKeyPath, ValueName, strValue

If IsNull(strValue) Then
    Wscript.Echo "The value is either Null or could not be found in the registry."
Else
    Wscript.Echo strValue
End If


How Can I Change the Internet Explorer Title Bar?

Hey, Scripting Guy! I recently signed up with a new Internet service and now every time I start Internet Explorer the window title says Internet Explorer Provided by Fabrikam.com. How can I change the Internet Explorer title bar?

-- AD

Hey, AD. You know, you’ve presented the Scripting Guys with a moral and ethical dilemma. Just about the same time we received your email we got a second email that read something like this:

“Hey, Scripting Guy! I know that a lot of companies have configured Internet Explorer so that each time their users start Internet Explorer the window title says Internet Explorer Provided by Fabrikam.com, and I’d like to do that too. How can I change the Internet Explorer title bar?”

So is it a good thing to have a title bar that says Internet Explorer Provided by Fabrikam.com or is it a bad thing to have a title bar that says Internet Explorer Provided by Fabrikam.com? Beats the heck out of us. But, then again , as Scripting Guys it’s not really our job to tell you which actions are good and which actions are evil. It’s just our job to provide people with scripts that can change the Internet Explorer title bar:

Const
HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objReg = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Internet Explorer\Main"
strValueName = "Window Title"
strValue = "The Scripting Guys"

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, strValue

By default Internet Explorer displays the page title for any Web page it opens (or the URL if the page doesn’t have a title). The title is then followed by the phrase Microsoft Internet Explorer. To change this to some other phrase (or to delete the appended phrase altogether) you need to modify the registry value HKCU\Software\Microsoft\Internet Explorer\Main\Window Title. For example, after setting the value of Window Title to The Scripting Guys Internet Explorer will look like this:

Cool, huh? If you change Window Title to Internet Explorer Provided by Fabrikam.com, then that’s the phrase that will appear after the page title. If you change the value to an empty string (“”) then the only thing that will appear in the Internet Explorer title bar is the page name:

Note. What if you delete the registry value? In that case, Internet Explorer automatically reverts back to the default phrase Microsoft Internet Explorer.

Now that we know how the title bar works, how does the script work? Well, it starts off by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this tells the Standard Registry Provider (the WMI object we use to modify the registry) which registry hive we want to work with. We then use this line of code to bind to the Standard Registry Provider on the local computer:

Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

And yes, unlike Windows Script Host’s registry methods, you can use the Standard Registry Provider to read from and write to the registry on a remote computer. All you have to do is assign the name of that remote machine to the variable strComputer.

After we make the connection we then assign values to three different variables:

· strKeyPath is the the registry key where Window Title is found. In this case, that’s Software\Microsoft\Internet Explorer\Main.

· strValueName is the name of the registry value we want to modify (Window Title).

· strValue is the new value to be assigned to Window Title. Because Window Title has a data type of REG_SZ, strValue must be a string value. As we noted earlier, setting strValue to an empty string (“”) will result in no additional verbiage being added to the Internet Explorer title bar.

To actually change the registry value all we have to do is call the SetStringValue method, passing our constant and three variables (in this order):

objReg.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, strValue

And there you have it. If Window Title doesn’t exist, no problem: SetStringValue will first create the new registry value and then assign it the desired string.

That’s all we can do with this; from here on it’s up to each of you to decide how to best use this new-found knowledge. Choose wisely.


How Can I Change the Size of the Temporary Internet Files Folder?

Hey, Scripting Guy! How can I change the amount of disk space set aside for Internet Explorer’s temporary files folder?

-- AD

Hey, AD. The first draft of this column we were able to bang out in no time; after all, it consisted entirely of this:

Beats us.

But after discussing how valuable an answer like that would be to our customers, and after realizing that this is the month when we’re supposed to write up our performance reviews, we decided maybe we’d look into this a little further. And although it took a little bit of research, we were able to come up with an answer. (Something you can bet we’ll feature very prominently on our performance review, right after the note that, “At least this year we didn’t set anything on fire.” Between those two items, this might be our best review yet.)

In case you aren’t sure what AD is referring to, start up Internet Explorer and select Internet Options from the Tools menu. In the Internet Options dialog box, on the General tab, click Settings. You should see a dialog box similar to this:

What AD would like to do is write a script that can change the Amount of disk space to use value (which, in this example, is set to 447 MB). We have good news for you, AD. You don’t have to write this script; we’ve already written it for you:

Const
HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet
Settings\5.0\Cache\Content"
strValueName = "CacheLimit"
dwValue = 358400
objRegistry.SetDWORDValue HKEY_CURRENT_USER,strKeyPath,strValueName,dwValue

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet
Settings\Cache\Content"
strValueName = "CacheLimit"
dwValue = 358400
objRegistry.SetDWORDValue HKEY_CURRENT_USER,strKeyPath,strValueName,dwValue

As it turns out, the amount of disk space set aside for temporary Internet files is regulated by two registry values:

· HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\Cache\Content\CacheLimit

· HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Cache\Content\CacheLimit

To change the size from 447 MB all we have to do is change the value of these two registry entries.

And that’s exactly what our script does. The script begins by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use that later on to tell the script that we want to work with the HKEY_CURRENT_USER portion of the registry. We then connect to the WMI service on the local computer, and, more specifically, to the StdRegProv class.

The next three lines of code are used to assign values to three different variables:

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\Cache\Content"
strValueName = "CacheLimit"
dwValue = 358400

The variable strKeyPath represents the path within the HKEY_CURRENT_USER portion of the registry; strValueName represents the registry value we want to change (in this case, CacheLimit). We use the variable dwValue to indicate the new size for the Temporary Internet Files folder. It’s important to note that, although the disk space amount is shown in megabytes in the Internet Explorer UI, the value stored in the registry is stored as kilobytes. That means we need to do a little math here. For example, suppose we want to set the disk space amount to 350 megabtes. In that case, we must multiply 350 by 1024; that will give us 358400 kilobytes. And that’s the value we assign to the variable dwValue. To set aside 238 megabytes multiply 238 by 1024 and assign that value (243712) to dwValue.

To actually change the value in the registry all we have to do is call the SetDWORDValue method, passing the appropriate variables as parameters:

objRegistry.SetDWORDValue HKEY_CURRENT_USER,strKeyPath,strValueName,dwValue

We then repeat the process for the second registry value. The next time you start Internet Explorer you should see that the amount of disk space set aside for temporary Internet files has changed:

Now if only we could write a script that would go in and change our performance review scores as easily as this script changes the Internet Explorer cache size….


How Can I Clear the List of URLs That Appear in the Internet Explorer Address Bar?

Hey, Scripting Guy! How can I clear the list of URLs that appear in the Internet Explorer address bar?

-- GA

Hey, GA. You know, if you’ve ever pondered the question, “What is it that makes the Scripting Guys different from other Microsoft employees?” well, we can save you some time and trouble by just giving you the answer: the Scripting Guys are dumber than other Microsoft employees. For example, it turns out that the list of URLs that appear in the Internet Explorer address bar drop-down list are stored in the registry; to be more specific, they are stored in this registry key:

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\TypedURLs

The obvious implication? Delete the values found in that registry key and you’ll delete the list of URLs that appear in the Internet Explorer address bar.

Of course, most Microsoft employees know how dangerous it is to mess around with the registry. For example (and we’re not making this up), when we wrote the Microsoft Windows 2000 Scripting Guide we included a chapter on writing scripts that manipulate the registry. That was fine, but our editors at the time tried to make us include the following disclaimer after each and every script in the chapter:

“Changing the registry with a script can easily propagate errors. The scripting tools bypass safeguards, allowing settings that can damage your system, or even require you to reinstall Windows. It is recommended that you do not use scripts to change the registry.”

In other words, “Here’s a script that changes a value in the registry. Now don’t use it.”

But, like we said, the Scripting Guys are … different … than other Microsoft employees. Without even bothering to ponder the possible consequences we wrote the following script, one that deletes all the registry values in the TypedURLs registry key:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Internet Explorer\TypedURLs"

objRegistry.EnumValues HKEY_CURRENT_USER, strKeyPath, arrValueNames, arrValueTypes

For Each strValue in arrValueNames
    objRegistry.DeleteValue HKEY_CURRENT_USER,strKeyPath,strValue
Next

Was it a bad idea to do that on our actual work computer, as opposed to, say, running the script on a test machine? Probably. But all’s well that ends well: as near as we can tell, not only did this script clear the list of URLs from the address bar, but it didn’t cause any problems along the way, either. As usual, you run scripts that play around with the registry at your own risk, but this one seems to work just fine.

Let’s take a few minutes to walk through the script and talk about how it does work. To begin with, we define a constant named HKEY_CURRENT_USER and set the value to &H80000001; this simply tells the script which registry hive to work with. We then use these two lines of code to connect to the WMI service on the local computer:

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

And, yes, this script can be modified to work against a remote computer: all you have to do is assign the name of the remote machine to the variable strComputer. For example:

strComputer = "atl-ws-01"

So far, so good, right? After connecting to the WMI service we assign the registry path within HKEY_CURRENT_USER (Software\Microsoft\Internet Explorer\TypedURLs) to a variable named strKeyPath. That brings us to this line of code:

objRegistry.EnumValues HKEY_CURRENT_USER, strKeyPath, arrValueNames, arrValueTypes

As it turns out, each URL that appears in the address bar is stored as a separate registry value: url1, url2, url3, etc. What we’re doing here is using the EnumValues method to retrieve a collection of all the registry values found in the TypedURLs key. We give EnumValues four parameters:

· HKEY_CURRENT_USER, the costant that represents the registry hive.

· strKeyPath, the variable that indicates the registry key we want to work with.

· arrValueNames, an “out” parameter that EnumValues will fill with the names of all the registry values found in TypedURLs.

· arrValueTypes, a second out parameter that EnumValues will fill with the registry data type for each value in TypedURLs. For this particular script we don’t care about the data type, but because it’s a required parameter we need to include it.

In other words, after calling EnumValues we’ll get back a collection of all the registry values found in TypedURLs, a collection that will be stored in the variable arrValueNames. That means we can now easily delete each value: all we have to do is set up a For Each loop to walk through the collection and, within that For Each loop, use the DeleteValue method to delete each value. That’s what we do here:

For Each strValue in arrValueNames
    objRegistry.DeleteValue HKEY_CURRENT_USER,strKeyPath,strValue
Next

As you can see, DeleteValue takes three parameters: the constant HKEY_CURRENT_USER, the variable strKeyPath, and the variable strValue (which simply represents the registry value to be deleted.)

That’s all you have to do. After the registry values have been deleted no items will appear in the Internet Explorer address bar drop-down list. However, as you type new URLs in the address bar new registry values will automatically be created. Over time, you’ll eventually build up a brand-new collection of typed URLs, which you can then delete using this script, ensuring that the great circle of Internet Explorer life goes on.

Note. One little quirk to watch out for. Suppose you delete all the URLs, then open Internet Explorer and type a new address into the address bar. This address will be immediately added to the drop-down list; however, if you look in the registry you won’t see any new values in TypedURLs. For some reason these new URLs aren’t added to the registry until that instance of Internet Explorer has been terminated. If you close Internet Explorer and then check the registry you’ll see your new registry value.

Like we said, this seems to work just fine, despite the fact that many people would caution you against using a script to delete values from the registry. And now, if you’ll excuse us, someone down the hall needs to borrow a pair of scissors. We’re going to run some over to them right now.


How Can I Configure the History Setting in Internet Explorer?

Hey, Scripting Guy! How can I configure the Days to keep pages in history setting in Internet Explorer?

-- AK

Hey, AK. To tell you the truth, at first we were a little hesitant about answering this question. Why? Well, we’ve all seen enough TV to know that you aren’t supposed to mess around with history. Remember that episode of Futurama where Fry ended up becoming his own grandfather? We didn’t want that happening to any of us.

After giving it a little more thought, however, we realized that most of these TV shows dealt with regular old history; very few of them dealt specifically with Internet Explorer history settings. (Yes, it seems like a show about the history feature in Internet Explorer would be a real blockbuster, but apparently not.) And so we decided to take a chance and write the following script:

HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\URL History"
ValueName = "DaysToKeep"
dwValue = 25

objReg.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

As you can see, this script uses WMI to directly modify the registry; that’s pretty much the only way to manage Internet Explorer settings programmatically. Some people will tell you that it can be dangerous to directly modify the registry. That’s true; it can be. However, because all we’re doing is modifying the history setting in Internet Explorer, we don’t believe any great harm can arise from running this script. Besides, we’re assuming that the danger from running the script will be counterbalanced by the danger involved in doing anything to disrupt the space-time continuum and change the course of history.

The script begins by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this constant to tell the script which registry hive to work with. We then use these two lines of code to connect to the WMI service on the local computer:

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Can you use this same approach to connect to the WMI service on a remote computer? Of course you can, and there are two ways to do that. One way is to go back in time and grab the remote computer before anyone else can get it; that way, in this era, that machine will actually be your local computer. If that sounds like more trouble than it’s worth, however, you can also simply assign the name of the remote machine to the variable strComputer. For example, this code connects you to the WMI service on the remote computer atl-ws-01:

strComputer = "atl-ws-01"
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Next we need to assign values to three different variables:

· strKeyPath. This is the path – within the HKEY_CURRENT_USER registry hive – to the registry key housing the value to be changed. In our script, that path is equal to SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\URL History.

· ValueName. The name of the registry value to be changed. We want to change the Internet Explorer history setting, which happens to be stored in a value named DaysToKeep.

· dwValue. The new value to be assigned to DaysToKeep. We want history information to be maintained for 25 days, so we set the variable dwValue to 25.

All we have to do now is call the SetDWORDValue method, passing as method parameters the constant HKEY_CURRENT_USER and our three variables:

objReg.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

The change will take effect the next time you start Internet Explorer.

Important. Like we said, the change takes effect the next time you start Internet Explorer. If the next time you start Internet Explorer you also discover that you’re your own grandfather, well, don’t say we didn’t warn you.


How Can I Create a Shortcut That Opens Internet Explorer With the Address Bar Hidden?

Hey, Scripting Guy! How can I create a shortcut that opens Internet Explorer with the address bar hidden?

-- WA

Hey, WA. You know, any time a science fiction writer starts to run low on ideas he or she writes a story in which someone goes to bed and then wakes up in the morning to find that the world has completely changed overnight. Why? Because someone somewhere built a time machine, went back in time, and then did something that changed the future. (Yes, we know: time machines all include a warning sticker that tells people to avoid doing anything that might change the future. But nobody ever pays attention to those stickers.)

Something very similar seems to have happened here. When the Scripting Guy who writes this column went to bed last night, he knew that there was an easy and obvious answer to this question; in fact he was positive that there was an Internet Explorer startup switch that could be used for this very purpose. And yet, when he woke up this morning he discovered that there really wasn’t such a startup switch. Could he have been wrong? That’s a bit far-fetched, don’t you think? It’s far more likely that someone went back in time and removed the hide-the-address-bar startup switch from Internet Explorer. Disaster!

But have no fear; the Scripting Guys never allow some reckless time traveler to keep them from answering a question. Instead, we came up with a compromise solution, one that requires two items: a script, and a shortcut that calls that script.

Let’s start with the script:

Set objExplorer = CreateObject("InternetExplorer.Application")

strWebSite = Wscript.Arguments(0)

objExplorer.Navigate strWebSite
objExplorer.AddressBar = 0
objExplorer.Visible = 1

As you can see, there’s not much to this. We start out by creating an instance of the InternetExplorer.Application object; as the name implies, that’s going to give us a running instance of Internet Explorer that we can call our own. We then use this line of code to take the first argument supplied to our script and store it in a variable named strWebSite:

strWebSite = Wscript.Arguments(0)

Where’s that argument going to come from? We’ll explain that in just a minute.

Next we use the following block of code to: 1) open the Web site stored in the variable strWebSite; 2) hide the address bar; and 3) make Internet Explorer visible on screen:

objExplorer.Navigate strWebSite
objExplorer.AddressBar = 0
objExplorer.Visible = 1

Like we said, we couldn’t find a startup switch that would hide the address bar. However, if we programmatically start Internet Explorer we can hide the address bar simply by setting the AddressBar property to 0. So that’s what we did.

In case you’re interested, these lines of code hide the Internet Explorer toolbar and status bar, respectively:

objExplorer.ToolBar = 0
objExplorer.StatusBar = 0

All of that makes up the script called by our Internet Explorer shortcut. Now, let’s take a look at a second script, one that actually creates this shortcut:

Set objShell = CreateObject("Wscript.Shell")
strFolder = objShell.SpecialFolders.Item("Desktop")

Set objShortcut = objShell.CreateShortcut(strFolder & "\Open Web Site.lnk")
objShortcut.TargetPath = "c:\scripts\test.vbs"
objShortcut.Arguments = "http://www.microsoft.com/technet/scriptcenter"
objShortcut.Save

This is pretty simple little script, too. We start out by creating an instance of the Wscript.Shell object, the object used for creating shortcuts. Because we want this shortcut to appear on the desktop we then use this line of code to retrieve the path to the user’s desktop folder:

strFolder = objShell.SpecialFolders.Item("Desktop")

With us so far? Good. Our next step is to create the shortcut, something we do using the aptly-named CreateShortcut method:

Set objShortcut = objShell.CreateShortcut(strFolder & "\Open Web Site.lnk")

As you can see, all we need to do is pass CreateShortcut the file path for the new shortcut. Because we want this shortcut to appear on the desktop we create a path that combines the path to the desktop folder (a value stored in the variable strFolder), a \, and the shortcut file name: Open Web Site.lnk. In other words, we’re creating a shortcut with a path similar to this:

C:\Documents and Settings\Ken Myer\Desktop\Open Web Site.lnk

Next we need to assign values to two properties: TargetPath and Arguments. The TargetPath is the script we want to call whenever we use the shortcut; in this case that’s the script C:\Scripts\Test.vbs. The Arguments property, meanwhile, represents any command-line arguments that we want to pass to the script. Because we want this shortcut to open a link to the Script Center (http://www.microsoft.com/technet/scriptcenter) we assign the Script Center URL to the Arguments property.

Finally, we call the Save method to officially save our new shortcut to the file system.

Now for the exciting part: what’s going to happen when we use this shortcut? Well, the shortcut will call C:\Scripts\Test.vbs, passing the Script Center URL as the first (and only) command-line argument. Test.vbs will:

· Create an instance of Internet Explorer.

· Use the Navigate method to open the Script Center URL.

· Hide the address bar.

· Appear on screen.

Give it a try and see for yourself.

Or, alternatively, get a time machine, go back and time, and make sure that a startup switch that enables you to hide the address bar is added to Internet Explorer. Whichever way is easier for you is fine by us.


How Can I Determine Which Version of Internet Explorer is Installed on a Computer?

Hey, Scripting Guy! How can I determine which version of Internet Explorer is installed on a computer?

-- MG

Hey, MG. Probably the best (and definitely the easiest) way to do this is to use WMI; after all, using WMI makes it as easy to get this information from remote computers as it is to get it from the local machine. Here’s a sample script that returns version information for Internet Explorer and – as an added bonus – returns the Product ID and the encryption level of Internet Explorer as well:

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

Set colIESettings = objWMIService.ExecQuery _
    ("Select * from MicrosoftIE_Summary")

For Each strIESetting in colIESettings
    Wscript.Echo "Version: " & strIESetting.Version
    Wscript.Echo "Product ID: " & strIESetting.ProductID
    Wscript.Echo "Cipher strength: " & strIESetting.CipherStrength
Next

For the most part this is just a regular old WMI script; the only tricky part lies in connecting to the WMI service. That’s because the class we need to work with – MicrosoftIE_Summary – happens to live in the root\cimv2\Applications\MicrosoftIE namespace. Make sure you spell that out completely; if you specify the root\cimv2 namespace (which is standard for most WMI scripts) the script will fail with a “Class not found” error.

Beyond that we simply query the MicrosoftIE_Summary class and echo back values for the Version, ProductID, and CipherStrength properties.

Although not well-known and hardly publicized, the root\cimv2\Applications\MicrosoftIE namespace dates back to at least Internet Explorer 5.00.2920.0000, which happens to be the version of Internet Explorer that shipped with Windows 2000. If you’re interested in other Internet Explorer values that can be retrieved using the classes found in this namespace, check out the scripts found in this section of the Script Center Script Repository.


How Can I Get a List of All My Internet Cookies?

Hey, Scripting Guy! How can I get a list of all my Internet cookies?

-- ZO

Hey, ZO. OK, we have to admit it: you tricked the Scripting Guys, and we fell for it, hook, line, and sinker. As soon as we saw your email – and the word “cookies” – there was a mad scramble to see who would get to answer this question. It was only after the dust settled and there was only one Scripting Guy left standing that we realized you were talking about Internet cookies rather than, say, chocolate chip cookies. Oh, well.

Note. Actually, that’s not true: usually when a new question is received there’s a cloud of dust alright, but it’s dust kicked up by all the other Scripting Guys running away as fast as they can so they won’t have to answer the thing. As usual, that leaves poor Jean – the Cinderella of the scripting world – to do all the work all by herself. But will you ever hear a single word of complaint from Jean? No, never. (Editor’s Note: Honest, all I changed in this paragraph were a couple of typos. Someone tried to spell Jean as G-r-e-g.)

Oh, sure, you might hear several thousand words of complaint. But never just a single word. (Editor’s Note: OK, maybe G-r-e-g was right.)

Admittedly, tallying up your Internet cookies isn’t half as much fun as tallying up your chocolate chip or oatmeal raisin cookies. On the other hand, it is easier to write a script that returns a list of all your Internet cookies:

Const HKEY_CURRENT_USER = &H80000001

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

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
strValueName = "Cookies"
objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strFolder

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strFolder & "'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile In colFileList
    Wscript.Echo objFile.FileName
Next

Before we get started we should point out that if all you want to do is retrieve the list of cookies found on your computer (or, more correctly, found in your user profile) then this script is perhaps a tad bit more complicated than it needs to be. That’s because we wanted a script that could retrieve information from a remote computer as well as from a local computer. If all you care about is the local machine, however, then this script works equally well:

Const COOKIES = &H21&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(COOKIES)
Set objFolderItem = objFolder.Self
Wscript.Echo objFolderItem.Path

Set colItems = objFolder.Items
For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

So what’s the difference between the two scripts? Well, the second script relies on the Shell.Application object to locate the Cookies folder. That’s easy for the Shell object to do because the Cookies folder happens to be a “special folder,” a folder whose location is well-known – and tracked by – the operating system. As you can see, we simply call the Namespace method (passing along the hex value that represents the Cookies folder) and then let the Shell object take it from there.

Unfortunately, though, the Shell object works against only the local computer. If you want to return cookie information from a remote machine you need to use WMI to determine the location of the Cookies folder on that computer, then let WMI retrieve the information for you. And that’s exactly what our first script does.

To do all of that the script starts out by defining a constant named HKEY_CURRENT_USER; we’ll use this later on to indicate the registry hive we want to work with. We then make two different connections to the WMI service. The first connection binds us to the WMI service itself:

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

Meanwhile, the second connection binds us to the System Registry provider:

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

Could we have done this by using (and then reusing) a single connection? Sure. We just thought it would be easier for people to follow the logic if we used one object reference named objWMIService to represent the WMI service and a second object reference (objRegistry) to represent our connection to the registry provider. But if you prefer to carry out this task using just one object reference, well, so much the better.

Our next step is to read the Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Cookies registry value in order to determine the location of the Cookies folder. To do that we start off by defining two variables: strKeyPath, which represents the registry path within the HKEY_CURRENT_USER hive; and strValueName, which represents the registry value we want to read (Cookies):

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
strValueName = "Cookies"

With our two variables defined we can then call the GetStringValue method to retrieve the folder location:

objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValueName,strFolder

As you can see, we pass GetStringValue four parameters: our HKEY_CURRENT_USER constant; strKeyPath; strValueName; and strFolder, an “out” parameter that the method will fill with the path to the Cookie folder (for example, C:\Documents and Settings\kenmyer\Cookies).

And once we have the folder location safely stashed away in the variable strFolder we can then use this crazy-looking query to return a collection of all the files found in that folder:

Set colFileList = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='" & strFolder & "'} Where " _
        & "ResultClass = CIM_DataFile")

The rest – as you no doubt know – is easy: we simply set up a For Each loop to walk through all the files (cookies) in the collection, echoing back the name of each one:

For Each objFile In colFileList
    Wscript.Echo objFile.FileName
Next

Piece of cake. Um, so to speak.

Incidentally, you will also see cookie files in the Temporary Internet Files folder as well as in the Cookies folder. In general, the cookies found in the Temporary Internet Files folder are pointers to the real cookies; that’s why emptying the Temporary Internet Files folder will often leave the cookies in place. If all you want to do is list the cookies then you can just focus on the Cookies folder and ignore anything in the Temporary Internet Files folder; most (if not all) of the cookies in the Temporary Internet Files folder will simply be duplicate pointer entries.

Now, as for those other cookies, well, feel free to send them to the following:

The Microsoft Scripting Guys
Microsoft Corporation
Building 43/2023
One Microsoft Way
Redmond, WA 98052-6399

We promise to, uh .. take care of … any chocolate chip, oatmeal raisin, or other non-Internet cookies we receive.


How Can I Make Internet Explorer Check for a New Version Each Time I Visit a Web Page?

Hey, Scripting Guy! How can I make sure that Internet Explorer 6 checks for a new version on each visit to a Web page?

-- MD

Hey, MD. In baseball lingo we’d call this a can of corn: a lazy fly ball we couldn’t possibly drop. This is a tricky little question but for some strange reason we just happened to know the answer. Thus a can of corn.

You know, you’re right: maybe we should do a column on baseball lingo:

Dear Baseball Lingo Guy! Are there any ways to describe how someone is pitching besides saying they’re pitching good or they’re not pitching good?
-- GS

Hey, GS. You bet there is. If a pitcher is getting hit – and hit hard – it’s likely because he’s throwing meat, or maybe watermelons. If he’s pitching good he might be throwing BBs or bullets. Or maybe he’s just bringing it or throwing smoke. Or maybe …

OK, fine. But if you change your mind, send those baseball lingo questions to scripter@microsoft.com.

So what about our scripting question? Well, just to make sure we’re all on the same page here, we’re talking about one of the Internet Explorer Temporary Internet Files settings. To access this through the Internet Explorer UI, click Tools, click Internet Options, and then, in the Internet Options dialog box, click Settings. You should see a dialog box that looks like this:

We’re talking about the four radio buttons at the top, the ones specifying when (or if) Internet Explorer will check for newer versions of cached Web pages.

So how can we script this? Well, it turns out that the setting for these radio buttons is stored in the registry, although in a somewhat less than intuitive location: HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\SyncMode5. If you change the value of SyncMode5, you’ll change the behavior of Internet Explorer when it comes to checking for new versions of stored pages. SyncMode5 can be set to one of four different values:

Setting | Value
Every visit to the page | 3
Every time you start Internet Explorer | 2
Automatically | 4
Never | 0

And because this is a simple DWORD registry value we can modify the setting (on remote computers as well as on the local computer) by using a WMI script just like this one:

Const
HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objRegistry = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet
Settings"
strValueName = "SyncMode5"
dwValue = 3
objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

The script begins by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this later on to tell WMI that we want to work with the HKEY_CURRENT_USER portion of the registry. We then connect to the WMI service on the local computer; we could also connect to the WMI service on a remote machine simply by assigning the name of that computer to the variable strComputer. Note that the WMI class we need to use – StdRegProv – is found in the root\default namespace. In comparison, most WMI system administration scripts use classes found in the root\cimv2 namespace.

We then assign values to three variables:

· strKeyPath, which is assigned the path to the registry key where SyncMode5 lives.

· strValueName, which is assigned the name of our target registry value (SyncMode5).

· dwValue, which is assigned the value 3. As we’ve already seen, if SyncMode5 is set to 3, then Internet Explorer will be configured to check for a newer version each time it visits a page.

From there we simply call the SetDWORDValue method, passing the appropriate constants and variables, and thus configuring Internet Explorer to check for a newer version each time it visits a page.

Like we also say in the baseball world, you can put this one in the book.


How Can I Periodically Refresh a Web Page?

Hey, Scripting Guy! How can I periodically refresh a Web page?

-- TZ

Hey, TZ. You know, the last couple of weeks have been very difficult for the Scripting Guys; after all, we were positive that one of us would be nominated to become the next Chief Justice of the United States Supreme Court. (We even bought the robes and everything, robes which – for reasons unknown – Peter won’t let us return.) Instead, we’re forced to sit and watch John Roberts answer questions about strict constructionism and the applicability of legal precedent. Talk about easy! We’d like to see John Roberts answer a question like “How can I periodically refresh a Web page?”

Oh, right: you’d probably like to see us answer a question like “How can I periodically refresh a Web page?” Okey-doke:

On Error Resume Next

Set objExplorer = CreateObject("InternetExplorer.Application")

objExplorer.Navigate "http://www.microsoft.com/technet/scriptcenter"
objExplorer.Visible = 1

Wscript.Sleep 5000

Set objDoc = objExplorer.Document

Do While True
    Wscript.Sleep 30000
    objDoc.Location.Reload(True)
    If Err <> 0 Then
        Wscript.Quit
    End If
Loop

The trick here is to use a script to start Internet Explorer, then have that script pass commands to the browser. To that end we begin by adding the On Error Resume Next statement (more on that later), and then create an instance of the InternetExplorer.Application object. We use the Navigate method to open the desired Web page (hey, what a coincidence: it’s the Script Center home page!) and then set the Visible property to 1. (Is that important? It is if we actually want to be able to see our instance of Internet Explorer.)

Next we use Wscript.Sleep to pause the script for 5 seconds (5,000 milliseconds). We do that to ensure that Internet Explorer is fully-loaded before proceeding; if Internet Explorer is not completely loaded our next line of code will likely fail, and that means our script will likely fail.

That next line of code, incidentally, creates an object reference to the Internet Explorer document object. That’s needed because, to refresh a Web page, we have to reload the current Internet Explorer document. That’s just the way the Internet Explorer object model works.

After creating this object reference we set up a Do While loop that will run forever. (Don’t worry; there’s an easy way to stop the script.) Inside that loop we call the Sleep method and pause the script for 30 seconds; that’s because we decided to refresh the Web page every 30 seconds. What if we wanted to refresh the page every two minutes? No problem; just use this line of code, which pauses the script for 120 seconds (120,000 milliseconds):

Wscript.Sleep 120000

When 30 seconds are up we call the Reload method to reload the current document. We pass Reload a single parameter – True – which tells Internet Explorer to reload the document from the Web server. Without this parameter, Reload would reload the document from the Internet Explorer cache, meaning we wouldn’t be getting the latest and greatest version of the Web page.

After calling the Reload method we then check the value of the Err object. If an error has occurred (and the prime culprit for that would be the fact that someone has closed browser window) we simply terminate the script. If no error has occurred we loop around, wait for 30 seconds, and then start all over again.

And yes, that is the easy way to end the script: just close the browser window. Do that and within 30 seconds the script will terminate.

So there you have it: a script that periodically refreshes a Web page. We’d like to see a Supreme Court nominee (or full-fledged justice, for that matter) write something like that.

Well, OK: we mean besides Ruth Bader Ginsburg.


How Can I Return a List of Downloaded Controls and Applets for Internet Explorer?

Hey, Scripting Guy! Is there an easy way to find out what programs have been downloaded for Internet Explorer (the items that are in the folder C:\Windows\Downloaded Program Files)? I can use a script to get the executable file names and dependent files, but I’d like to be able to get the names as they are shown in the folder itself.

-- AC

Hey, AC. When you download ActiveX controls or Java applets, these items are typically placed in the Downloaded Program Files folder. You can view these items in Windows Explorer, or you can view them from within Internet Explorer. (Click Internet Options and then, on the General tab, click Settings. In the Settings dialog box, click View Objects.)

When you look at the folder contents using Windows Explorer, you see friendly names like MSN File Upload Control. However, if you use the dir command or use, say, a FileSystemObject script to access the folder you see executable file names like MsnUpld.cab. You’d like to be able to use a script to return those friendly names you see in Windows Explorer.

So how do you do that? Why you use this script, of course:

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

Set colIESettings = objWMIService.ExecQuery _
    ("Select * from MicrosoftIE_Object")

For Each strIESetting in colIESettings
    Wscript.Echo "Code base: " & strIESetting.CodeBase
    Wscript.Echo "Program file: " & strIESetting.ProgramFile
    Wscript.Echo "Status: " & strIESetting.Status
    Wscript.Echo
Next

This script relies on a class – MicrosoftIE_Object – and a namespace -- root\cimv2\Applications\MicrosoftIE – that, as far as we know, have never been documented. But it seems to do the job; run the script and you’ll get output similar to this:

Code base: http://sc.groups.msn.com/controls/FileUC/MsnUpld.cab
Program file: MSN File Upload Control
Status: Installed

As you can see, the ProgramFile property gives you the friendly name as shown in the Downloaded Program Files folder.


How Can I Return the URLs Associated with My Internet Explorer Favorites?

Hey, Scripting Guy! How can I return the URLs associated with my Internet Explorer Favorites?

-- DW

Hey, DW. It’s time for annual performance reviews here at Microsoft, which means we Scripting Guys are supposed to be looking back and detailing all the many things we’ve done in the past year. Unfortunately, instead of finding things we did do it’s much easier for us to find things that we didn’t do. Take this question, for example. You’d think we would have already written a script that returns the URLs for all of your Internet Explorer Favorites, wouldn’t you? And yet, for some reason, we never have. We must have been too … busy ….

Or something.

On the bright side, if we hurry we can get this column in before this year’s review deadline. That way we’ll at least have one accomplishment for the year. That might not sound like much, but it’s one more accomplishment than we usually have.

With that in mind, let’s take a look at a script that returns the name and the associated URL for all the links in the Internet Explorer Favorites folder:

Const FAVORITES = &H6&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(FAVORITES)

For Each objItem in objFolder.Items
    If objItem.IsLink Then
        Set objLink = objItem.GetLink
        Wscript.Echo objItem.Name
        Wscript.Echo objLink.Target
        Wscript.Echo
    End If
Next

Gee; if we had known this script was going to be that easy we would have written it a long time ago. As you can see, there isn’t much to the code. We start out by creating a constant named FAVORITES and setting the value to &H6&; we’ll use this constant to tell the script which special folder (Internet Explorer Favorites) we want to connect to. Next we create an instance of the Shell.Application object, then use the Namespace method to bind to the Favorites folder. Notice that, by using this approach, we don’t have to know the actual path to the Favorites folder; the Shell object will go out and find the folder for us.

Our next step is to check all the items in the Favorites folder and then return the URL for any links found there. To accomplish that task we first set up a For Each loop to loop through all the items in the folder. (How do we get at that collection of items? They’re all part of the Items property for the folder object.) That’s what we do here:

For Each objItem in objFolder.Items

Inside the For Each loop we use the IsLink property to determine whether or not the item in question is a shortcut file. If IsLink is True we then use the GetLink method to retrieve the shortcut properties for the file. Complicated? No; that requires just two lines of code:

If objItem.IsLink Then
    Set objLink = objItem.GetLink

After retrieving the shortcut properties we then echo the shortcut Name and Target, with Target representing the URL associated with the shortcut. We then loop around and return the name and URL for the next link in the Favorites folder. That’s all there is to it.

Well, that’s a good point: as handy as this might be we probably still need something besides this script to put on our performance review. On top of that, the script shows only “top-level” links; that is, links found within the Favorites folder itself. Suppose you have subfolders within the Favorites folder. Will this script be able to open up those subfolders and retrieve information about the links found there? Well ….

But that’s OK; we can fix that. Here’s a revised script that uses a recursive subroutine to retrieve information from any subfolders found in the Favorites folder. We won’t discuss this script – or the idea of recursion – in any detail today; for more information on writing recursive functions or subroutines take a look at the Recursion section in the Microsoft Windows 2000 Scripting Guide. For now, we’ll simply point out that the script starts out exactly the way our previous script does: it binds to the Favorites folder and walks through the collection of items, checking to see if each item is a link.

In this revised script, however, after we check to see if each item in the folder is a link we then use the IsFolder property to determine whether or not that item happens to be a folder. If the item is a folder, we call a recursive subroutine named GetSubFolderLinks, passing the folder object as the sole parameter to the subroutine. Inside the subroutine we then use the GetFolder method to bind us to that subfolder.

Once we’ve made a connection to the subfolder we can then walk through the items found in there, retrieving the name and URL for any links, and even calling the GetSubFolderLinks subroutine should our subfolder contain sub-subfolders. We realize that’s a bit hard to grasp: how can we call the GetSubFolderLinks subroutine when we’re already in the GetSubFolderLinks subroutine? All we can say is this: that’s the whole idea behind recursive functions and subroutines. Take a look at the Scripting Guide for more information.

Or, just run the following script and don’t worry too much about it:

Const FAVORITES = &H6&

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(FAVORITES)

For Each objItem in objFolder.Items
    If objItem.IsLink Then
        Set objLink = objItem.GetLink
        Wscript.Echo objItem.Name
        Wscript.Echo objLink.Target
        Wscript.Echo
    End If
    If objItem.IsFolder Then
        GetSubFolderLinks(objItem)
    End If
Next

Sub GetSubFolderLinks(objItem)
    Set objSubFolder = objItem.GetFolder
    For Each objSubItem in objSubFolder.Items
    If objSubItem.IsLink Then
        Set objSubLink = objSubItem.GetLink
        Wscript.Echo objSubItem.Name
        Wscript.Echo objSubLink.Target
        Wscript.Echo
    End If
    If objSubItem.IsFolder Then
        GetSubFolderLinks(objSubItem)
    End If
    Next
End Sub

And hey, no need to thank us, DW: for the Scripting Guys, it’s all in a year’s work.

Um, all in day’s work ….


How Can I Switch Between Using a Proxy Server and Not Using a Proxy Server?

Hey, Scripting Guy! At work we need to use a proxy server to connect to the Internet; when I take my laptop home to work, however, I don’t need to use a proxy server. I know I can use the Internet Options in Control Panel to change between using a proxy server and not using a proxy server; is there any way to switch between the two using a script?

-- EA

Hey, EA. Let’s hope there’s a way to do this using a script; if there isn’t, this column will be much shorter than usual. (Of course, many people might argue that that wouldn’t be such a bad thing.) If you aren’t sure what EA is talking about, he’s talking about the bottom set of options (the options in the section labeled Proxy server) in the following dialog box:

This, by the way, is a fairly common scenario: lots of companies use proxy servers on their internal network, but few Internet Service Providers use proxy servers for their consumer networks. If you use a laptop computer both at home and at work, you’ll constantly find yourself switching between using a proxy server and not using a proxy server.

So how can we do this using a script? Well, as it turns out there are three registry values that control these settings. (Yes, from the dialog box it appears as though there are four settings that need to be controlled. Don’t worry; we’ll explain how this all works.) These three registry values – found in the HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings key in the registry – include:

· ProxyEnable. This DWORD value configures the Use proxy server for your LAN check box. If ProxyEnable is set to 1, then use of a proxy server is enabled (i.e., the check box is checked). If ProxyEnable is set to 0, then Internet Explorer will not use a proxy server.

· ProxyServer. This REG_SZ (string) value is used to configure both the Address and Port settings. To do this the proxy server address and port must be passed in this format: address:port. In other words, to set myproxy as the address of the proxy server and port 80 as the port you assign this value to ProxyServer: myproxy:80.

Incidentally, if you disable the use of a proxy server you do not have to clear this registry value; this value is used only when Internet Explorer is using a proxy server. This makes things nice for script writers: to disable the proxy server you only have to set the value of ProxyEnable to 0; you can leave both ProxyServer and ProxyOveride as-is.

· ProxyOverride. This REG_SZ value configures the Bypass proxy server for local addresses check box. To instruct Internet Explorer to bypass the proxy server for local addresses set this value to <local>. To instruct Internet Explorer not to bypass the proxy server for local addresses set the ProxyOverride value to 127.0.0.1.

Having said that, it’s possible that your ProxyOverride value might not be <local>. Before you write your script you might want to use Regedit to verify the values for Bypass proxy server for local addresses when enabled and when disabled.

Got all that? Now that we know the registry values that need to be configured (as well as how to configure them) the rest is as easy as writing a WMI script that enables the proxy server, specifies the proxy server address and port, and tells Internet Explorer to bypass the proxy server for local addresses:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings"

strValueName = "ProxyEnable"
dwValue = 1
objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

strValueName = "ProxyServer"
strValue = "svcproxy:80"
objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, strValue

strValueName = "ProxyOverride"
strValue = "<local>"
objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, strValue

The script starts off by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; we’ll use this constant later on to tell the script we want to work with the HKCU portion of the registry. We then connect to the WMI service, this time binding to the root\default namespace and the StdRegProv class (the WMI class used for reading and writing registry values).

After we make the connection we next assign the registry path (SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings) to a variable named strKeyPath; we then assign the registry value (ProxyEnable) and the configuration value (1) to the variables strValueName and dwValue. We now have all the information we need to call the SetDWORDValue method and change the value of ProxyEnable to 1:

objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

After that we repeat the same process for ProxyServer and ProxyOverride. Note, however, that both of these registry values have a REG_SZ data type; therefore we use the SetStringValue method instead of SetDWORDValue. (If you have no idea what we’re talking about when we say things like SetDWORDValue and SetStringValue then take a look at the Registry chapter in the Microsoft Windows 2000 Scripting Guide.)

As we noted earlier, disabling the proxy server is much easier: all we have to do is set the value of ProxyEnable to 0:

Const
HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objRegistry = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet
Settings"

strValueName = "ProxyEnable"
dwValue = 0
objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

In fact, after your proxy settings have all been configured (that is, after you’ve initially assigned values to ProxyServer and ProxyOverride), you can toggle back and forth between using a proxy server and not using a proxy server simply by configuring the ProxyEnable key. In other words, after you have a proxy address and port and after you’ve decided what you want to do with the ProxyOverride value then all you need to do is run this script to re-enable use of the proxy server:

Const
HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objRegistry = GetObject("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet
Settings"

strValueName = "ProxyEnable"
dwValue = 1
objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

To disable the proxy server just set the value of ProxyEnable to 0. It’s that easy.


How Can I Tell if ActiveX is Enabled in Internet Explorer?

Hey, Scripting Guy! How can I tell if ActiveX is enabled in Internet Explorer?

-- JV

Hey, JV. You just had to ask this question, didn’t you? Actually, this isn’t a particularly hard question to answer, it’s just a little complicated. But that has to do more with the way Internet Explorer is configured than it does with writing a script to retrieve this information.

To begin with, Internet Explorer does not have a management object model; instead, the only way we can programmatically retrieve Internet Explorer settings and property values is by writing a script that grabs this information from the registry. That’s pretty easy; we often use registry-reading scripts in this column. The tricky part comes in figuring out which registry values need to be read, and knowing how to interpret the data that comes back.

Note. Another tricky part lies in knowing which ActiveX setting you’re interested in; for better or worse, Internet Explorer has several settings related to ActiveX controls. For today’s column, we’re assuming you want to read the value for this setting: Run ActiveX controls and plug-ins.

Let’s start by figuring out which registry values need to be modified. As it turns out, there is no global setting for Internet Explorer security settings; instead, these settings are managed by Internet Explorer zones. There are four such security zones; the zone names and their values are shown in the following table:

Zone Name | Zone Value
Intranet sites | 1
Trusted sites | 2
Internet sites | 3
Restricted sites | 4

Settings for Internet Explorer security zones can be found in the HKEY_CURRENT_USER\ Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\ portion of the registry; to access a particular zone you need to access the subkey corresponding to that zone. To determine the appropriate subkey, just tack the zone value to the preceding registry path. For example, to get at settings for the Internet sites zone (value 3) you need to access this registry subkey:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3

You can see the 3 tacked on at the end. Want to access settings for the Intranet sites zone (value 1)? Okey-doke:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1

Once you’ve located the correct registry subkey you then need to know which registry value to read. Unfortunately (at least for script writers) these registry values have somewhat-cryptic names; for example, the one we’re interested in is named 1200. (Why? We have no idea.) If you’re gung-ho on using scripts to read/manage Internet Explorer settings then you might want to take a look at the Managing Internet Explorer Enhanced Security Configuration whitepaper. Only a portion of the document deals with scripting, but that portion does map these cryptic registry values to the corresponding properties in the user interface. And, of course, many of these settings can be found in the Tweakomatic. (The Tweakomatic, unlike the whitepaper, will actually write the scripts for you.)

So are we ready to finally write a script and actually do something here? Almost. The other thing you need to know is that the configuration information is stored in the registry as a DWORD (numeric) value. Does it help if you know that the ActiveX controls setting is configured as a 3 rather than 65536? Probably not. But this table might help:

Registry Value | User Interface Value
0 | Enabled
1 | Prompt
3 | Disabled
65536 | Administrator Approved

And, no, that last value is not a misprint; it really is 65536. Go figure.

OK, now we’re ready to write a script. Here’s a sample script that retrieves setting information for the Intranet sites zone (zone value 1):

HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1"
ValueName = "1200"

objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

Wscript.Echo "Run ActiveX Controls and Plug-ins"

If IsNull(dwValue) Then
    Wscript.Echo "Intranet sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Intranet sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Intranet sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Intranet sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Intranet sites: Administrator Approved"
End If

We begin by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this tells the script which registry hive we want to work with. We then connect to the WMI service; note that the StdRegProv (Standard Registry provider) class is located in the root\default namespace. (Many script writers assume that the class is in root\cimv2, like most WMI classes. Nope.)

Next we assign values to a pair of variables:

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1"
ValueName = "1200"

As you can see, the variable strKeyPath contains the registry path within HKEY_CURRENT_USER (don’t include HKEY_CURRENT_USER in the path or the script will fail). The variable ValueName, meanwhile, is set to 1200, which happens to be the registry value we want to read.

We then call the GetDWORDValue method, which enables us to read a DWORD value in the registry:

objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

Note that we need to pass GetDWORDValue several parameters:

· HKEY_CURRENT_USER, the constant that tells the script which registry hive to use.

· strKeyPath, the variable containing the registry path.

· ValueName, the variable representing the registry value we want to read.

· dwValue, an “out parameter” that will end up holding the value read from the registry. If you’re thinking, “Wait, we didn’t assign a value to dwValue,” you’re right. And that’s by design: we don’t assign values to an out parameter. Instead, GetDWORDValue will read whatever happens to be stored in the registry value in question (1200) and then the method will assign that value to dwValue.

Yeah, that is kind of neat, isn’t it?

At this point we could simply echo back the value retrieved from the registry. As we noted, however, that will be a value such as 1 or 3 or 65536. Therefore, we set up a simple little If Then ElseIf block to examine the returned value and echo back a more meaningful message:

If IsNull(dwValue) Then
    Wscript.Echo "Intranet sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Intranet sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Intranet sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Intranet sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Intranet sites: Administrator Approved"
End If

You’re right: once you understand where (and how) the values are stored in the registry, this isn’t hard at all.

Just to save you some typing (and/or copying and pasting), here’s a script that returns information for all four security zones:

HKEY_CURRENT_USER = &H80000001

strComputer = "."
Set objReg = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1"
ValueName = "1200"
objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

Wscript.Echo "Run ActiveX Controls and Plugins"

If IsNull(dwValue) Then
    Wscript.Echo "Intranet sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Intranet sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Intranet sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Intranet sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Intranet sites: Administrator Approved"
End If

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\2"
ValueName = "1200"

objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

If IsNull(dwValue) Then
    Wscript.Echo "Trusted sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Trusted sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Trusted sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Trusted sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Trusted sites: Administrator Approved"
End If

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3"
ValueName = "1200"

objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

If IsNull(dwValue) Then
    Wscript.Echo "Internet sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Internet sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Internet sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Internet sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Internet sites: Administrator Approved"
End If

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\4"
ValueName = "1200"

objReg.GetDWORDValue HKEY_CURRENT_USER, strKeyPath, ValueName, dwValue

If IsNull(dwValue) Then
    Wscript.Echo "Restricted sites:  The value is either Null or could not be found in the registry."
ElseIf dwValue = 0 Then
    Wscript.Echo "Restricted sites: Enabled"
ElseIf dwValue = 1 Then
    Wscript.Echo "Restricted sites: Prompt"
ElseIf dwValue = 3 Then
    Wscript.Echo "Restricted sites: Disabled"
ElseIf dwValue = 65536 Then
    Wscript.Echo "Restricted sites: Administrator Approved"
End If

Run the script and you should get back output similar to this:

Run ActiveX Controls and Plugins
Intranet sites: Enabled
Trusted sites: Enabled
Internet sites: Enabled
Restricted sites: Disabled

Is there more we could do here? Maybe; after all, we can also configure this registry value. But that’s a chore for another day.


How Can I Tell if Any Internet Explorer Windows Are Open to a Particular Web Site?

Hey, Scripting Guy! How can I determine if I have any Internet Explorer windows open to a particular Web site?

-- N

Hey, N. Before we begin the Scripting Guys would like to state, for the record, that we have never tried to obtain each other’s phone records nor have we ever even considered listening in on each other’s phone conversations. This has become an issue in the tech world, and we thought it was important that everyone knows where we stand.

Well, OK, if you want to get technical, at one time we did try listening in on Peter Costantini’s phone calls home. However, seeing as how none of us speak Martian we soon gave up on that.

In fact, not only do we not listen to each other’s phone conversations (some of us barely listen to our own phone conversations) but – and, for once, this is actually relevant to the question you asked – we typically don’t try to determine if any of us have any browser windows open to a particular Web site. That’s not because we aren’t nosy; we are. That’s simply because this is a difficult task to perform against remote computers. In turn, that’s because the only way we know to do this is to use the Windows Shell object, an object that can only be created – and used – on the local machine.

So what does that mean? Well, that means we’ll show you a solution that will work, albeit only on your local computer. That’s fine, except for one thing: what if you want to get this information from a remote machine? In that case we can think of a couple possibilities. For one, you could use the WSH Controller object or WMI’s Win32_Process class to kick off the script on a remote machine. Either of those methods will work, although you will have to modify the code so that the results aren’t displayed in a message box but are, instead, saved to a text file or database or something.

Note. Why can’t you display a message box using these methods? Well, you can; it’s just that the message box will pop up on the remote machine.

Alternatively, you might configure this script to run as a scheduled task; that way you could have it run every x number of minutes, check to see if any browser windows are open to the specified Web site, and then report back the results. Here, too you’ll have to modify the code so that the results are saved somewhere rather than displayed in a message box. That’s not particularly complicated, just something you’ll have to decide for yourself.

But enough with the excuses. Let’s take a look at a script that can tell us whether or not a browser window is open to a specified Web site:

Set objShell = CreateObject("Shell.Application")
Set objShellWindows = objShell.Windows

If objShellWindows.Count = 0 Then
    Wscript.Echo "No browser windows are open to the Script Center."
    Wscript.Quit
End If

blnFound = False

For i = 0 to objShellWindows.Count - 1
    Set objIE = objShellWindows.Item(i)
    strURL = objIE.LocationURL
    If InStr(strURL, "http://www.microsoft.com/technet/scriptcenter")Then
        blnFound = True
    End If
Next

If blnFound Then
    Wscript.Echo "At least one browser window is open to the Script Center."
Else
    Wscript.Echo "No browser windows are open to the Script Center."
End If

Just like we said we would, we start off by creating an instance of the Shell.Application object. With that object in tow we then use the following line of code to return a collection of all the Shell windows currently open:

Set objShellWindows = objShell.Windows

In case you’re wondering, Shell windows are, well, windows that belong to the Shell. If you have Windows Explorer or a My Computer window open then you have at least Shell window open. The same is true for Internet Explorer: a running instance of Internet Explorer will appear in the Shell’s Windows collection.

Which, needless to say, is why we grabbed this collection in the first place.

Our next step is to check the value of the collection’s Count property; this tells us how many Shell windows are currently open. Why do we bother doing that? Well, if the Count is 0 that means there aren’t any Shell windows open; if no Shell windows are open then it’s a pretty good bet that there aren’t any Shell windows open to a particular Web site. Therefore, if the Count is 0 we echo back the fact that no browser windows are open to the target Web site, then use the Quit method to terminate the script. That’s what this block of code is for:

If objShellWindows.Count = 0 Then
    Wscript.Echo "No browser windows are open to the Script Center."
    Wscript.Quit
End If

Cool. But what if the Count isn’t 0? What then?

Well, after setting a variable named blnFound to False (more on that momentarily) we set up a For Next loop that runs from 0 to the collection Count minus 1:

For i = 0 to objShellWindows.Count - 1

Note. We knew you were going to ask that: why does the loop run from 0 to the Count minus 1? That’s because our collection operates just like an array: the first item in the collection has an index number (subscript) of 0, while the last item has an index number equal to the total number of items minus 1. If we have 9 items in the collection, the index number of the last item will be 8 (9 – 1). Thus we start at 0 (the first item) and continue through the Count minus 1 (the last item).

Inside the loop we use this line of code to bind to the current item in the collection:

Set objIE = objShellWindows.Item(i)

As you no doubt know, the first time through the loop the counter variable i is equal to 0; that means that this code will bind us to item 0, the first item in the collection.

After making the connection we store the value of the LocationURL property in the variable strURL. We then use VBScript’s InStr function to determine whether or not the window is open to the target Web site (which is what the LocationURL property tells us). For example, if our target Web site is the Script Center, we know that any page in the Script Center has to start with the string http://www.microsoft.com/technet/scriptcenter. Consequently, we use InStr to determine whether that string appears anywhere in the LocationURL:

If InStr(strURL, "http://www.microsoft.com/technet/scriptcenter")Then
        blnFound = True

If the target string is found (meaning InStr returned a value other than 0) we then set the value of the variable blnFound to True; that way we’ll know that we found at least one window open to the Script Center. If the target string is not found then we leave blnFound alone. Either way, we then loop around and check the next item in the collection.

That brings us to our final block of code:

If blnFound Then
    Wscript.Echo "At least one browser window is open to the Script Center."
Else
    Wscript.Echo "No browser windows are open to the Script Center."
End If

Here we’re checking the value of blnFound. If blnFound is True, that means that, somewhere along the line, we stumbled upon a browser window open to the Script Center; in response, we echo back that fact. If blnFound is False, that can only mean one thing: we didn’t find any browser windows open to the Script Center. (Remember the value of blnFound starts out as False, and changes only if we find the target Web site.) In turn, we then echo back the fact that we didn’t find any browser windows open to the Script Center.

Like we said, N, this might not be the ultimate solution for you; however, it should be enough to get you started. (And it might be exactly what you need if you plan on running this script locally.) If this doesn’t quite answer your question just let us know. We’ll either try to come up with a better solution for you or we’ll just listen in on one of Dean’s phone conversations and see if he can come up with a better solution for you.


How Can I Tell Whether a Web Page is Accessible?

Hey, Scripting Guy! How can I tell whether or not a Web page is accessible?

-- JW

Hey, JW. To tell you the truth, these are the kinds of questions we hate, questions where we don’t know the answer off the top of our heads but where it seems like there ought to be a way to do this. Sometimes in the world of scripting the hard things turn out to be easy but, conversely, the easy things sometimes turn out to be hard.

Fortunately, this one turned out to be easy. We knew there was a way to retrieve the contents of a Web page; what we didn’t know was whether there was a straightforward way to determine whether those contents were retrievable (that is, whether the Web page was accessible). But here you go. This script will tell you whether the home page for the Script Center is up and running:

On Error Resume Next

strURL = "http://www.microsoft.com/technet/scriptcenter/default.mspx"

Set objHTTP = CreateObject("MSXML2.XMLHTTP")
objHTTP.Open "GET", strURL, FALSE
objHTTP.Send

Wscript.Echo(objHTTP.statusText)

We begin by assigning the URL for the Script Center to a variable named strURL. Next we create an instance of MSXML2.XMLHTTP, a COM object that provides a way for client computers to interact with HTTP servers. We then call the Open method, passing three parameters: “GET” (which means to retrieve information from the server); strURL, the variable holding the URL to the Script Center home page; and FALSE, to make the call synchronous. After that we use the Send method to send our request to the HTTP server.

So how do we know if http://www.microsoft.com/technet/scriptcenter/default.mspx is accessible or not? Well, we simply check the value of the statusText property. If statusText is OK then the page was accessible; if it’s anything other than OK, then something went wrong and we couldn’t reach the specified URL.

Incidentally, the contents of the Web page are returned as the ResponseText property of the MSXML2.XMLHTTP object. That means you can use the FileSystem object to save the contents of the page to a file. For example:

On Error Resume Next

Const ForWriting = 2

strURL="http://www.microsoft.com/technet/scriptcenter/default.mspx"

Set objHTTP = CreateObject("MSXML2.XMLHTTP")
objHTTP.Open "GET", strURL, FALSE
objHTTP.Send

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile("script_center.htm", ForWriting)
objFile.Write objHTTP.ResponseText
objFile.Close

Bear in mind that the formatting of the saved document might not be identical to the formatting of the Web page; that depends on such things as the style sheets used by the Web page; whether the page uses relative or absolute links to graphics, etc. In most cases, however, the page will be readable, even if the formatting might be a bit off.


How Can I List All the Groups in a Windows NT 4.0 Domain?

Hey, Scripting Guy! How can I list all the groups in a Windows NT 4.0 domain?

-- MN

Hey, MN. It’s Martin Luther King, Jr. Day here in the USA, and, normally, you’d expect the Scripting Guy who writes this column to start things off by relating some irrelevant anecdote about Dr. King. Oddly enough, however, the Scripting Guy who writes this column, while fairly well-versed with the history of the civil rights movement, doesn’t have any anecdotes about Martin Luther King, Jr. But don’t worry: fortunately he does have a couple of anecdotes about Martin Luther, instigator of the Protestant Reformation.

We hear you: that was a close one, wasn’t it? After all, without those Martin Luther anecdotes this daily scripting column would have been reduced to talking about scripting. Perish the thought!

But enough chitchat; we have interesting anecdotes to relate. For one, Luther is perhaps best known for the statement he made at the Diet of Worms: “Here I stand; I can do no otherwise.” What’s so interesting about that? Apparently, he didn’t actually say it; instead, that statement was tacked on to his actual speech by a later biographer.

Hopefully some future biographer will go through all the Hey, Scripting Guy! columns and attach suitably profound statements to each one of those.

Here’s another interesting anecdote. Although Luther often railed against superstition, he still believed that he was hounded by the Devil. In fact, on one occasion he threw a shoe at Lucifer, something which managed to, at least temporarily, chase the Devil away.

Note. Sure, that’s does sound like a good piece advice: if you find yourself hounded by the Devil just throw a shoe at him. To be honest, though, most of you will never actually see the Devil. Unlike, say, those of us who work at Microsoft.

Wait, wait; take it easy. If you’re thinking, “I knew it, I knew the Devil worked at Microsoft,” well, we have to burst your bubble: we were just joking. The Devil does not work at Microsoft.

He has enough problems as it is.

Now that we have our anecdotes out of the way let’s focus on the business at hand: listing all the groups in a Windows NT 4.0 domain. It’s hard to believe that, after nearly 700 Hey, Scripting Guy! columns we’ve never addressed this issue. But, now we have:

strDomain = "fabrikam"

Set colGroups = GetObject("WinNT://" & strDomain & "")
colGroups.Filter = Array("group")

For Each objGroup In colGroups
    Wscript.Echo objGroup.Name
Next

Man, if we’d known it was this easy to list all the groups in a Windows NT 4.0 domain we would have written this script a long time ago!

As you can see, we really don’t have to do much here. We start out by assigning the name of the domain (fabrikam) to a variable named strDomain. We then use the WinNT provider to bind to the domain. Pay close attention to the odd letter casing here: WinNT. ADSI provider names are case-sensitive; that means that winnt or WINNT or anything other than WinNT will cause the script to fail.

Binding to the domain gives us a collection of all the domain objects, including users, groups, printers, and what-have-you. Because we’re interested only in groups we next apply a Filter that limits returned data to, well, groups:

colGroups.Filter = Array("group")

And, yes, we do need to pass the Filter value (group) as an array, even though there’s just one item for this particular filter. For such a little script it’s just jam-packed with things you have to watch out for, isn’t it?

All that’s left at this point is to set up a For Each loop to loop through the collection and echo back the Name of each group:

For Each objGroup In colGroups
    Wscript.Echo objGroup.Name
Next

What’s that? You say that, in addition to the name of each group, you’d like to see the membership for each of these groups? All you had to do was ask:

strDomain = "fabrikam"

Set colGroups = GetObject("WinNT://" & strDomain & "")
colGroups.Filter = Array("group")

For Each objGroup In colGroups
    Wscript.Echo objGroup.Name
    For Each objUser in objGroup.Members
        Wscript.Echo objUser.Name
    Next
    Wscript.Echo
Next

Here’s another bonus: this same script, virtually unchanged, can also be used to return information about local groups as well. All you have to do is assign the name of the computer to the variable strDomain. Or, if you like things a little neater, change strDomain to a variable named strComputer, like so:

strComputer = "atl-fs-01"

Set colGroups = GetObject("WinNT://" & strComputer & "")
colGroups.Filter = Array("group")

For Each objGroup In colGroups
    Wscript.Echo objGroup.Name
    For Each objUser in objGroup.Members
        Wscript.Echo objUser.Name
    Next
    Wscript.Echo
Next

That’s all there is to it.

Incidentally, the Scripting Guy who writes this column actually has one more Martin Luther anecdote: as a young monk Luther used to punish himself by lying outside in the snow … at night … in the middle of winter. So why doesn’t the Scripting Guy who writes this column like to relate that anecdote? That’s easy: it’s almost mid-year review time here at Microsoft, and he doesn’t want to give his manager any ideas.


How Can I Check to See if a User Exists in an NT 4 Domain?

Hey, Scripting Guy! How can I check to see if a user exists in an NT 4 domain?

-- PA

Hey, PA. You know, if you were to ask us, “How do you back your car out of your driveway?” we’d probably tell you that – after checking all the mirrors – we turn so that we’re facing directly out the back window of the car and then slowly and cautiously begin backing out of the driveway. Now, that’s what we’d tell you. Is that the way we really back the car out of the driveway, or do we just kind of glance in the rear view mirror while we’re pulling out? Hey, we don’t have to answer questions like that; after all, isn’t this supposed to be a column about scripting?

Besides, we never even came close to hitting that Chevy.

Believe it or not, we actually had a good reason for bringing up the subject of backing out of the driveway: for some of us, at least, there’s a difference between how we should do it and how we actually do it. The same thing is true for checking to see if a user exists in a Windows NT 4 domain (or on a local computer, seeing as how the approach is exactly the same). There’s a way that you should make this check, but there’s at least one other way that you can make this check. We’ll show you both approaches, then leave it up to you to decide how you should do it. (Hmmm, We Report, You Decide. Wonder if anyone else is using that slogan?)

If we were dealing with Active Directory there’d be no question how to do this: we’d just run an ADO (ActiveX Data Objects) search and be done with it. Unfortunately, though, the WinNT provider – the scripting technology for working with NT 4 domains and with local user accounts – doesn’t support ADO; thus we can’t do a search against those user accounts. Instead, the “official” way to check for the existence of a user account in an NT 4 domain (or on a local computer) is to use a script similar to this:

Set objDomain = GetObject("WinNT://fabrikam")
objDomain.Filter = Array("User")

blnFound = FALSE

For Each objUser in objDomain
    If objUser.Name = "kenmyer" Then
        blnFound = TRUE
        Exit For
    End If
Next

If blnFound = TRUE Then
    Wscript.Echo "The user account exists in the domain."
Else
    Wscript.Echo "The user account does not exist in the domain."
End If

The script begins by binding to the fabrikam domain, then creates a Filter that limits the returned objects to user accounts. That’s what these two lines of code do:

Set objDomain = GetObject("WinNT://fabrikam")
objDomain.Filter = Array("User")

Next we create a variable named blnFound and set the value to False. We’ll use blnFound to keep track of whether or not we can find the user account in question. If we find the account, we’ll change blnFound to True; if we don’t find the account, then blnFound will stay False.

We then set up a For Each loop to loop through the list of user accounts. For each account in the collection we check to see if the Name property is equal to kenmyer. If it is, we set the value of blnFound to True, then use the Exit For statement to exit the For Each loop. (Having already found the name there’s no reason to cycle through the rest of the collection.) If the Name is not equal to kenmyer then we simply loop around and check the next account in the collection.

At the end of the script we check the value of blnFound and then echo back the appropriate message (that is, whether or not the user account exists in the domain).

All that works just fine, but if you have 15-20,000 users in your domain it could take a couple minutes for the script to cycle through all the user accounts. So is there a faster way to check for the existence of a user account in an NT 4 domain or a local computer? Yes, there is:

On Error Resume Next

Set objUser = GetObject("WinNT://fabrikam/kenmyer")

If Err.Number = 0 Then
    Wscript.Echo "The user account exists in the domain."
ElseIf Err.Number = -2147022676 Then
    Wscript.Echo "The user account does not exist in the domain."
Else
    Wscript.Echo "The user account status could not be determined."
End If

This script starts off with the On Error Resume Next statement. That’s important, because we aren’t going to retrieve a list of all the user accounts and then methodically check each one to see if the Name equals kenmyer. Instead, we’re going to try to bind directly to the account in question:

Set objUser = GetObject("WinNT://fabrikam/kenmyer")

If we’re able to bind to the account (that is, if the account exists), then the value of the Err object will be equal to 0. We check the value of Err.Number and, if it equals 0, we report back that the user account exists in the domain. If Err.Number is equal to -2147022676 we report back that the account does not exist in the domain. And if Err.Number is equal to something else we report that the account status could not be determined.

As we said, this approach is faster and requires less code. So then why don’t we “officially” recommend it? Well, the truth is, a lot of things can potentially go wrong with a script; because of that you hate to rely on the error number as the only way of determining whether an object exists or not. This script should work just fine, but we don’t recommend making a habit of using shortcuts like this. If anyone ever asks you, “Do the Scripting Guys recommend that you rely on error number as the only way to determine whether or not an object exists?”, make sure you tell them “No, they don’t.” That doesn’t mean we won’t do that from time-to-time, just that we don’t recommend it.

Now, did you say you had a question about cars and about driving? Well, what we Scripting Guys always recommend is – you still there? Hello? Come on: we know as much as those Car Talk guys do.

Well, not about cars, of course, but ….


How Can I Get the Full Name of a Windows NT 4.0 User When All I Have is Their Logon Name?

Hey, Scripting Guy! How can I determine the full name of a user in a Windows NT 4.0 domain when all I have is the user’s logon name?

-- NH

Hey, NH. In case you’re wondering, after a less-than-auspicious beginning the Scripting Guy who writes this column started to get used to swimming laps every night; in fact, he even started to enjoy the experience a little. Of course, no sooner did he do so than the Scripting Son came up with a new idea: core training. “That’s what everyone does,” remarked the Scripting Son. “Don’t you want to have a strong core?”

Well, to be perfectly honest: no. After all, the Scripting Dad has survived this long without having a strong core; surely he could get by for at least a little while longer with a weak core.

He became even more convinced of this after he found out what it actually meant to do core training. As near as he can tell, core training primarily involves contorting your body into an unnatural position, having someone stick a 25-pound weight on you, and then holding that position until you pass out or until time is up. For example, in one exercise you get down into a sort of pushup-like position, balancing on your toes and forearms. The Scripting Son then plops a 25-pound weight on your back and tells you to hold yourself up for 1 minute.

Admittedly, that doesn’t sound too bad, although it is. But it’s still something even the Scripting Guy who writes this column can handle. It only starts to get bad when the Scripting Son says, “You know, I can do 2 minutes.” And then the Scripting Dad says, “What, just 2? I can do 3 – no, wait: I can do 4!”

We don’t know if any of you have ever had the desire to see a Scripting Guy cry, but, if you do, just swing by the gym some night when he’s trying to do 4 minutes.

Or, if you prefer, swing by his office on the day he gets his performance review. Either way he’s bound to be crying.

Of course, we’d hate to see you cry, NH. So here’s a script that reports back the full name of a user (Ken Myer) when all you have is the user’s logon name (kenmyer):

strDomain = "fabrikam"
strUserName = "kenmyer"

Set objUser = GetObject("WinNT://"
& strDomain & "/" & strUserName)
Wscript.Echo objUser.FullName

Yes, it is a very simple little script. (And not by coincidence: thanks to the Scripting Games we were looking for something we could answer quickly and easily.) To start off with we simply assign values to a pair of variables: strDomain gets the name of our domain and strUserName gets the logon name of our user. We then use this line of code to bind directly to the user account in the NT 4.0 domain:

Set objUser = GetObject("WinNT://" & strDomain & "/" & strUserName)

Once we’ve made the connection we need just one more line of code to report back the user’s FullName:

Wscript.Echo objUser.FullName

And that’s it; that’s all we have to do.

Of course, we should note that, as an added bonus, this very same script can be used to retrieve the full name of a local user; in that case, you simply bind to the SAM (security account manager) on a computer as opposed to a domain. Here’s pretty much the same script we just showed you, slightly revised to work with local user accounts:

strComputer = "fabrikam"
strUserName = "kenmyer"

Set objUser = GetObject("WinNT://" & strComputer & "/" & strUserName)
Wscript.Echo objUser.FullName

If that looks vaguely familiar, well, it should: all we did was change the name of the variable strDomain to strComputer. Other than that ….

While we’re on the subject, the flip side to this question is this: if all I have is the user’s full name how can I get his or her logon name?

Note. In this era of CDs and DVDs does anyone even know what we’re talking about when we say “flip side?”

That’s a good point: nobody ever knows what we’re talking about when it comes to this column, do they?

OK, so how do you get the logon name if all you have is the full name? Well, with an Active Directory domain the answer would be obvious: you’d do an ADO search on the domain. Unfortunately, you can’t search for user accounts in an NT 4.0 domain or a local SAM. Fortunately, though, that doesn’t mean you can’t locate a particular user, it just means you have to go about it like this:

strDomain = "fabrikam"

Set colAccounts = GetObject("WinNT://" & strDomain & "")
colAccounts.Filter = Array("user")

For Each objUser In colAccounts
    If objUser.FullName = "Ken Myer" Then
        Wscript.Echo objUser.Name
        Exit For
    End If
Next

What we’re doing with this script is, first of all, binding to the domain itself; notice that we don’t specify a user account in our binding string:

Set colAccounts = GetObject("WinNT://" & strDomain & "")

After making the connection we then set the Filter property to user; this ensures that our collection will contain only user accounts, weeding out groups, printers, services, and other entities that might otherwise be there. That’s what we do here (and, yes, values to the Filter property must be passed as an array, even if we’re filtering on only one object type):

colAccounts.Filter = Array("user")

We then set up a For Each loop to walk through the entire collection. Inside that loop we check each individual user account to see if the FullName property is equal Ken Myer. If it is, then we echo back the value of the Name property and then exit the loop. If it isn’t, then we loop around and try again with the next account. That’s what this block of code does:

If objUser.FullName = "Ken Myer" Then
    Wscript.Echo objUser.Name
    Exit For
End If

Admittedly, it’s not as cool as doing an ADO search, but it’s the best we can do. And it will work, both with NT 4.0 domains and with local users.

We see you have another question. The answer to that one is no: you do not need to have a strong core in order to enter the 2007 Winter Scripting Games, and have a shot at winning a Dr. Scripto Bobblehead doll or a copy of the book Windows PowerShell in Action. (Well, not unless you usually write scripts with 25-pound weights strapped to your back.) Just enter an event (any event) and you’ll automatically be entered in the drawing for one of those prizes. Score 60 points in any one division and you’ll earn a Certificate of Excellence signed by the Scripting Guys. Pretty cool, huh?

Note. Why are we giving away Certificates of Excellence? Well, as it turned out, awhile back we bought a whole bunch of these awards, the idea being that we would hand them out to each other every time we did something really good. Let’s just say that we have plenty of certificates left over ….


How Can I List All the Users in an NT 4

How Can I List All the Users in an NT 4.0 Domain?

Hey, Scripting Guy! How can I list all the users in an NT 4.0 domain, along with their description and logon script path?

-- HH

Hey, HH. Ah, yes NT 4.0. Windows NT 4.0 reminds us of dandelions: it doesn’t matter whether you like dandelions or not, there’s simply no way to get rid of them. The same is true of NT 4.0. Regardless of what people think of NT 4.0, there’s simply no way to get rid of it: it’s surprising how many organizations still rely (and often rely heavily) on this version of Windows.

But that’s OK: we happen to be firm believers in the adage that if it ain’t broke then don’t fix it. (Editor’s Note: The opinions expressed here are not necessarily the opinions of the Microsoft Corporation or anyone else affiliated with Microsoft – especially anyone in the sales and marketing departments.) Besides, while NT 4.0 might be getting up there in years, as long as you’ve installed Service Pack 3 or later it’s fully-scriptable. In fact, you can use a script similar to this to return information about all the users in an NT 4.0 domain:

Set objDomain = GetObject("WinNT://fabrikam")
objDomain.Filter = Array("User")

For Each objUser in objDomain
    Wscript.Echo "User name: " & objUser.Name
    Wscript.Echo "Description: " & objUser.Description
    Wscript.Echo "Logon script path: " & objUser.LoginScript
    Wscript.Echo
Next

As you might expect, we begin by binding to the fabrikam domain, a process that requires just one line of code:

Set objDomain = GetObject("WinNT://fabrikam ")

Two things to watch out for here. First, because we’re dealing with NT 4.0, we need to use the WinNT provider; the LDAP provider is only for Active Directory. Second, this is one of those rare occasions when case (uppercase vs. lowercase, that is) matters: it has to be WinNT, not WINNT, winnt, or any other variation.

Note. This script can also be used as-is to retrieve information about local user accounts. Just replace the domain name (fabrikam) with the name of the computer.

No, really, that’s all you have to do. Honest.

After making the connection we then apply a Filter to limit the returned data to user accounts. That, too requires just a single line of code:

objDomain.Filter = Array("User")

At that point we’re practically done: all we have to do is set up a For Each loop to loop through the collection of user accounts. For each account in the collection we then echo back the values of the Name, Description, and LoginScript properties. Game, set, and match.

Of course, now that we’ve whet your appetite for scripting against NT 4.0 domains you might be wondering what other user account values you can retrieve. Here’s a “bonus” script that connects to a user account and echoes back all the attributes of that account and their values:

On Error Resume Next

strDomain = "fabrikam"
Set objUser = GetObject("WinNT://" & strDomain & "/kenmyer")
Set objClass = GetObject(objUser.Schema)

WScript.Echo "Mandatory properties for " & objUser.Name & ":"
For Each property in objClass.MandatoryProperties
    WScript.Echo property, objUser.Get(property)
Next

WScript.Echo "Optional properties for " & objUser.Name & ":"
For Each property in objClass.OptionalProperties
    WScript.Echo property, objUser.Get(property)
Next

Not bad for a piece of software that’s nearly 1,000 years old, is it?

Note. Granted, we might poke a little fun at NT 4.0. However, after calculating the average number of candles required for a Scripting Guy birthday cake, well, let’s just say we’re not so sure you should ruthlessly discard things just because they’re getting a little old ….


How Can I Rename a User Account in a Windows NT 4

How Can I Rename a User Account in a Windows NT 4.0 Domain?

Hey, Scripting Guy! How can I rename a user account in a Windows NT 4.0 domain?

-- JK

Hey, JK. You know, as this column is being written it’s a Monday here in Seattle, and it’s raining. And yes, we know what you’re thinking: when isn’t it raining in Seattle? Unfortunately, though, our weather is much crueler than that. After all, if it rained all the time you could plan for that and get used to it. In Seattle, though, we tend to have three or four absolutely gorgeous days: sunshine, blue skies, highs in the 80s. It’s only then, just about the time you think that summer has arrived, that it rains. And rains. And, well, rains.

And then, of course, it gets nice for a couple of days, we start to get our hopes up, and then the same vicious cycle repeats itself.

Of course, there are two things you can do when the weather acts up like this. One thing you can do is sit around and mope, bemoaning the fate that has befallen you and doing as little work as possible. On the other hand, you can always take advantage of the opportunity and – with nothing to distract you – tackle one of those challenging problems that you’ve been meaning to tackle for a long, long time.

We’ve chosen to mope.

Set objDomain = GetObject("WinNT://fabrikam")
Set objUser = GetObject(""WinNT://fabrikam/kmyer")
objDomain.MoveHere objUser.AdsPath, "kenmyer"

You can see why we chose to answer this question: all we had to do was write three lines of code and then we could go back to feeling sorry for ourselves. We start off by creating a pair of object references, one to the domain (objDomain) and one to the user account that we want to rename (objUser). That’s what we do here:

Set objDomain = GetObject("WinNT://fabrikam")
Set objUser = GetObject(""WinNT://fabrikam/kmyer")

And here’s the best part. After we’ve connected to the domain and to the user account it takes just one line of code to rename that account:

objDomain.MoveHere objUser.AdsPath, "kenmyer"

As you can see, all we do is call the MoveHere method and specify two parameters:

· The AdsPath to the user account. The AdsPath uniquely identifies a user account in much the same way a UNC path uniquely identifies a file. For example, the UNC path \\atl-fs-01\public\test.txt can refer to only one file: a file named Test.txt found in the Public folder on the computer atl-ws-01. Likewise the AdsPath WinNT://fabrikam/kmyer can refer to only one user account: an account named kmyer located in the Fabrikam domain.

· The new name (kenmyer) to be given to the user account.

And that’s it: call the method, and the kmyer user account will immediately be renamed kenmyer.

A few things to keep in mind here. First, neither Windows Script Host nor ADSI were available when Windows NT 4.0 first shipped. If you haven’t already done so this script won’t work unless you install both technologies on your domain controller. For more information, check out the Script Center FAQ.

Second, this script will work with Active Directory user accounts; it’s not limited to just NT 4.0 domains. We don’t really recommend you use this with Active Directory; with a few rare exceptions you should always use the LDAP provider (as opposed to the WinNT provider) when working with Active Directory. If you have no idea what we’re talking about, don’t feel bad: nobody ever has any idea what we’re talking about! But you can find a reasonably good explanation of ADSI providers (as well as which one to use, and when) in the Microsoft Windows 2000 Scripting Guide.

Finally, this script will not enable you to rename a local user account; instead it will fail with an “unspecified error” error message. So what do you do if you have the urge to rename local user accounts, at least on a Windows XP or a Windows Server 2003 computer? This Hey, Scripting Guy! column should help.

We should also mention that we agree with you: it is a bit odd that you use a method named MoveHere to rename a user account. Why don’t you just use something like, oh, say, Rename? That’s a question we can’t answer. The best we can do is tell you not to worry about it. The syntax might seem a little strange, but it works. And that’s all we really care about anyway.

In the meantime, let’s check tomorrow’s forecast:

Cloudy with showers. High 62F. Winds S at 10 to 15 mph. Chance of rain 60%.

Wonderful. Word of warning: if you thought today’s column was filled with self-pity just wait until you see tomorrow’s. We had a little bit of rain on Saturday, a lot of rain on Sunday, rain today, and a 60% chance of rain tomorrow. There’s just nothing like Seattle in the summertime, is there? (Editor’s Note: It’s actually still spring, which might explain this if it weren’t for the fact that summer works pretty much the same way.)


How Can I Determine Last Month’s Date?

Hey, Scripting Guy! How can I determine last month’s date? I need to be able to take the month and year for the previous month (using the format mmyy) and create a file name like this: test0606.xls.

-- DR

Hey, DR. We have to admit that, at first, this question posed a bit of a problem for the Scripting Guys. Not because this is an especially tough script to write; it isn’t. It’s just that we never have to bother with scripts to find out about things that happened in the past: if we want old and out-of-date information we just go ask Greg. If you ever need information about something no else cares about anymore, trust us: Greg’s the guy to see.

On the other hand, Greg always seems to leave early to coach baseball, and he adamantly refuses to make house calls; that means there’s little chance he’ll drop by and help you determine last month’s date. (Granted, no one has ever actually asked him to make a house call, but he still refuses out of general principle.) So if Greg won’t help us are we out of luck here? Of course not; as it turns out, Greg can be more-than-adequately replaced by just a few lines of VBScript code:

intMonth = Month(Date)
intYear = Year(Date)

If intMonth = 1 Then
    intMonth = 12
    intYear = intYear - 1
Else
    intMonth = intMonth - 1
End If

If intMonth < 10 Then
    intMonth = "0" & intMonth
End If

intYear = Right(intYear, 2)

strFileName = "test" & intMonth & intYear & ".xls"

Wscript.Echo strFileName

As you can see, determining the current month and the current year is pretty easy: we just need to use the Month and Year functions, respectively:

intMonth = Month(Date)
intYear = Year(Date)

Those two lines of code are going to give us back values like 7 and 2006, assuming that we’re currently in July, 2006. And, as usual, you’re way ahead of us: if the current month is equal to 7, then all we have to do is subtract 1 from the current month and then we’ll know the previous month: month number 6 (June). Case closed!

Or is it? As Greg would be the first to point out, there’s a problem with that approach. Suppose we run our script on January 3, 2006. January is month 1; if we subtract 1 from 1 we get … um, month 0. On top of that, the month prior to January 2006 is December 2005, but we still have 2006 as the year. Uh-oh.

Wait, put down the phone; there’s no need to call Greg. Here’s how we can overcome that little obstacle:

If intMonth = 1 Then
intMonth = 12
intYear = intYear
- 1
Else
intMonth = intMonth
- 1
End If

All we’re doing here is setting up an If-Then statement to check the value of intMonth. If intMonth is equal to 1 (which will be the case if we run the script in January) we use these two lines of code to set the month to 12 (December) and the year to the current year minus 1 (e.g., 2006 - 1, or 2005):

intMonth = 12
intYear = intYear - 1

If intMonth is equal to anything other than 1 then we simply assign the variable a new value: the current value (e.g., 7) minus 1. We’re now able to correctly determine the previous month and year, even if we happen to run the script in January.

Oh: right. Greg (who’s now feeling a bit miffed because we don’t need his help) has just pointed out another problem. DR wants to use the format mmyy; that is, two digits for the month and two digits for the year. In other words, he wants 06 and 06, and we currently have 6 and 2006. Now is it time to go crawling back to Greg, begging for his help?

Listen, we don’t need Greg’s help in order to turn 6 2006 into 06 06. For starters, let’s take a look at a block of code that will turn 6 into 06:

If intMonth < 10 Then
    intMonth = "0" & intMonth
End If

Nothing to it, right? Here we’re simply checking to see if the value of intMonth is less than 10. Why 10? Well, if intMonth is equal to 10, 11, or 12, then there’s no problem; in that case the month already has two digits. We’re only concerned about months 1 through 9, the single-digit months. If we’re dealing with one of those months we just use this line of code to place a leading zero before the month number:

intMonth = "0" & intMonth

Just like that intMonth changes from 6 to 06. Which is just what we wanted it to do.

Converting 2006 to 06 is even easier; to handle that problem we just use the Right function to grab the two right-most characters off the end of 2006:

intYear = Right(intYear, 2)

Once that line of code executes the variable intMonth will be equal to 06 and the variable intYear will be equal to 06. That means we can then use this line of code to piece together our file name:

strFileName = "test" & intMonth & intYear & ".xls"

And that’s about it. To verify that the process worked we finish the script by echoing back the new file name:

test0606.xls

Cool. And we didn’t need Greg’s help at all.

Note. In all fairness, we should point out that Greg still has some use around here. After all, if we ever need to know why they called the infield fly rule even though an outfielder made the catch, or if we absolutely have to know what happens if a fielder tosses his glove at the ball, well, Greg will still be the guy we turn to. Of course, other than that ….


How Can I Determine the Beginning and Ending Date of the Previous Month?

Hey, Scripting Guy! Given a specific date, how can I determine the beginning and ending date of the previous month? In other words, given 8/11/2005, I need to get back 7/1/2005 and 7/31/2005.

-- PH

Hey, PH. You know, this was a tough question for us: after all, as Microsoft employees we’re always looking forward; we never look back. Last month? We don’t even know the meaning of the words.

OK, true: the Scripting Guys don’t know the meaning of lots of words. But you know what we mean.

But guess what? If a reader of the Hey, Scripting Guy! column needs a script that can tell you the beginning and ending dates of the previous month, well, by golly that’s what that reader’s going to get:

dtmTargetDate = Date
intDay = Day(dtmTargetDate)

dtmLastDay = dtmTargetDate - intDay
Wscript.Echo "Last day of previous month: " & dtmLastDay

dtmFirstDay = Month(dtmLastDay) & "/1/" & Year(dtmLastDay)
Wscript.Echo "First day of previous month: " & dtmFirstDay

Yes, just a few cryptic-looking lines (don’t ask us any questions about DNS or DHCP, because we seem to have used up our entire quota of the letter D for the month). It’s actually a fairly simple little script.

The first line of code simply specifies the target date. In this sample script we’re using the current date (indicated by the Date function). However, we can specify any date we want in this first line. For example:

dtmTargetDay = #12/19/1989#

In line 2 we use the VBScript Day function to extract the day portion of the current date; if the date happens to be 8/3/2005, then intDay will be equal to 3. Why do we care about the day portion of the date? Well, suppose it is 8/3/2005 and – using our date arithmetic skills – we subtract 3 days from that date. That doesn’t give us 8/0/2005; instead, it gives us 7/31/2005, which just happens to be the last day of the previous month. As it turns out, determining the last day of the previous month is no more difficult than subtracting the day (3) from the date and then echoing the resulting value:

dtmLastDay = dtmTargetDate - intDay
Wscript.Echo "Last day of previous month: " & dtmLastDay

Calculating the first day of the month is even easier, in part because we cheat a little. It’s pretty much a guarantee that the first day of the month will always be day 1. Therefore, all we need to do is grab the month and year from our variable dtmLastDay (which holds the value of the last day of the previous month), jam everything together, and then echo the result:

dtmFirstDay = Month(dtmLastDay) & "/1/" & Year(dtmLastDay)
Wscript.Echo "First day of previous month: " & dtmFirstDay

When we run the script using 8/3/2005 as the target date we get this output:

Last day of previous month: 7/31/2005
First day of previous month: 7/1/2005

There you have it: a script that will actually transport you back in time. Could Back to the Future: The Scripting Edition be far behind?


How Can I Determine the Date for the Friday in a Given Week?

Hey, Scripting Guy! Given a date, can a script tell me the date that the Friday for that week occurs?

-- DLR

Hey, DLR. Too bad you didn’t ask which dates Saturdays, Sundays, and holidays fall on; coincidentally enough, each of the Scripting Guys have memorized – years in advance – all the days that we aren’t required to come to work. When it comes to work days like Fridays, however, we don’t do quite as well.

But that’s OK: we don’t need to know the dates of all the Fridays because we can get a script to figure that out for us. In fact, here’s a script that – given a date – will report back the date that the Friday of that week falls on:

dtmDate = #6/23/2005#

intDay = WeekDay(dtmDate)
intAdder = 6 - intDay
dtmFriday = dtmDate + intAdder

Wscript.Echo dtmFriday

How can such a little script accomplish such an amazing feat? Well, let’s see if we can figure that out. The script begins by assigning the date 6/23/2005 (June 23, 2005) to a variable named dtmDate. Note that the pound signs (#) are optional; we could have surrounded the date with double quote marks instead. However, using pound signs ensures that VBScript interprets the value as a date and not as something else.

Next we use the WeekDay function to determine the day of the week for June 23, 2005 (it’s a Thursday, by the way). WeekDay is going to return one of the following values:

Constant | Value | Description
vbSunday | 1 | Sunday
vbMonday | 2 | Monday
vbTuesday | 3 | Tuesday
vbWednesday | 4 | Wednesday
vbThursday | 5 | Thursday
vbFriday | 6 | Friday
vbSaturday | 7 | Saturday

Note. The above table – and the script we just showed you – both assume that Sunday is the first day of the week.

This gives us a value representing the day of the week; in this case we’ll get back a 5, indicating that June 23, 2005 falls on a Thursday. What we need to do now is programmatically determine the number of days between this date and the Friday of that week. That’s something we can do with a single line of code:

intAdder = 6 - intDay

Confused? Don’t be; this actually makes sense. If you refer to the table you’ll see that Friday has a value of 6. If we take the value for Friday (6) and subtract the value for Thursday (5) we get 1. And guess what? If we add 1 day to June 23, 2005 we’ll get the date of the Friday for that week: June 24, 2005. That’s what we do with this line of code:

dtmFriday = dtmDate + intAdder

See how that works? In fact, this simple little equation even works with Saturdays. Suppose we’re checking the date June 25, 2005, which happens to be a Saturday (and has a WeekDay value of 7). Our equation would work out to this:

intAdder = 6 – 7

As you well know, 6 – 7 equals -1. Subtract 1 day from June 25, 2005 and you get June 24, 2005, a Friday. Pretty slick, huh?

Incidentally, there are all sorts of cool things you can do with dates and date arithmetic; for more information, take a look at the VBScript Primer chapter in the Microsoft Windows 2000 Scripting Guide.

As for the Scripting Guys, it’s nice to have a script that can tell you the date that a Friday falls on or the date that a Saturday or Sunday falls on. But we’re not satisfied with that; we plan on writing a script that will turn any given day into a Saturday (just a coincidence that we all get Saturdays off.) If you suddenly stop seeing Hey, Scripting Guy! columns (which aren’t published on Saturdays) you’ll know we succeeded.

Based on the way our initial research is going, however, you can expect to see a new column tomorrow, same as always. (This is turning out to be much harder than we’d hoped.)


How Can I Determine the Day of the Week?

Hey, Scripting Guy! I have a script that does certain management tasks based on the day of the week. I know how get the date in a script, but how can I tell whether it’s a Monday or a Tuesday or whatever?

-- CT, Tallahassee, FL

Hey, CT. This is actually much easier then it might sound; that’s because VBScript has a built-in function called DatePart that can take any date you give it and tell you everything from the hour to the minute to the day of the week. All you have to do is pass DatePart two items:

Item | Description
The date part you’re looking for. | In this case, we’re looking for the day of the week, so we pass the value “w”. If we were looking for, say, the year, we’d pass the value “y”.
The date in question. | For this script, we simply assign the current date to the variable dtmToday.

Our script ends up looking something like this:

dtmToday = Date()

dtmDayOfWeek = DatePart("w", dtmToday)

Select Case dtmDayOfWeek
    Case 1 Wscript.Echo "Sunday"
    Case 2 Wscript.Echo "Monday"
    Case 3 Wscript.Echo "Tuesday"
    Case 4 Wscript.Echo "Wednesday"
    Case 5 Wscript.Echo "Thursday"
    Case 6 Wscript.Echo "Friday"
    Case 7 Wscript.Echo "Saturday"
End Select

Note the one tricky part: the day of the week comes back as an integer (1 = Sunday, 2 = Monday, etc.). Therefore, for the purposes of this script we used a Select Case statement to convert that integer into the actual day of the week itself.

For more information about the DatePart function, see this portion of the Microsoft Windows 2000 Scripting Guide.


How Can I Determine the Elapsed Time Between Two Dates?

Hey, Scripting Guy! How can I tell how many days there are between two dates? For example, if I have a log file that says an application started on January 1, 2005 and that the application ended today, is there a way to determine how many days the application ran?

-- AL

Hey, AL. You know, in the second Hey, Scripting Guy! column ever written ("How Can I Get Yesterday's Date?"), we talked about simple date arithmetic. For some reason, though, we didn’t talk about this kind of date arithmetic, the ability to determine elapsed time between two dates. So let’s rectify that right now.

As it turns out, doing date arithmetic such as this is remarkably easy with VBScript; that’s because VBScript includes a DateDiff function designed to carry out calculations such as this. Want to know the number of days between today and January 1, 2005? Then just use a script like this one:

dtmEndingDate = Date
intDays = DateDiff("d", #1/1/2005#, dtmEndingDate)
Wscript.Echo intDays

Yep, that’s it: just three lines of code. We begin by assigning today’s date (Date) to a variable named dtmEndingDate; that’s what this line of code does:

dtmEndingDate = Date

We then call the DateDiff function and store the results in a variable named intDays; that’s what this line of code does:

intDays = DateDiff("d", #1/1/2005#, dtmEndingDate)

Note that DateDiff requires three parameters. First, we need to indicate the time interval we’re using. For this script we’re using days, so we pass the parameter “d”. We could also use time intervals such as hours (“h”), weeks (“w”), or months (“m”). For a complete list, see "Working with Dates and Times" in the Microsoft Windows 2000 Scripting Guide.

Second, we need to indicate the starting date (in this case, 1/1/2005); note that we surround the date using pound signs (#) to ensure that VBScript interprets the value as a date.

Finally, we indicate the ending date. VBScript will calculate the number of days between the two dates and stash that number in the variable intDays. At that point all we have to do is to echo back the elapsed time.

As we noted, you can use different time intervals; you’re not limited to just days. For example, suppose you know that your application started at 8:00 AM on January 1, 2005 and ended at 9:30 PM on January 5, 2005. Want to know how many hours the application was running? Here’s a script that does just that:

intHours = DateDiff _
    ("h", #1/1/2005 8:00 AM#, #1/5/2005 9:30 PM#)
Wscript.Echo intHours

Note that the value returned will be rounded to the nearest hour. To get even more specific, you could calculate the elapsed time in minutes and then divide by 60 in order to determine hours and fraction of an hour. But we’ll leave that up to you.


How Can I Determine the First Friday in a Month?

Hey, Scripting Guy! How can I determine the first Friday in a month?

-- JB

Hey, JB. You know, when we first glanced at this question we were hoping it said, “How can I determine if any given day in a month is a Friday?” Why? Well, if that was the case then we had a sure-fire method for you: just walk through the cafeteria in the Scripting Guys building at lunch time. If you walk into the cafeteria at noon on, say, a Monday or a Thursday, the place will be jam-packed; walk through on a Friday, however, and you feel like you’re the last person on Earth. Nobody is around on a Friday.

Note. Is that because all these dedicated Microsoft employees are working through lunch, making sure they get everything done before the weekend? Um, sure; why not?

Of course, that wasn’t your question; instead, you wanted to know how you can identify the first Friday in a month. Admittedly, you might not find the answer to that simply by walking through the Microsoft cafeteria. Instead, you might need to use a script similar to this:

dtmDate = #11/1/2006#

Do Until x = 1
    intDayOfWeek = Weekday(dtmDate)
    If intDayOfWeek = 6 Then
        Wscript.Echo "The first Friday of the month is " & dtmDate & "."
        Exit Do
    Else
        dtmDate = dtmDate + 1
    End If
Loop

What’s that? Sure, we can spare a few minutes to explain how this script works. After all, it’s Friday; we pretty much have the place to ourselves.

As you can see, we start out by assigning a date – November 1, 2006 – to a variable named dtmDate:

dtmDate = #11/1/2006#

Note. Are the pound signs – the # characters – required when assigning a date to a variable? Not necessarily, but it’s a good idea. When VBScript sees the pound signs there’s no confusion; it knows that the value being assigned is supposed to be a date.

Oh, right: there’s a very good reason why we set the date to November first. After all, we want to determine the first Friday in the month; therefore we need to start with day 1, just in case the first day in November is a Friday. If it is, great. If it isn’t, then we’ll check to see if the second day in the month is a Friday. And so on. At least that’s the plan; let’s see if it works.

After assigning the date we then set up a Do Until loop that runs until a variable named x is equal to 1. Because x has never been assigned a value, its current value is 0; in fact, the value of x will always be a zero. In other words, we’ve set up a loop designed to run forever. But don’t panic: after all, in the scripting world forever doesn’t always take very long.

Note. What does that mean? Good question; hopefully we’ll figure it out ourselves before we reach the end of this column.

Inside the loop the very first thing we do is use the Weekday function to determine the integer value corresponding to the given date:

intDayOfWeek = Weekday(dtmDate)

Depending on the day of the week, the Weekday function will return one of the following values:

Day of the Week | Value
Sunday | 1
Monday | 2
Tuesday | 3
Wednesday | 4
Thursday | 5
Friday | 6
Saturday | 7

Because we’re interested in only Fridays we use this line of code to determine whether or not the Weekday value for 11/1/2006 (the current value of dtmDate) is equal to 6:

If intDayOfWeek = 6 Then

Suppose the Weekday value for 11/1/2006 is 6 (it isn’t, but suppose it is). In that case we echo back the fact that the first Friday in November, 2006 is 11/1/2006 and then call the Exit Do statement to exit our not-so-endless loop:

Wscript.Echo "The first Friday of the month is " & dtmDate & "."
Exit Do

Note. Yes, even though our loop is designed to run forever all we have to do is call Exit Do and we’ll immediately exit the thing. If only we had a similar function to help us exit those meetings that also seem designed to run forever.

That’s nice, but what if the Weekday value for 11/1/2006 isn’t equal to 6 (which it isn’t)? No problem; in that case we simply add 1 day to the date (making the value of dtmDate equal to 11/2/2006) and then loop around and use the Weekday function to test that date:

dtmDate = dtmDate + 1

This process continues until we finally encounter a Friday. When that happens, we’ll get back a message similar to this, we’ll exit the loop, and the script will terminate:

The first Friday of the month is 11/3/2006.

Nice.

By the way, if you want to get a head start on next year, here’s a script (we’ll leave it up to you to figure out exactly how it works) that reports back the first Friday in every month for the year 2007:

For i = 1 to 12
    dtmDate = CDate(i & "/1/2007")

    Do Until x = 1
        intDayOfWeek = Weekday(dtmDate)
        If intDayOfWeek = 6 Then
            Wscript.Echo "The first Friday of the month is " & dtmDate & "."
            Exit Do
        Else
            dtmDate = dtmDate + 1
        End If
    Loop
Next

That’s going to give us back data that looks like this:

The first Friday of the month is 1/5/2007.
The first Friday of the month is 2/2/2007.
The first Friday of the month is 3/2/2007.
The first Friday of the month is 4/6/2007.
The first Friday of the month is 5/4/2007.
The first Friday of the month is 6/1/2007.
The first Friday of the month is 7/6/2007.
The first Friday of the month is 8/3/2007.
The first Friday of the month is 9/7/2007.
The first Friday of the month is 10/5/2007.
The first Friday of the month is 11/2/2007.
The first Friday of the month is 12/7/2007.

The secret here is that we’ve added a second loop. We want to report data back for 12 months, so we set up a For Next loop that runs from 1 through 12. Then, we use the counter variable for that loop (which we named i) in a little formula that determines the first day in each month:

dtmDate = CDate(i & "/1/2007")

As you can see, when i is equal to 1 we’ll be assigning dtmDate the value 1/1/2007; when i is equal to 2 (which it will be the second time through the loop) then we’ll be assigning dtmDate the value 2/1/2007. Etc.

Note. The function CDate? CDate is short for character-to-date, and is designed to transform a string value into a date-time value. Again, we’re just making sure that VBScript treats dtmDate as a date. And sure, we could have used CDate (instead of pound signs) when assigning the date in our first script. Why didn’t we? No reason, really; we just felt like using pound signs.

One last thing. If the Scripting Editor tries to slip a note in here saying that this column exaggerates the number of people who skip … lunch … on a Friday, well, look at it this way. Who are you going to trust when it comes to being lazy and weaseling out work: the Scripting Editor, or the Scripting Guy who writes this column? (Or, to put it a little more accurately, the Scripting Guy who writes this column if he doesn’t have to coach baseball or if he isn’t taking a week off or ….)

That’s what we thought.


How Can I Determine the Week of the Month a Date Falls In?

Hey, Scripting Guy! How can I determine the week of the month a date falls in?

-- AK

Hey, AK. You’re probably familiar with the book Moby Dick, the story of a crazy sea captain who became obsessed with hunting down and finishing off the great white whale. Well, this question turned out to be the Moby Dick of the scripting world. When we first read it we turned to the VBScript documentation, assuming we’d find the WeekOfTheMonth function in there. We didn’t: no such function exists. We then did a cursory search of the newsgroups and couldn’t find an answer there either. And then we became absolutely obsessed with figuring out how you can determine the week of the month a date falls in.

Well, sort of obsessed: we didn’t actually do anything about it, although every now and then we’d think, “Man, we should try to figure out that week of the month thing.” And then finally, a couple days ago, we sat down and tried to come up with a solution.

And guess what: we came up with one. We’re not claiming this is a particularly elegant solution, and we have no doubt that there are better and more efficient ways of solving this problem. But hey, it works, and that’s really the bottom-line, right? (As you might expect, it’s always us less-than-elegant types who argue that elegance doesn’t really matter.)

Here’s a script that will tell you the week of the month that December 19, 2005 falls in:

dtmTargetDate
= #12/19/2005#

dtmDay = DatePart("d", dtmTargetDate)
dtmMonth = DatePart("m", dtmTargetDate)
dtmYear = DatePart("yyyy", dtmTargetDate)

dtmStartDate = dtmMonth & "/1/" & dtmYear
dtmStartDate = CDate(dtmStartDate)

intWeekday = Weekday(dtmStartDate)
intAddon = 8 - intWeekday

intWeek1 = intAddOn
intWeek2 = intWeek1 + 7
intWeek3 = intWeek2 + 7
intWeek4 = intWeek3 + 7
intWeek5 = intWeek4 + 7
intWeek6 = intWeek5 + 7

If dtmDay <= intWeek6 Then
strWeek = "Week 6"
End If

If dtmDay <= intWeek5 Then
strWeek = "Week 5"
End If

If dtmDay <= intWeek4 Then
strWeek = "Week 4"
End If

If dtmDay <= intWeek3 Then
strWeek = "Week 3"
End If

If dtmDay <= intWeek2 Then
strWeek = "Week 2"
End If

If dtmDay <= intWeek1 Then
strWeek = "Week 1"
End If

Wscript.Echo strWeek

This is a semi-complicated script so we only have room in this column to provide an overview of how it works; if you want the gory details you’ll have to sort them out for yourself. We should also note that this script assumes that the first week in the month is whatever week day 1 falls in; we’re not interested in the first full week of the month or the first week with a workday in it or anything like that. If you want to calculate weeks like that, well, you’re on your own there, too.

But enough about that. The script itself is built on the assumption that the key to determining the week of the month that a date falls in is this: determining the day of the week for the first day of the month. Why? Well, suppose day 1 falls on a Saturday. That means day 2 falls on a Sunday which – for our purposes – would mean that day 2 occurs in week 2. But suppose day 1 falls on a Friday? In that case, day 2 falls on a Saturday which – again, for our purposes – would mean that day 2 falls in week 1.

Confused? Take a look at the calendar for December 2005. We’re saying that December 1, 2, and 3 fall in week 1; December 4 marks the first day of week 2:

From this picture we know that our date – December 19, 2005 – falls in week 4. But how do we determine that programmatically?

We begin by using VBScript’s DatePart function to extract the day (d), month (m), and year (yyyy) from the date:

dtmDay
= DatePart("d", dtmTargetDate)
dtmMonth = DatePart("m", dtmTargetDate)
dtmYear = DatePart("yyyy", dtmTargetDate)

We then construct a new date representing December 1, 2005 using this code:

dtmStartDate = dtmMonth & "/1/" & dtmYear
dtmStartDate = CDate(dtmStartDate)

In the first line we put together the date string – 12/1/2005 – and in the second line we use the CDate function to ensure that VBScript treats the string as a date-time value. That’s not too bad, is it?

But now it gets a little crazy. We begin by using the Weekday function to determine the day of the week for December 1st:

intWeekday = Weekday(dtmStartDate)

Weekday returns an integer value ranging from 1 (Sunday) to 7 (Saturday). Because December 1st falls on a Thursday we get back a 5.

Next we need to determine which date represents the last day in week 1; that’s important because once we know that we can calculate the end dates for each week. And once we know that we can figure out which week any given date falls in. To determine the end date for week 1 we use this code:

intAddon = 8 - intWeekday

Why do we use that code? Good question. If you look at the calendar, December 1 occurs on a Thursday, which has an integer value of 5. If we subtract 5 from 8 we get 3. And take another look at the calendar: December 3rd just happens to be the last day of week 1. Turns out that all we have to do is subtract the Weekday value from 8 and we’ll know the date for the last day of week 1.

Yeah, we know. But play around with it a bit and you’ll see that it works.

Now that we know that week 1 ends on December 3rd we can easily calculate the end dates for every other week; after all, week 2 will end on December 3rd plus 7 days, or December 10th. Because a month can have as many as six weeks we go ahead and calculate end dates for six weeks (the fact that most months won’t have six weeks doesn’t matter):

intWeek1
= intAddOn
intWeek2 = intWeek1 + 7
intWeek3 = intWeek2 + 7
intWeek4 = intWeek3 + 7
intWeek5 = intWeek4 + 7
intWeek6 = intWeek5 + 7

This next part we could have done in a few less lines of code, but we wanted to make it clear what we’re doing; therefore we put the code together using a bunch of If-Then statements. We’ve already determined the day part of our target date: 19. And now we’ve determined the end date for each week in the month:

Week | End Date
6 | 38
5 | 31
4 | 23
3 | 17
2 | 10
1 | 3

Our next step is to see if our day – 19 – is less than or equal to the end date for the various weeks. Is 19 less than the week 6 end date of 38? Yes, so we set the value of the variable strWeek to “Week 6”. Is it less than the week 5 end date of 31? Yes, so we now set the value of strWeek to “Week 5”. How about week 4? Yes, so now strWeek equals “Week 4”. Make sense?

Ok, so what about week 3? Is 19 less than or equal to 17? No, so we don’t change the value of strWeek; we leave it alone. When we finish the last of our If-Then statements we echo the results: turns out that December 19, 2005 falls in week 4 of the month.

Again, a cursory explanation for a semi-complicated script and we apologize for that. But at least the script works. And as crazy as the script might seem, it was still a heck of a lot easier than trying to hunt down and kill a great white whale (even when it comes to our obsessions we try to take the easiest possible route).


How Can I Determine Which Day of the Year a Date Falls On?

Hey, Scripting Guy! How can I determine which day of the year a date falls on? For example, March 2, 2006 is the 61st day of the year.

-- TW

Hey, TW. You know, we almost feel guilty about answering this question. Why? Well, after all, we can answer it by writing just one line of code:

Wscript.Echo DatePart("y", "3/2/2006")

Note. So if it’s that easy why do we almost feel guilty about answering the question; why don’t we actually feel guilty? Well, to be honest, we believe the Scripting Guys deserve a break once in awhile. After all, look at a typical Scripting Guys day: we might have to write a Scripting Week song or put together a crossword puzzle; for that matter, we might be forced to play around with Microsoft Agent or Windows Media Player. This is obviously arduous and back-breaking work, and yet we do it day in and day out. (Well, except for the extended vacation time we all just finished taking.) You see what we have to go through on a daily basis; surely you can cut us some slack every now and then?

As far as determining the day of the year for a particular date, well, needless to say that doesn’t require much effort: we simply call the DatePart function and pass it two parameters:

· “y”, which tells DatePart that we want to know the day of the year.

· “3/2/2006”, which tells DatePart which date we need information for.

That’s all we have to do. In turn, DatePart calculates the day of the year and the script echoes the value – 61 – back to us.

What’s cool about DatePart is that it can retrieve other date-related information besides day of the year. For example, want to know the week of the year that 3/2/2006 falls in? Here you go:

Wscript.Echo DatePart("ww", "3/2/2006")

In this case DatePart returns a 9, meaning that 3/2/2006 falls in the ninth week of 2006. How about the quarter:

Wscript.Echo DatePart("q", "3/2/2006")

Did someone ask what day of the week 3/2/2006 falls on? Here’s a script that can help you with that:

intDay = DatePart("w", "3/2/2006")

Select Case intDay
    Case 1
        Wscript.Echo "Sunday"
    Case 2
        Wscript.Echo "Monday"
    Case 3
        Wscript.Echo "Tuesday"
    Case 4
        Wscript.Echo "Wednesday"
    Case 5
        Wscript.Echo "Thursday"
    Case 6
        Wscript.Echo "Friday"
    Case 7
        Wscript.Echo "Saturday"
End Select

Note. If you can’t stand the suspense any longer, 3/2/2006 falls on a Thursday.

Here’s a complete list of all the different date parts that DatePart can return:

Setting | Description
yyyy | Year
q | Quarter
m | Month
y | Day of the year
d | Day
w | Day of the week
ww | Week of the year
h | Hour
n | Minute
s | Second

And now, if you’ll excuse us, we have another grueling day to try and get through. Should we have filet mignon again at lunch, or should we try something new today? It’s a tough job, but someone has to do it.

Note. Please don’t point out to our managers that no one has to do this. Thanks!


How Can I Get Yesterday’s Date?

Hey, Scripting Guy! I’m writing a script, and I need to know yesterday’s date. I can use Date() to figure out today’s date, but how do I figure out yesterday’s date?

-- SS, Tacoma, WA

Hey, SS. You’re right; it’s easy to determine today’s date using a script:

dtmToday = Date()
Wscript.Echo dtmToday

But what about yesterday’s date? Listen, we’ve got good news for you: it’s just as easy to determine yesterday’s date as it is today’s date. That’s because VBScript can do date arithmetic: give it today’s date, and then you can simply subtract 1 day to determine yesterday’s date. In other words:

dtmYesterday = Date() - 1
Wscript.Echo dtmYesterday

Want to know tomorrow’s date? You’re way ahead of us:

dtmTomorrow = Date() + 1
Wscript.Echo dtmTomorrow

Cool, huh? Incidentally, you can do even fancier date arithmetic using VBScript. Want to know the date that’s 37 days from July 30, 2004? You can calculate that using the DateAdd function:

dtmDate = DateAdd("d", 37, "July 30, 2004")
Wscript.Echo dtmDate

For more information about the DateAdd function, see this portion of the Microsoft Windows 2000 Scripting Guide.


How Can I Separate Hours and Minutes in a Date-Time Value?

Hey, Scripting Guy! In determining system uptime I get back a value like 65.75 hours. How can I convert this value to "65 hours and 45 minutes"?

-- JM

Hey, JM. We’ll see what we can do for you, but we have to be honest: this is a very sad day for the Scripting Guys. Thanks to the baseball playoffs the Scripting Guy who writes this column had never seen the beginning of Sunday Night Football on TV. This past Sunday he watched the opening of the show for the first time, and was shocked – shocked – by what he saw: One of the great rock-and-roll songs of all time – Joan Jett’s I Hate Myself for Loving You – being twisted (by Pink) into some futile attempt to replicate the success of the Monday Night Football theme song. Oh, Joan: say it isn't so. (Editor’s Note: The Scripting Editor seems to be in the minority by liking the original and yet also kind of liking the Sunday Night version.)

Needless to say, that was very distressing. If Hank Williams, Jr. (who sings the Monday Night Football theme song) wants to sell out, well, so what? Good for him. But not Joan Jett. What's next? Bob Dylan selling iPods? The Rolling Stones selling everything from life insurance to cars (or software)?

Never mind; we don’t want to know.

So does that mean that everything is for sale these days, that nothing is sacred anymore? Not quite. Fortunately, the best things in life – like scripts that can convert the value 65.75 into hours and minutes – are still free:

numValue = 65.75

arrValues = Split(numValue, ".")

numHours = arrValues(0)
numMinutes = (arrValues(1) / 100) * 60

Wscript.Echo numHours & " hours and " & numMinutes & " minutes"

As you can see, this script (brought to you without commercial interruption) starts out by assigning the time value – 65.75 – to a variable named numValue. That was easy. Now comes the hard part: how do we separate the integer portion of the value from the decimal portion of the value? That is, how do we separate the 65 and the 75?

What’s that? Oh, right: we’re supposed to answer that, aren’t we? Well, although there might be other ways to do this, we decided to use the VBScript Split function to split the value 65.75 on the decimal point (.). That’s what we do with this line of code:

arrValues = Split(numValue, ".")

When we execute that bit of code we get back an array (an array we named arrValues) consisting of two items:

· 65

· 75

By happy coincidence, those just happen to be the two values we wanted to extract from 65.75. Nice, huh?

OK; now that we have our two values what do we do with them?

Well that hardly seems fair; you mean we have to answer that question, too? Oh, very well. The 65 (which represents 65 hours) is fine just as it is; therefore we simply take that value and assign it to a variable named numHours. Because 65 is the first item in the array it has an index number of 0; therefore, we assign numHours the array item that has the index number of 0. In other words:

numHours = arrValues(0)

The 75 is a different story. After all, this doesn’t represent 75 minutes; it represents .75 hours, a value we need to convert to minutes. Fortunately (if not for you then for the Scripting Guy who writes this column) this doesn't require much in the way of fancy mathematics; in fact, all we have to do is convert 75 to .75 (something we can do by dividing 75 by 100) and then multiply .75 by 60. Believe it or not, that will give us the number of minutes (.75 x 60 = 45). That’s what we do with this line of code:

numMinutes = (arrValues(1) / 100) * 60

Yes, it looks a little complicated, but it’s actually pretty straightforward. Because 75 is the second item in the array, it has the index number 1; that’s why we reference arrValues(1). As you can see, we simply take that value and divide it by 100, then take the result of that equation and multiply it by 60. And then we grab the product of those two numbers and stash it in a variable named numMinutes. At this point we’re all-but done; after all, we have the number of hours stored in the variable numHours and the number of minutes stored in the variable numMinutes. All we have to do now is echo back these two values:

Wscript.Echo numHours & " hours and " & numMinutes & " minutes"

When we do, we get back the following:

65 hours and 45 minutes

That should do the trick, JM. Thanks for the question, and don’t worry: unlike the rest of the world, the Scripting Guys have never sold out.

Note. OK, if you want to get technical, no one has ever actually asked the Scripting Guys if they would sell out. But, hey, let's not quibble over minor details like that, OK? The important thing is that the Scripting Guys have never sold out, ever.

Although they are willing to listen to offers, if you know what we mean ….


How Can I Subtract a Specified Number of Days from a UTC Value?

Hey, Scripting Guy! Suppose I get a date-time value in UTC format. How can I subtract x number of days from that value?

-- MW

Hey, MW. First of all, we’d like to thank you for your question; in addition, we’d like to welcome you and everyone else to today’s Open House in honor of the 500th Hey, Scripting Guy! column. We know that a few of you are wondering why we’re so excited about the 500th column; isn’t 500 just another number? Well, to be honest, we’re pretty proud of this achievement. In fact, the consensus among the Scripting Guys is that producing 500 daily columns on system administration scripting just might be the greatest feat in literary history.

But don’t take our word for it. Let’s check the mail and see what some of our loyal readers have to say about this:

Hey, Scripting Guy! Congratulations on column number 500; that’s pretty good. But did you know that a South African novelist named Mary Faulkner wrote 904 books in her lifetime? Now that’s impressive.

-- RS

Um, no, RS, we didn’t know about Mary Faulkner. But, then again, we write about scripting, you know, highly-technical stuff. Mary Faulkner was a novelist; how hard could it be to write 904 novels?

Let’s try another letter:

Hey, Scripting Guy! Wow: 500 columns, and all of them on highly-technical stuff like scripting. Congratulations; I can’t think of anyone who’s been able to top that achievement.

Well, OK, maybe Isaac Asimov. Did you know that, in addition to 463 books, he wrote over 1600 full-length essays on highly-technical topics? In fact, Asimov is the only writer to have published books that encompass all 10 of the major Dewey Decimal categories. On top of that, he wrote an estimated 90,000 letters and postcards during his lifetime!

-- SB

Gee, that’s … great; good for him. Let’s try one more:

Hey, Scripting Guy! Although everyone is focusing on the 500th column, I happened to notice that you guys are approaching your second anniversary (August 2, 2006 to be exact). Wow: writing a column every day for the past two years is really something. Did anyone ever tell you that Dear Abby wrote her column every day for 46 years, from 1956 through 2002?

-- CM

You know, rather than spend any more time on letters maybe we should get right to today’s question.

By the way, what is today’s question?

Oh, that’s right: subtracting x number of days from a UTC date-time value. For those of you who aren’t familiar with the term, UTC is short of Universal Time Coordinate; when you get a UTC value (as you often will when working with WMI) you end up with a date that looks something like this:

20060720132442.000000-420

Note. True. But many of us have had dates that looked even scarier than that. And a few of us were the dates that looked even scarier than that.

We won’t bother explaining how to decipher this value; if you have a burning desire to know how to do that then take a peek at the Microsoft Windows 2000 Scripting Guide. Instead, let’s focus on the topic at hand, subtracting a specified number of days from this UTC value. How do we do that? To tell you the truth, we have no idea.

But that’s OK: if we can’t subtract a specified number of days from our UTC value then we’ll simply turn that UTC value into something that we can subtract a specified number of days from. What we’re going to do is convert the UTC value into a regular date-time value, subtract the desired number of days, and then convert the answer back to a UTC value. This will have the desired effect, and no one will ever know that we cheated:

Const CONVERT_TO_LOCAL_TIME = True

Set dtmStart = CreateObject("WbemScripting.SWbemDateTime")
dtmStart.SetVarDate Now, CONVERT_TO_LOCAL_TIME
Wscript.Echo dtmStart

dtmRegular = dtmStart.GetVarDate(CONVERT_TO_LOCAL_TIME)
Wscript.Echo dtmRegular

dtmNew = DateAdd("d", -37, dtmRegular)
Wscript.Echo dtmNew

Set dtmEnd = CreateObject("WbemScripting.SWbemDateTime")
dtmEnd.SetVarDate dtmNew, CONVERT_TO_LOCAL_TIME
Wscript.Echo dtmEnd

A couple of notes here. First, we opted to use the SWbemDateTime object for this script. The good news is that this makes it very easy to switch back and forth between UTC dates and regular dates. The bad news? This limits this script to working on Windows XP and Windows Server 2003. Could we modify this script to work on, say, Windows 2000? Yes, but there’s quite a bit of explanation involved in doing so. For now we’ll stick with this version; if anyone needs a Windows 2000 version let us know and we’ll see what we can come up with.

Note. In the meantime, you can find functions for converting between date-time formats here. Oh: and over here, too.

Second, we should also point out that the script is a tiny bit longer than it needs to be. That’s because we periodically pause for a moment and echo back a date-time value; we do that simply so you can watch what happens as we switch from one format to another. That seemed a little more educational, and we Scripting Guys are all about education. Along the same lines, we create an extra variable or two, just so you don’t get confused as to which variable is holding which information.

Unlike people (and we aren’t mentioning any names, Mary Faulkner) who just like to show off how prolific they are. Besides, if you add up all the books the Scripting Guys have written, well, we’re only 903 behind Ms. Faulkner. We’re right behind you and gaining fast, Mary!

But back to the script. We start out by defining a constant named CONVERT_TO_LOCAL_TIME and setting the value to True; we’ll use this constant to tell the SWbemDateTime object that we want to convert our date-time value to local time (as opposed to Greenwich Mean Time).

Note. You know, we never thought about this before, but if you live in Greenwich, England it probably doesn’t matter whether you convert to local time or not. The Scripting Guy who writes this column was actually in Greenwich, England last summer. Oddly enough, he never once thought about UTC date-time values while he was there. Go figure.

After defining the constant we create an instance of the WbemScripting.SWbemDateTime object, then call the SetVarDate method to convert the current date and time to a UTC date-time value:

dtmStart.SetVarDate Now, CONVERT_TO_LOCAL_TIME

Notice that we pass SetVarDate two parameters: the date-time value to be converted, and the constant CONVERT_TO_LOCAL_TIME. We then echo back the date-time value we just created, a value that should look something like this:

20060720134533.000000-420

Of course, now that we have a UTC value we want to get rid of it; we’ve already decided that we don’t know how to subtract x number of days from something like that. Therefore, we take the object reference dtmStart and call the GetVarDate method:

dtmRegular = dtmStart.GetVarDate(CONVERT_TO_LOCAL_TIME)

This simply converts our UTC value back to a more human-friendly date-time value, a value we store in a variable named dtmRegular. We then echo back the value of dtmRegular, which should look similar to this:

7/20/2006 1:45:33 PM

Still with us? Good. Now that we have a “real” date and time to work with we can use VBScript’s DateAdd method to subtract a specified number of days (say, 37) from that value. That’s what we do here:

dtmNew = DateAdd("d", -37, dtmRegular)

As you can see, this isn’t especially complicated. We simply call DateAdd, passing it three parameters:

· “d”, which tells the function to work with days as the unit of measure. (As opposed to hours, minutes, years, etc. For a complete list of allowed units see the Scripting Guide.)

· -37, the number of days to subtract. If we wanted to add 37 days we’d simply leave off the minus sign.

· dtmRegular, the variable containing our base date, the date we’re going to add our subtract time from.

The results of this function get stored in yet another variable (dtmNew), a variable which will contain a value similar to this:

6/13/2006 1:45:33 PM

And there you go: June 13, 2006 occurred 37 days prior to July 20, 2006. Not bad, huh?

Just for the heck of it, we then convert the June 13th value back to a UTC value. That’s what this line of code does:

dtmEnd.SetVarDate dtmNew, CONVERT_TO_LOCAL_TIME

In turn, that gives us a UTC value just like this one:

20060613134533.000000-420

The circle of life is complete.

If you’d just as soon forego all that echoing of intermediate stages along the way, well, here’s a condensed script that simply reports back the desired date and time in UTC format:

Const CONVERT_TO_LOCAL_TIME = True

Set dtmStart = CreateObject("WbemScripting.SWbemDateTime")
dtmStart.SetVarDate Now, CONVERT_TO_LOCAL_TIME
dtmRegular = dtmStart.GetVarDate(CONVERT_TO_LOCAL_TIME)
dtmRegular = DateAdd("d", -37, dtmRegular)
dtmStart.SetVarDate dtmRegular, CONVERT_TO_LOCAL_TIME

Wscript.Echo dtmStart

There you have it, MW: column number 500 is officially in the books. Just think: 1,101 more and we can claim to have passed Isaac Asimov. And then, 54 years from now, we’ll set our sights on Dear Abby.

You know, on second thought, maybe we’ll just try and catch Isaac Asimov and then call it good. You win, Dear Abby: you beat us fair and square.


How Can I Subtract Days From a UTC Date-Time Value in Windows 2000?

Hey, Scripting Guy! On a Windows 2000 computer, how can I subtract days from a UTC date-time value?

-- KP

Hey, KP. Yes, we do blame all of this on Scripting Guy Dean Tsaltas. The Scripting Guy who writes this column has known Dean for six years now, and Dean has spent those entire six years complaining about the weather in the Seattle area. Not because it was bad, mind you, but because it was boring. “In the spring and summer it’s always 75 degrees and sunny,” he’d say. “In the fall and winter it’s always 50 degrees and cloudy. You could set your watch by it.”

Note. Technically, yes, you could set your watch by it. But we don’t recommend it. After all, if someone asks you the time and you reply, “Oh, it’s about 20 after 50 degrees and cloudy,” well, that just doesn’t seem all that helpful to us.

Anyway, Dean obviously complained one too many times, and, in the process, upset the weather gods. In retaliation– and in the past 12 weeks or so – we’ve had record-setting rainfall, unprecedented windstorms and power failures, and ice storms. Oh, and this past Wednesday, an unholy mixture of sleet, hail, and snow, a crazy combination which – in the city of Kirkland, at least – turned the roads from bare and dry to sheets of ice in a matter of minutes.

The best part, of course, is that the Scripting Guy who writes this column took the bus on Wednesday, primarily because he wanted the Scripting Son to have the better car, just in case the predictions of heavy snow came true. Although he is somewhat ashamed to admit this, the Scripting Guy who writes this column usually doesn’t take the bus, mainly because he has to transfer. But on Wednesday he didn’t have to transfer.

That’s because his first bus could only get within 8 or 9 blocks of the transfer station before a car darted in front of it. That caused the bus driver to have to stop on an icy hill, and that caused the bus to get stuck. Consequently, all the passengers had to get off and walk to the transfer station in downtown Kirkland.

In turn, that meant that the Scripting Guy who writes this column missed his transfer; he could see the bus rounding the corner and driving off just as he reached the transfer station. Not wanting to stand around in the cold for 30 minutes waiting for the next bus, he decided to walk for awhile.

Within a few minutes he caught up to and then passed his bus, a bus which hadn’t moved an inch since rounding he corner. Shrugging his shoulders the Scripting Guy trudged on; within a few minutes he passed another bus, a bus which had to have been sitting in traffic for at least half an hour. Not too far down the road he passed a third bus, one which had to have been sitting in traffic for at least an hour. Although it’s about 5 miles from downtown Kirkland to the Scripting Home, the Scripting Guy who writes this column decided he’d just walk the rest of the way.

And for good reason: for the first 3 miles or so of his journey the only time he saw a car move was when a driver gave up, pulled off to the side of the road, and parked. At that point each car would then creep forward one car length. But that didn’t happen too often. After all, this is Seattle, and people don’t get out of their car for anything around here. You name it and we have a drive-through window for it: the bank, the fast food place, the coffee shop, the pharmacy. Microsoft has yet to install a drive-through window where employees can pull up and do their work without ever having to leave their car, but we’re sure it’s coming.

Anyway, an hour or so later the Scripting Guy who writes this column walked through his front door, a little cold and quite a bit wet. Thanks a lot, Dean!

On the bright side, though, the walk did give him plenty of time to come up with a script that can subtract a specified number of days (say, 30) from a UTC value. The script is still a bit cold and a bit wet, but here goes:

strComputer = "."

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

Set objOS = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

For Each strOS in objOS
    dtmInstallDate = strOS.InstallDate
    dtmDate = CDate(Mid(dtmInstallDate, 5, 2) & "/" & _
        Mid(dtmInstallDate, 7, 2) & "/" & Left(dtmInstallDate, 4))
    Wscript.Echo dtmDate - 30
Next

Obviously the first step in subtracting days from a UTC (Universal Time Coordinate) value is to get yourself a UTC value. Rather than create a make-believe value we opted to use a real one: the date that the operating system was installed on the local computer. To that end, we start off by connecting to the WMI service on the local computer, then use this line of code to retrieve a collection consisting of the current operating system installed on that computer:

Set objOS = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

Note. Yes, we know: it would be very cool if, on a multi-boot machine, this code could retrieve a collection of all the operating systems installed on the computer. Unfortunately, though, that’s not the case; the best we can do is identify the operating system currently in use.

Next we set up a For Each loop to loop through our collection (which, again, will have only one item in it). Inside that loop we grab the value of the InstallDate property and store it in a variable named dtmInstallDate. On our test computer that means that dtmInstallDate is equal to this:

20060301162313.000000-480

Note. No, the Scripting Guy who writes this column didn’t freeze his brain. That really is the installation date; it’s just in UTC format. If you need more information on deciphering a UTC value see the Microsoft Windows 2000 Scripting Guide.

Now, if we were running Windows XP, Windows Server 2003, or Windows Vista, our job would be much easier; that’s because we could use the SWbemDateTime object to easily convert this UTC value to a “real” date-time value. However, we’re running on Windows 2000; therefore, we need to use this crazy-looking code in order to convert 20060301162313.000000-480 to a real date:

dtmDate = CDate(Mid(dtmInstallDate, 5, 2) & "/" & _
    Mid(dtmInstallDate, 7, 2) & "/" & Left(dtmInstallDate, 4))

As it turns out, there’s actually a little rhyme and reason to a UTC value:

· The first four digits – 2006 – represent the year.

· The next two digits – 03 – represent the month.

· The next two digits – 01 – represent the day.

In other words, our UTC value (ignoring hours and minutes and seconds) happens to be March 1, 2006. That oddball line of code is simply using the Mid and Left functions to grab the appropriate characters in the string, combine them with a few forward slash characters (/), and then create the date value 03/01/2006. In other words:

· Mid(dtmInstallDate, 5, 2) grabs the month: characters 5 and 6.

· Mid(dtmInstallDate, 7, 2) grabs the day: characters 7 and 8.

· Left(dtmInstallDate, 4) grabs the year: characters 1, 2, 3, and 4.

Put them all together and run them through the CDate function (which converts a string value to a date-time value) and we end up with a “real” date: 03/01/2006. And once we have a real date, subtracting 30 days from it (and echoing back the value) is as easy as this:

Wscript.Echo dtmDate - 30

Does that really work? Of course it does:

1/30/2006

And, sure, you could convert 1/30/2006 to a UTC value if you wanted to or needed to; for more information, see the Scripting Guide. We’d loan you our copy, except we had to burn it in order to keep warm during our 5-mile slog through the snow and sleet.

Thanks again, Dean.


How Can I Tell if a Date Falls within a Specified Time Period?

Hey, Scripting Guy! How can I tell if a date falls within a specified time period?

-- JW

Hey, JW. By amazing coincidence on our way to work this morning we heard two different commercials, from two different companies, touting “Christmas in July.” That prompted us to answer your question as soon as we got to the office. After all, if these companies had a copy of the script you’re looking for, then maybe they’d know that Christmas doesn’t come in July.

Note. Of course, we could be wrong about that, so maybe you should send all the Scripting Guys a present anyway, just to be on the safe side.

So how do we know that Christmas doesn’t come in July? We simply ran the following script:

dtmStartDate = #7/1/2005#
dtmEndDate = #7/31/2005#
dtmTargetDate = #12/25/2005#

If dtmTargetDate >= dtmStartDate AND dtmTargetDate <= dtmEndDate Then
    Wscript.Echo "The target date is within the specified range."
Else
    Wscript.Echo "The target date is not within the specified range."
End If

As you can see, we begin by assigning values to three variables. The variables dtmStartDate and dtmEndDate represent our date range; in this case, July 1, 2005 through July 31, 2005. The variable dtmTargetDate is the date in question: we want to know if this day (Christmas Day) comes in July.

To do that, we need to know if the target date is greater than or equal to the start date and if the target date is less than or equal to the end date. Here’s the line of code that checks for that:

If dtmTargetDate >= dtmStartDate AND dtmTargetDate <= dtmEndDate Then

In order to be true a date must meet both conditions. December 25, 2005 is greater than or equal to the start date of July 1, 2005, so our target date meets condition 1. However, December 25, 2005 is not less than or equal to July 31, 2005. The target date fails condition 2, which means that the statement is not true. (Remember, both conditions must be true.) As a result, we know that Christmas doesn’t come in July, and we echo a message to that effect. Had both conditions been true, we would have echoed the jolly holiday message, “The target date is within the specified range.”

Hope that helps, JW. Merry Christmas to all and to all a good night.


How Can I Tell If a Script is Running on the First or Second Monday of a Month?

Hey, Scripting Guy! We have a logon script that needs to perform certain tasks only on the first and second Mondays of the month. How can I determine whether the script is running on the first or second Monday in a month?

-- WA

Hey, WA. You know, a few months ago the Scripting Guy who writes this column noticed that his watch had stopped; consequently, he went to the store and paid $20 to get a new battery. Less than two months later his watch stopped again. Could the battery have died after just two months? Maybe, but, then again, maybe his watch was truly broken. With that in mind the Scripting Guy who writes this column went back to the store and this time, rather than buy a new battery, he bought a new watch that was on sale for $9.95.

Now, we know what you’re thinking, you’re thinking, “Why would one of the world-famous Scripting Guys wear a $9.95 watch? Shouldn’t the Scripting Guys all wear Rolexes or something like that?”

No, they shouldn’t. And here’s why. Last night this Scripting Guy was boiling water in preparation for cooking some noodles. (Of course, the Scripting Guy who writes this column can cook. Any time you want boiling water for dinner just let him know.) As he reached across the stove to turn down the burner, his $9.95 watch somehow slipped off his arm and plopped into the pot of boiling water.

Amazingly enough, the watch continued to work; being immersed in boiling water for a minute or so had no effect on it whatsoever. Try doing that with a Rolex!

Note. Never mind; whatever you do don’t try doing that with a Rolex. Microsoft’s insurance company probably won’t be too happy if thousands of people suddenly start filing claims for new Rolex watches just because the Scripting Guys told them to try dunking those watches in boiling water.

So what does any of that have to do with writing a script that can determine whether it’s running on the first or second Monday of the month? As near as we can tell, nothing. But it you’ve ever wondered what the Scripting Guy who writes this column is really like, well, now you know.

Note. But please don’t tell the other Scripting Guys about this incident. After all, they look at the Scripting Guy who writes this column as if he was Mary Poppins: practically perfect in every way.

Editor’s Note. Please don’t tell the Scripting Guy who writes this column that the other Scripting Guys know better. He’s happy in his little fantasy world, we’d hate to ruin that for him.

But don’t despair, WA; the following chunk of code should help a bit more when it comes to writing a script that can determine whether it’s running on the first or second Monday of the month:

Dim arrMondays(1)

dtmMonth = Month(Date)
dtmYear = Year(Date)
dtmDate = CDate(dtmMonth & "/1" & "/" & dtmYear)

Do Until i = 1
    intWeekDay = Weekday(dtmDate)
    If intWeekDay = 2 Then
        arrMondays(0) = dtmDate
        arrMondays(1) = dtmDate + 7
        Exit Do
    End If
    dtmDate = dtmDate + 1
Loop

For Each strMonday in arrMondays
    If Date = strMonday Then
        Wscript.Echo "Carry out the task."
    Else
        Wscript.Echo "Don't carry out the task."
    End If
Next

So how does this script work? You would have to ask us that, wouldn’t you?

Well, let’s see if we can figure it out. We start out by declaring an array named arrMondays, and sizing this array so that it can hold two items:

Dim arrMondays(1)

Why two items? Because we need to determine the dates of the first two Mondays in the month. Two Mondays, two slots in the array.

Next we use this block of code to construct a date equal to the first day of the current month:

dtmMonth = Month(Date)
dtmYear = Year(Date)
dtmDate = CDate(dtmMonth & "/1" & "/" & dtmYear)

As you can see, in the first two lines of code we use the Month function to determine the current month, then use the Year function to determine the current year. In line 3, we string those two values, the number 1 and a few / marks together in order to create a date like this:

2/1/2007

Good question: what is the CDate function for in the preceding line of code? Well, CDate is short for character-to-date; it’s designed to convert a string value to a date-time value. We include it here to make sure that 2/1/2007 gets treated as a date and not as a string.

That’s another good question: why do we care about the first day of the month anyway? Well, we have no idea when the first and second Mondays occur; on top of that, we don’t know any fancy way to determine when the first and second Mondays do occur. So we’re going to simply start with the first day of the month and see if it happens to be a Monday. We’ll then check the second day of the month and see if it happens to be a Monday. And then we’ll check the third day of the month and …. We’ll continue checking, day-by-day, until we find the first Monday. And once we know the date of the first Monday all we’ll have to do is add 7 days and we’ll know the date of the second Monday.

Note. And, yes, suppose we wanted to run the script on the first and third Mondays: in that case, all we’d need to do is add 14 to the date of the first Monday.

Here’s the block of code we use to determine the dates of the first two Mondays:

Do Until i = 1
    intWeekDay = Weekday(dtmDate)
    If intWeekDay = 2 Then
        arrMondays(0) = dtmDate
        arrMondays(1) = dtmDate + 7
        Exit Do
    End If
    dtmDate = dtmDate + 1
Loop

What we’ve done here is set up a Do Until loop designed to run until the counter variable i is equal to 0; in effect this is a loop designed to run until we specifically exit the thing. (Why? Because the way the script is constructed, i will never be equal to 0.) Inside that loop we use the Weekday function to determine the day of the week for the first day in the month:

intWeekDay = Weekday(dtmDate)

The Weekday function returns an integer value for the day of the week: 1 for Sunday, 2 for Monday, 3 for Tuesday, etc. Because we only care about Mondays we check to see if intWeekDay is equal to 2. Suppose it isn’t. In that case, we simply add one day to the date-time value dtmDate, loop around, and make this same check with the second day of the month:

dtmDate = dtmDate + 1

But what happens when we do find a Monday? In that case, we assign the value of dtmDate to the first item in the array; we then add 7 days to that value and assign the resulting date to the second item in the array:

arrMondays(0) = dtmDate
arrMondays(1) = dtmDate + 7

And then we call the Exit Do statement and exit the loop.

All that’s left now is to loop through the two dates in the array and check to see if either one happens to be the current date; if it is, then the script must be running on either the first or second Monday of the month. If it is the first or second Monday we’d then run a specified bit of code. In our sample script, we simply echo back whether we should (or should not) perform the appointed task:

For Each strMonday in arrMondays
    If strMonday = Date Then
        Wscript.Echo "Carry out the task."
    Else
        Wscript.Echo "Don't carry out the task."
    End If
Next

That’s all there is to it.

You know, speaking of time, time is definitely running out on the 2007 Winter Scripting Games; in fact, the Games officially end tomorrow morning (February 23, 2007) at 8:00 Redmond time. We know you have your eyes on one of those Dr. Scripto Bobblehead dolls, and there’s only one way to get one: enter at least one event in the Scripting Games, and thus get your name in the Bobblehead drawing. Did we mention that time is running out? Oh. Well, it’s worth mentioning again: time is running out.


Can I Change a Message Box Caption?

Hey, Scripting Guy! Whenever I use a message box, the caption reads Windows Script Host. Is there any way to change that caption?

-- TT, Reno, NV

Hey, TT. To answer your question, no, not if you’re using Wscript.Echo; in that case, you’re stuck with the caption Windows Script Host. However, you can create a custom caption using the VBScript Msgbox function. For example:

strMessage = "Here is my message."
Msgbox strMessage, 0, "This is my message box"

In this sample script, we’re passing three parameters to the Msgbox function:

Parameter | Description
strMessage | This is simply a variable that contains the message we want to display.
0 | This value indicates that the message box should only have an OK button. By passing different values, you can display message boxes that have Yes and No buttons, message boxes that have Abort, Retry, Ignore buttons, etc.
“This is my message box.” | This is the custom caption you want displayed on your message box.

There’s one important thing to keep in mind here. When you use Wscript.Echo, your messages are displayed in a message box only if you are running under Wscript; if you are running under CScript messages are echoed to the command windows instead. With the Msgbox function, messages are always displayed in message boxes, regardless of which script host you are using.

For more information about modifying message box parameters, see the VBScript documentation on MSDN.


Can I Copy Script Output to the Clipboard?

Hey, Scripting Guy! Is there any way to have my script output copied to the Clipboard?

-- ZW, Marseilles, France

Hey, ZW. Well, as long as you don’t mind crazy work-arounds then, yes, it’s actually pretty easy to copy script output to the Clipboard. To start with, you need to construct a string that contains the desired output. After that, you create an instance of Internet Explorer, and open it to a blank page. You can then take advantage of the capabilities built into the Internet Explorer object model to copy the string to the Clipboard; in particular, you use the clipboardData.SetData method to accomplish this trick. A simple script to copy something to the Clipboard looks like this:

strCopy = "This text has been copied to the clipboard."

Set objIE = CreateObject("InternetExplorer.Application")
objIE.Navigate("about:blank")
objIE.document.parentwindow.clipboardData.SetData "text", strCopy
objIE.Quit

Run the script, then open up Notepad and click Paste; you should see your string in all its glory.

By the way, all of this happens “under the covers;” Internet Explorer won’t actually pop up on your screen. That’s because, by default, any instance of IE you create from a script runs hidden, at least until you do something like this:

objIE.Visible = True

For more information about clipboardData.SetData, see the DHTML reference documentation on MSDN.


Can I Edit .INI Files Using a Script?

Hey, Scripting Guy! Can I edit.INI files using a script?

-- MZ

Hey, MZ. Yes, you can edit .INI files (or any kind of text files, for that matter) using a script. The method you have to use might not be the most elegant process in the world, but it’ll work. And that’s all we really care about, right?

Before we get into the whys and wherefores of editing .INI files, let’s take a look at a sample file. This happens to be a section of the .INI file for Adobe Reader, although most .INI files will look similar:

[OEM
Install]
DisplayWelcomeDlg=YES
DisplayEULA=NO
DisplayTypeOfInstallDlg=NO
DisplaySelectDestDirDlg=YES
DisplayCustomDlg=NO
DisplayUserInfoDlg=NO
DisplayConfirmRegDlg=NO
DisplayStartCopyDlg=NO
DisplayFinishDlg=NO
DisplayFinalMessage=YES
DisplayRebootDlg=YES
ProgGroupName=
DefaultDestDir=
UserName=
UserCompanyName=
UserSerialNumber=

Suppose we want to do two things to this file: we want to change DisplayWelcomeDlg to NO, and we want to set the UserName to Ken Myer. Ideally, we’d write a script that would simply search the file for DisplayWelcomeDlg and change the assigned value to NO, search for the UserName property and set the value to Ken Myer, and then save the changes. Unfortunately, the FileSystemObject – the technology we need to use to read and modify text files – doesn’t have those capabilities. Instead, we’ll have to edit the .INI file by brute force. That probably doesn’t make much sense to you at the moment, but hopefully you’ll understand what we mean by the time you reach the end of this column.

Let’s show you the completed script, and then we’ll explain how it works:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile("sample.ini", ForReading)

Do Until objTextFile.AtEndOfStream
    strNextLine = objTextFile.Readline

    intLineFinder = InStr(strNextLine, "DisplayWelcomeDlg")
    If intLineFinder <> 0 Then
        strNextLine = "DisplayWelcomeDlg=NO"
    End If

    intLineFinder = InStr(strNextLine, "UserName")
    If intLineFinder <> 0 Then
        strNextLine = "UserName=Ken Myer"
    End If

    strNewFile = strNewFile & strNextLine & vbCrLf
Loop

objTextFile.Close

Set objTextFile = objFSO.OpenTextFile("sample.ini", ForWriting)

objTextFile.WriteLine strNewFile
objTextFile.Close

We start off by defining two constants: ForReading and ForWriting. These two constants will be required when we read and write to the .INI file. Note that there isn’t a constant called ForEditing; that’s because the FileSystemObject doesn’t allow you to both read and write a file at the same time. Instead, we’ll have to open the file and read it, then we’ll have to close the file and re-open it for writing. At that point, we’ll be able to save our changes. Like we said, it’s not the most elegant system ever devised, but, hey, it is what it is.

After defining the two constants we open our .INI file (sample.ini) for reading. We then set up a Do loop in which we’ll read the file line-by-line, until there is nothing left to read (when the AtEdnOfStream property is TRUE). We start by using the Readline method to read the first line of the file, and store that line in the variable strNextLine.

Now the fun begins. As we noted earlier, the FileSystemObject doesn’t really allow you to search a file; instead, we have to read the file line-by-line, then individually check each line to see if it happens to be of interest. That’s exactly what we’re doing with this line of code:

intLineFinder = InStr(strNextLine, "DisplayWelcomeDlg")

This code uses the InStr method to see if the string DisplayWelcomeDlg can be found anywhere within the variable strNextLine (and, remember, this variable contains the .INI line we just read in). InStr works by reporting the character position at which the target string is found. For example, suppose strNextLine equaled this:

xxxxxDisplayWelcomeDlgxxxxx

In that case, InStr would return a 6, because our target string starts at the sixth position (we have 5 x’s, then we have DisplayWelcomeDlg). If the target string can’t be found, then InStr returns a 0.

And that’s exactly what happens when we read the first line of the .INI file. Because the first line in the file is [OEM Install], InStr returns a 0. Consequently, we skip our first If-Then statement, and continue on. We then check to see if UserName can be found anywhere in the variable. Again, InStr returns a 0, so we skip the second If-Then statement as well.

This is what we mean by editing the file using brute force: we’re checking each and every line of the file for each of our test conditions. Suppose we wanted to modify the DisplayUserInfoDlg property, too. Well, then we’d have to add a third test condition, one similar to this:

intLineFinder = InStr(strNextLine, "DisplayUserInfoDlg")

Got that? We’ve now checked the first line of the .INI file, and it isn’t a line of interest. So do we just discard it and move on? Nope, not quite. Instead, we use this line of code:

strNewFile = strNewFile & strNextLine & vbCrLf

What this code does is construct a brand-new .INI file in memory, storing this new file in the variable strNewFile. The first time we run through the loop, strNewFile will consist of everything currently in strNewFile (which is nothing the first time through) plus whatever happens to be in the variable strNextLine ([OEM Install]) plus a carriage return linefeed (vbCrLf). In other words, after one iteration strNewFile looks like this:

[OEM Install]

Exciting, isn’t it?

Now we loop around and read the second line in the text file (DisplayWelcomeDlg=YES). And guess what: this time InStr finds the target string, and the variable intLineFinder will equal 1 (because DisplayWelcomeDlg begins in the first character position). Because intLineFinder does not equal 0, we now enter our first If-Then block. Inside that block, we simply change the value of strNewLine:

strNextLine = "DisplayWelcomeDlg=NO"

Now our variable doesn’t contain the text we read in from the .INI file; instead, it contains this modified text:

DisplayWelcomeDlg=NO

In other words, we didn’t really edit the value of the DisplayWelcomeDlg property. Instead, we simply threw out the old line and replaced it with a brand-new line. However, the end result is no different than it would have been had we’d been able to edit the property value directly.

When we get to the end of the loop, the variable strNewFile will now look like this:

[OEM Install]
DisplayWelcomeDlg=NO

See how this works? We’ll keep reading through the .INI file line-by-line, and we’ll keep constructing the variable strNewFile line-by-line as well. By the time the script is finished, strNewFile will contain this information:

[OEM
Install]
DisplayWelcomeDlg=NO
DisplayEULA=NO
DisplayTypeOfInstallDlg=NO
DisplaySelectDestDirDlg=YES
DisplayCustomDlg=NO
DisplayUserInfoDlg=NO
DisplayConfirmRegDlg=NO
DisplayStartCopyDlg=NO
DisplayFinishDlg=NO
DisplayFinalMessage=YES
DisplayRebootDlg=YES
ProgGroupName=
DefaultDestDir=
UserName=Ken Myer
UserCompanyName=
UserSerialNumber=

And, yes, that is exactly the same information we want in our modified .INI file.

We’re on our way now. We next close the file sample.ini, then immediately reopen it for writing (silly, but that’s what we have to do). We then use this single line of code to replace the existing text in sample.ini with the information stored in the variable strNewFile:

objTextFile.WriteLine strNewFile

When we close the file again, the data will be saved, and we will have successfully modified our .INI file.

Now, we don’t claim this is the coolest piece of code ever written; it’s not. And there might very well be .INI files where this doesn’t work. But the truth is, it’s probably the best you’re going to be able to do, at least with the tools built into the operating system.


Can I Enable the Caps Lock Key?

Hey, Scripting Guy! I have a script where users enter some information in an Input box. The information needs to be entered in all-capital letters, so my instructions say, “Please make sure the Caps Lock key is on before entering the information.” They don’t always do that, however. Is there a way to turn the Caps Lock key on and off using a script?

-- BW, Medford, OR

Hey, BW. We don’t know of a way to turn the Caps Lock key on and off, but we do know a way to mimic the effect of having the Caps Lock key on. After all, the whole point of the Caps Lock key is to turn everything you type into uppercase letters. For example, you might type this:

this is my sentence.

But Caps Lock will make it appear on screen like this:

THIS IS MY SENTENCE.

So how can we achieve the same affect in a script? Simple: we just use the VBScript function UCase, which switches all the letters in a string to their uppercase equivalent. For example, here’s a simple two-line script that gathers information from a user and then uses the UCase function to switch all the letters to uppercase when echoing the value to the screen:

strMessage = InputBox("Please enter your message:")
Wscript.Echo UCase(strMessage)

Incidentally, the above script doesn’t actually change the case of the letters in the string strMessage; it just displays them in uppercase. If you really want all the letters converted to uppercase, try this script instead:

strMessage = UCase(InputBox("Please enter your message:"))
Wscript.Echo strMessage

Looks crazy, but it works.

For more information about the UCase function, see the VBScript documentation on MSDN.


How Can I Access a Type Library From within a Script?

Hey, Scripting Guy! How can I access a type library from within a script?

-- XA

Hey, XA. For those of you unfamiliar with the term, a type library is a file or a component that contains type information for a COM object; type information describes the object and the things you can do with it. Among the information found in a type library are the constants used by that object. For example, the FileSystemObject includes these three constants and their values:

ForReading (1)
ForWriting (2)
ForAppending (8)

So what, you say? Well, any time you access a text file using the FileSystemObject, you need to reference one of these constants. For example, suppose you want to open the file C:\Scripts\Test.doc and write the current date and time to that file. To do that, you need a script similar to this:

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\scripts\test.txt", ForWriting)

objTextFile.WriteLine Now
objTextFile.Close

Notice two things here. First, when we open the file (using the OpenTextFile method), we include the constant ForWriting because we have to tell the FileSystemObject whether the file is being opened for reading, for writing, or for appending. Second, notice that -- in the very first line of the script -- we explicitly assign the value 2 to the constant ForWriting. That’s because VBScript doesn’t have access to type libraries; therefore, we have to tell our script that the value of ForWriting is equal to 2.

By contrast, programming languages like Visual Basic can directly access a type library. If we were writing a Visual Basic program, we wouldn’t have to define the constant ForWriting and assign it the value 2. Instead, we could simply use the constant in our code, and Visual Basic would access the type library and look up the value of ForWriting for us.

In other words, programming languages have access to type libraries; therefore, you don’t have to define any of the constants found in those libraries. VBScript does not have access to type libraries; consequently, you have to manually define any constant used in your script. (And in some cases – such as using ADO to write scripts that interact with databases – you might find yourself using scores of constants, all of which will have to be defined.)

We know: you’re sitting there saying “Come on, come on, give us the workaround, we know you have one.” And you’re right, we do have a workaround; sort of. If you’re writing a “traditional” VBScript script (that is, something with a .vbs file extension), you’re out of luck. But if you’re willing to write a Windows Script File (something with a .wsf file extension), well, that’s a different story.

WSF files made their debut in WSH 5.6 and – to be honest – they haven’t made much of an impact since. (For more information, see the WSF documentation on MSDN). The problem with WSF files is that the advantages they add (like providing access to a type library) are often outweighed by the disadvantages of having to deal with wrapping your script up in a bunch of XML tags. But if XML doesn’t bother you and if you’d really like to be able to access those type libraries, then you can use code similar to this. (Be sure and save this with a .wsf file extension and not a .vbs file extension.)

<package>
    <job id="Test">
        <object id="objFSO" progid="Scripting.FileSystemObject"/>
         <reference object="Scripting.FileSystemObject" />
            <script language="VBScript">
                Set objTextFile = objFSO.OpenTextFile _
                    ("c:\scripts\test.txt", ForWriting)
                objTextFile.WriteLine Now
                objTextFile.Close
            </script>
    </job>
</package>

Of course it’s crazy looking; that’s why very few people use WSF files. If you look inside the <script> tag, however (which is where you place your VBScript code), you’ll see the script we used earlier, with two exceptions: there’s no code for creating the FileSystemObject, and there’s no code defining the constant ForWriting. Instead, we use the <object> tag and the <reference> tag to take care of those two tasks.

In our regular VBScript script, we used this line of code to create an instance of the FileSystemObject:

Set objFSO = CreateObject("Scripting.FileSystemObject")

In the WSF file, we use the <object> tag to accomplish this same feat:

<object id="objFSO" progid="Scripting.FileSystemObject"/>

Note the ID assigned to the object: objFSO, the same object reference we used in our .vbs file.

After we create our object reference, we can then get access to the type library (and all the constants defined within) by using the <reference> tag:

<reference object="Scripting.FileSystemObject" />

That’s all there is to it; we can now refer to any constant found in the FileSystemObject without having to define that constant. Cool, huh? Of course, it’s not always this easy; sometimes you might have to use a GUID rather than a ProgId (like “Scripting.FileSystemObject”) when creating an object and a reference. But this will at least get you started.


How Can I Attach a File to an Email Sent Using CDO?

Hey, Scripting Guy! I understand how to use CDO to send an email, but how do I include an attachment with that email?

-- RT

Hey, RT. And, incidentally, thanks for this question. At least one of the Scripting Guys spent most of his Thanksgiving weekend eating and, on the rare occasions when he wasn’t eating, playing tackle football with a phalanx of nephews much bigger and – sigh – much younger than he is. It’s safe to say that this Scripting Guy was hoping to ease his way back to work, and this question is a good way to start.

For those of you who aren’t familiar with CDO (short for Collaboration Data Objects) this technology provides a way for you to send email from a script. As long as you have an SMTP server located somewhere on your network, you can create and send an email using code similar to this:

Set objEmail = CreateObject("CDO.Message")

objEmail.From = "helpdesk@fabrikam.com"
objEmail.To = "administrator@fabrikam.com"
objEmail.Subject = "Server down"
objEmail.Textbody = "Server1 is no longer accessible over the network."

objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = _
        "smtpmailer"
objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
objEmail.Configuration.Fields.Update

objEmail.Send

We’re not going to explain this script in any detail today; for a quick overview of the individual properties used here, see the Microsoft Windows 2000 Scripting Guide. You can probably figure out the From, To, Subject, and Textbody properties without too much trouble. The URIs (e.g., http://schemas.microsoft.com/cdo/configuration/sendusing) are boilerplate code that can typically be left as-is; in most cases, the only one you’ll need to change is smtpserver. In our sample code, we’ve referenced an SMTP mail server we named smtpmailer; you’ll need to replace that with the name of your SMTP mail server.

By the way, these URIs concern a lot of people: they wonder why they need to connect to Microsoft in order to send an email. The truth is, you don’t actually connect to Microsoft; these URIs are just properties. Why are they referenced like this? To be honest, we don’t know. But don’t worry: your mail doesn’t get routed through Microsoft, and no one here reads it. Trust us, we have enough problems keeping up with our own email, let alone taking time to read someone else’s.

Now, what about adding an attachment to this email? All it takes is one additional line of code, much like this line which attaches the files C:\Scripts\Output.txt to the email:

objEmail.AddAttachment "C:\Scripts\Output.txt"

That’s it; add this line of code to the script, and you’ll have yourself an attachment. The entire script will look something like this:

Set objEmail = CreateObject("CDO.Message")

objEmail.From = "helpdesk@fabrikam.com"
objEmail.To = "administrator@fabrikam.com"
objEmail.Subject = "Server down"
objEmail.Textbody = "Server1 is no longer accessible over the network."
objEmail.AddAttachment "C:\Scripts\Output.txt"

objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = _
        "smtpmailer"
objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
objEmail.Configuration.Fields.Update

objEmail.Send

As long as we have your attention, we’ve had a couple people ask us how they can send email if their SMTP server requires authentication. To tell you the truth, that’s a tough question for us to answer because (for various reasons) we don’t have a way to test that scenario. However, adding a user name and password to your script is a good place to start:

objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendusername") = "fabrikam\kenmyer"
objEmail.Configuration.Fields.Item _
    ("http://schemas.microsoft.com/cdo/configuration/sendpassword") = "&gr54#wgha"

The preceding code logs you on to the SMTP server as fabrikam\kenmyer, with the password &gr54#wgha. Note that this does send the user name and password in cleartext. Consequently, you probably don’t want to send email using an Administrator account. Instead, create a user account that has the right to send email (but little, if anything else) and log on using that account. For more information, check out the official CDO documentation on MSDN.


How Can I Compare Two String Values Regardless of Letter Case?

Hey, Scripting Guy! I have a script that compares two email addresses and tells me whether or not they are the same. Sometimes the email addresses are the same, but the case is different: for example, one address might be example@abc.com and the other might be example@ABC.com. My script always tells me that these are different email addresses. How can I fix this?

-- PS

Hey, PS. You know, if people knew what the Scripting Guys were really like they’d never trust us with scripting questions like this. For example, a week or so ago one of the Scripting Guys was setting up a family vacation. While making reservations he needed to spell his sister’s married name to the ticket agent. “K as in – “ he said, and then stopped dead in his tracks. For the life of him, this Scripting Guy couldn’t think of a single word that started with the letter K.

So what does that have to do with your question? Nothing. We just thought it was an interesting anecdote.

Ok, but what about your question? As you know, we often tell people not to worry about uppercase and lowercase when it comes to writing scripts. “For the most part VBScript is not case sensitive,” we tell people. “ABC is the same as abc.”

And, in our defense, this is generally true of the keywords, functions, statements and other goodies that make up VBScript. For example, this line of code – odd as it might look – will pop up a message box, no problem:

mSGboX "This is a message box."

However, just because you can type Msgbox any way you want doesn’t mean that the procedures and tests VBScript carries out are also case-insensitive. For example, try running this script, which compares example@abc.com and example@ABC.com:

str1 = "example@abc.com"
str2 = "example@ABC.com"

If str1 = str2 Then
    Wscript.Echo "The strings are equal."
Else
    Wscript.Echo "The strings are not equal."
End If

Run the script, and you’ll get this message:

The strings are not equal.

Why? Well, by default, when VBScript compares string values it compares the ASCII values of each character in the string. In the wonderful world of ASCII, the uppercase A and the lowercase a have different values (65 and 97, respectively). Because the ASCII values are different VBScript insists that the two strings are different.

So how can we work around this issue? We’re going to show you two different solutions.

For starters, you can use the VBScript function StrComp (for string comparison); this helps ensure that your script does a text comparison rather than a binary comparison. (A binary comparison sees A and a as different characters; a text comparison does not.) For example:

str1 = "example@abc.com"
str2 = "example@ABC.com"

intCompare = StrComp(str1, str2, vbTextCompare)

If intCompare = 0 Then
    Wscript.Echo "The strings are equal."
Else
    Wscript.Echo "The strings are not equal."
End If

In this script we specify our two string values (str1 and str2) and then call the StrComp function. StrComp gets three parameters: the two strings being compared and the VBScript constant vbTextCompare. The results of the string comparison are then stored in a variable we named intCompare. If intCompare equals 0 then the two strings are equal; if intCompare is equal to anything but 0, then the two strings are different.

Give it a try and see what happens. You should get back this message:

The strings are equal.

Whew. Much better.

Here’s another way to ensure that strings get compared without worrying about letter case. In this script we use the UCase function to convert all the letters in the two strings to uppercase (in other words, both strings will be converted to EXAMPLE@ABC.COM). Because there are no lowercase letters to worry about this script will report back that the two strings are the same:

str1 = UCase("example@abc.com")
str2 = UCase("example@ABC.com")

If str1 = str2 Then
    Wscript.Echo "The strings are equal."
Else
    Wscript.Echo "The strings are not equal."
End If

Remember, you should use one of these techniques only if case doesn’t matter. If case does matter (that is, if example@ABC.com should not be considered identical to example@abc.com), then just use a regular old equivalence test:

str1 = "example@abc.com"
str2 = "example@ABC.com"

If str1 = str2 Then
    Wscript.Echo "The strings are equal."
Else
    Wscript.Echo "The strings are not equal."
End If

And in case you’re wondering, our Scripting Guy eventually did come up with “Kansas” as a word that started with the letter K. Good for him!


How Can I Convert Names to Proper Case?

Hey, Scripting Guy! I have a script that my help desk people use to create user accounts. Unfortunately, sometimes these help desk personnel get in a hurry and type in names like this: kEn MYEr. How can I convert names to proper case (i.e., Ken Myer)?

-- LC

Hey, LC. You know what: you’re in luck. Usually when it comes to doing anything properly the Scripting Guys are the last people you’d want to ask. In fact, the only exception that we know of is converting names to proper case, with the first letter in the name written in uppercase and the remaining letters written in lowercase. That we can do.

Note. Hey, everyone has to be able to do something. And while we would have preferred being able to throw a fastball past Albert Pujols or winning an Olympic gold medal, being able to convert names to proper case would have been our next choice anyway.

We’ll warn you in advance that the script that performs this task might look a bit cryptic; that’s because VBScript (unlike Visual Basic) doesn’t have a built-in method for converting strings to proper case. But that’s OK; after all, if it was too easy it wouldn’t be any fun:

strFirstName = "kEn"
strLastName = "MYEr"

intFirstName = Len(strFirstName)
strFirstLetter = UCase(Left(strFirstName, 1))
strRemainingLetters = LCase(Right(strFirstName, intFirstName - 1))

strFirstName = strFirstLetter & strRemainingLetters

intLastName = Len(strLastName)
strFirstLetter = UCase(Left(strLastName, 1))
strRemainingLetters = LCase(Right(strLastName, intLastName - 1))

strLastName = strFirstLetter & strRemainingLetters

Wscript.Echo strFirstName, strLastName

Let’s walk you through the process. To begin with, we simply assign the values kEn and MYEr to variables named strFirstName and strLastName; these, needless to say, are the two names we need to convert.

Note. Yes, even though it was needless to say, we said it anyway. Go figure.

Seeing as how first things are supposed to come first, we begin by tackling the user’s first name. To do that, we use the Len function to determine the number of letters in the string kEn (you got it: there are 3 letter in kEn):

intFirstName = Len(strFirstName)

Next we need to grab just the first letter of the name and convert it to uppercase. We do that by combining a pair of functions. We use the Left function to take the first letter; that is, we take one letter from the left side of the string (in case you’re wondering, the 1 indicates the number of letters we want to grab):

Left(strLastName, 1)

That’s going to give us the letter k. We then use the UCase function to convert that letter to uppercase:

UCase(Left(strLastName, 1))

We now have the uppercase letter K which we store in a variable named strFirstLetter. That’s an awful lot, yet all those steps are carried out in a single line of code:

strFirstLetter = UCase(Left(strLastName, 1))

See how that works? OK. Now we need to convert all the remaining letters in the name to lowercase. That’s what we do with this line of code:

strRemainingLetters = LCase(Right(strLastName, intLastName - 1))

Yes, it does look a little crazy. So let’s break it down. What we want to do is take all the letters in the first name except for the very first letter. To do that we use the Right function and, beginning from the right, take x number of letters. What is x? Well, in this case x will be the total number of letters in the string minus 1. In other words, 3 – 1, or 2. That’s going to give us the letters En (which is all we want) leaving out the initial letter k.

Make sense? Here’s the code that does that:

Right(strLastName, intLastName - 1)

And what do we do with those letters? Well, this time we use the LCase function to convert each letter to lowercase:

LCase(Right(strLastName, intLastName - 1))

After that we take those lowercase letters and stash them in a variable named strRemainingLetters:

strRemainingLetters = LCase(Right(strLastName, intLastName - 1))

Yes, it can be a little confusing. But walk yourself through the code a time or two and you should catch on. Alternatively, modify the code and do the Left/Right thing first and then call UCase or LCase:

intFirstName = Len(strFirstName)

strFirstLetter = Left(strFirstName, 1)
strFirstLetter = UCase(strFirstLetter)

strRemainingLetters = Right(strFirstName, intFirstName - 1)
strRemainingLetters = LCase(strRemainingLetters)

If it helps to do things step-by-step, well, so much the better.

Finally, we need to reconstruct the user’s first name. To do that we use the variable strFirstLetter (which contains the uppercase version of the first letter in the user’s name) and combine it with the variable strRemainingLetters (which contains the lowercase version of all the remaining letters in the user’s name):

strFirstName = strFirstLetter & strRemainingLetters

We repeat this entire process for the last name, then echo back the “new” first and last names for our user:

Wscript.Echo strFirstName, strLastName

And what do we get when we do this?

Ken Myer

It’s a thing of beauty. And properly done, too!


How Can I Create a GUID Using a Script?

Hey, Scripting Guy! We have items that we need to track using unique identification numbers. My boss suggested that we use GUIDs for this purpose. Is there any way to create a GUID using a script?

-- DX

Hey, DX. GUIDs (globally unique identifiers) are a good choice for a job like this; we don’t pretend to understand the algorithm behind GUIDs, but you are all-but-guaranteed that every GUID you generate will be unique. Although it’s theoretically possible to generate a duplicate GUID, it’s also theoretically possible that Bill Gates will give all his money to the Scripting Guys. We aren’t going to lose any sleep over either possibility.

There’s actually a very easy way to generate GUIDs, although it almost seems like cheating. (Hey, we said almost.) The Scriptlet.TypeLib object is designed to help you create Windows Script Components (basically a way of taking a script you wrote and making it act like a COM object). The Scriptlet.Typelib object includes a method that generates GUIDs for use with Windows Script Components type libraries; however, there’s no reason you can’t use this method to generate a GUID for other purposes. (After all, a GUID is a GUID is a GUID.) If you need a GUID, here’s a two-line script that will supply you with one:

Set TypeLib = CreateObject("Scriptlet.TypeLib")
Wscript.Echo TypeLib.Guid

Not bad, huh? Of course, this script merely echoes the GUID back in a message box; if you want to actually use that GUID somewhere else, you’ll have to type it in. That might be OK if it wasn’t for the fact that GUIDs look like this:

{24DD18D4-C902-497F-A64B-28B2FA741661}

And so, being the generous, self-serving guys that we are, we modified the script a bit. This script generates a GUID but then, instead of displaying that GUID in a message box, copies the GUID to the Clipboard:

Set TypeLib = CreateObject("Scriptlet.TypeLib")
strGUID = TypeLib.Guid

Set objIE = CreateObject("InternetExplorer.Application")
objIE.Navigate("about:blank")
objIE.document.parentwindow.clipboardData.SetData "text", strGUID
objIE.Quit

If you’re wondering how we do that, well, we’re cheating once again. VBScript can’t copy items to the Clipboard, but Internet Explorer can. So we’re simply generating a GUID and then storing that value in a variable named strGUID. We then create an invisible instance of Internet Explorer, and use the clipboardData.SetData method to copy that GUID to the clipboard. That’s what we do in this line of code:

objIE.document.parentwindow.clipboardData.SetData "text", strGUID

We quit Internet Explorer, and now are free to go paste that GUID in anywhere we want to use it.


How Can I Create an Environment Variable Using a Script?

Hey, Scripting Guy! Can I create an environment variable using a script?

-- OD

Hey, OD. WMI is a great technology for scripting, but it has its eccentricities. For example, can you create a process using a script? Of course you can; after all, WMI’s Win32_Process class has a Create method. Can you create a service using a script? Of course you can; after all, WMI’s Win32_Service class has a Create method. Can you create an environment variable using a script? Of course you can, despite the fact that the Win32_Environment class doesn’t have a Create method. Strange but true!

In fact, here’s a script that creates a new environment variable named TestValue:

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

Set objVariable = objWMIService.Get("Win32_Environment").SpawnInstance_

objVariable.Name = "TestValue"
objVariable.UserName = "System"
objVariable.VariableValue = "This is a test"
objVariable.Put_

We know: this looks different from most WMI scripts. But don’t worry, we’re going to walk through the code, step-by-step.

Our environment variable script begins the same way most WMI scripts do: by connecting to the WMI service. After connecting to the WMI service you typically call ExecQuery to return a collection of items. However, we’re not going to do that with this script. Instead, we’re going to use the SpawnInstance_ method to create a “blank” environment variable, one that exists only in memory. (We use the term “blank” because SpawnInstance_ creates an environment variable that has all the properties of an environment variable; however, at the time of creation none of those properties are assigned a value. Hence a “blank” instance.)

This line of code creates an object reference (objVariable) that points to our new environment variable (which, again, exists only in memory when created):

Set objVariable = objWMIService.Get("Win32_Environment").SpawnInstance_

And, no, that’s not a misprint: the name really is SpawnInstance_, with an underscore coming at the end. You’ll see a lot of underscores before this column is finished.

What we need to do now is fill in the properties of our environment variable. In particular, we need to specify three values:

· Name. This is simply the name of the environment variable. In this sample script we use the clever name TestValue.

· UserName. This is the owner of the environment variable. To make this a system variable, set UserName to System; to make it a user-specific environment variable, set UserName to the user account name (for example, fabrikam\kenmyer).

· VariableValue. The assigned value of the environment variable. Once again we’ve gone overboard with our cleverness, assigning TestValue the value This is a test.

And, yes, that’s why we Scripting Guys get paid the big bucks.

With the three property values assigned, we then call the Put_ method, which actually writes the new environment variable to the operating system. Mission accomplished.

What do you mean, “Are you sure?” If you don’t believe us, a quick way to verify creation is to use a script similar to this one. This script uses a WQL query to select all environment variables with the name TestValue, then echoes the property values of that variable to the screen:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_Environment Where Name = 'TestValue'")

For Each objItem in colItems
    Wscript.Echo "Name: " & objItem.Name
    Wscript.Echo "User Name: " & objItem.UserName
    Wscript.Echo "Variable Value: " & objItem.VariableValue
    Wscript.Echo
Next

Of course, what’s the fun of having a brand-new, custom-made environment variable if you never get to play with it? If you want to change the value of your new environment variable, simply connect to that variable (using the same WQL query we just showed you) and assign a new value to the VariableValue property. Call the Put_ method to write the changes to the operating system, and your environment variable will take on the new value. For example, this sample script changes the value of TestValue to New value:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_Environment Where Name = 'TestValue'")

For Each objItem in colItems
    objItem.VariableValue = "New value"
    objItem.Put_
Next

Child’s play.

But what if you want to delete your environment variable? (Yes, even after all the work you did to create it.) That’s easy enough: we just take the same basic approach we used when modifying the value of the environment variable. This time, however, we don’t assign a new value to the VariableValue property; instead we simply call the Delete_ method to delete the variable:

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

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_Environment Where Name = 'TestValue'")

For Each objItem in colItems
    objItem.Delete_
Next

Again, note the underscore on the end of the method name: Delete_.

Of course, there’s still one question left unanswered: how did we know that the Win32_Environment class supported the SpawnInstance_ and Delete_ methods? In this case, we used Wbemtest.exe. Call up Wbemtest and take a look at the Win32_Environment class:

If you look at the very top of the screen (under the Qualifiers heading) you’ll see the CreateBy and DeleteBy qualifiers, followed by the methods used to create a new instance (PutInstance) and delete an existing instance (DeleteInstance). That’s how we could tell that Win32_Environment supported SpawnInstance_ and Delete_.


How Can I Determine if a WMI Property is Writeable?

Hey, Scripting Guy! Hey, Scripting Guy! We have an application that installs a WMI class named UIDLightInfo. That class includes a property named UIDLightState. How can I tell whether or not I can change the value of this property?

-- JC

Hey, JC. You know, we once reviewed a scripting course where the authors continually dealt with the question, “How do I know if I can do x?” Their answer was always the same: “Try it and see what happens. If your script blows up that means you probably can’t do it.” We were always glad that these guys were teaching a scripting class rather than serving as driving instructors:

“Do you think we’d survive if I drove off this cliff?”

“Try it and see what happens.”

Admittedly, there might be times when the only way to find out an answer is to close your eyes, run the script, and hope for the best. This isn’t one of those times, however. WMI is a very powerful technology for retrieving information about processes, services, and other items involving your computer; it’s an equally powerful technology for retrieving information about WMI itself. If you want to know something about WMI namespaces, classes, methods, properties, or what-have-you, the best advice is to just ask WMI.And that’s exactly what this particular script does. This is a generic script that can check a specified property, in a specified class, in a specified namespace, on a specified computer, and then return the following information:

· Whether the property stores values as an array.

· The data type of the property.

· Whether the property is read-write or read-only.

Here’s the script:

strComputer = "."
strNamespace = "\root\cimv2"
strClass = "Win32_Printer"
strProperty = "PortName"

blnWriteable = False

Set objClass = GetObject("winmgmts:\\" & strComputer & strNameSpace & ":" & strClass)

For Each objClassProperty In objClass.Properties_
    If objClassProperty.Name = strProperty Then
        If objClassProperty.IsArray Then
            Wscript.Echo "This property is an array."
        End If

        Select Case objClassProperty.CIMType
            Case 8
                Wscript.Echo "This is a string property."
            Case 11
                Wscript.Echo "This is a Boolean property."
            Case 13
                Wscript.Echo "This is an Object property."
            Case 101
                Wscript.Echo "This is a date-time property."
            Case 102
            Wscript.Echo "This is a Reference property."
            Case 103
                Wscript.Echo "This is a string-type property."
            Case Else
                Wscript.Echo "This is a numeric property."
        End Select

        For Each objQualifier in ObjClassProperty.Qualifiers_
            If objQualifier.Name = "write" Then
                blnWriteable = True
            End If
        Next
    End If
Next

If blnWriteable = True Then
    Wscript.Echo "This property is read-write."
Else
    Wscript.Echo "This property is read-only."
End If

We begin by defining some variables that will be used to connect to the desired computer, namespace, class, and property. Our sample script connects to the root\cimv2 namespace on the local computer and queries the PortName property in the Win32_Printer class; if you want to query a different property/class/whatever, just change the values accordingly. For example, assuming your UIDLightInfo class is found in the root\cimv2 namespace then the first four lines of your script might look like this:

strComputer
= "."
strNamespace = "\root\cimv2"
strClass = "UIDLightInfo"
strProperty = "UIDLightState"

In addition, we assign the value False to a fifth variable, blnWriteable. We’re going to assume, by default, that all properties are read-only; that’s what the False is for. Later in the script we’ll test to see if the property is writeable; if it is, then we’ll change the value of blnWriteable to True. Otherwise we’ll just leave it be.

We then use this line of code to connect to the WMI service and bind to the desired class:

Set objClass = GetObject("winmgmts:\\" & strComputer & strNameSpace & ":" & strClass)

When we bind to a class (as opposed to returning instances of that class) we can retrieve information about the class itself. For example, each WMI class has a property named Properties_; as you might expect, this contains a list of all the properties for that class. We use this line of code to set up a For Each loop to walk through the collection of properties:

For Each objClassProperty In objClass.Properties_

Inside that loop we check each property to see if the property name matches the name we assigned to the variable strProperty. If it does, we retrieve some detailed information about that particular property; that’s because properties also have properties. For example, we can look at the IsArray property to determine whether or not the property stores values as an array:

If objClassProperty.IsArray Then
    Wscript.Echo "This property is an array."
End If

In addition, we can use the CIMType property to determine the data type for the property. Because CIMType returns information as an integer value, we set up a Select Case block to convert the value to something a bit more meaningful. For example, if CIMType is an 8, our script echoes back the fact that the property has a string data type:

Case 8
    Wscript.Echo "This is a string property."

Last, but surely not least, we take a look at the “qualifiers” for this property. Qualifiers are additional information that might or might not be present; if they are present they help define the property. For example, if the property has the read qualifier then that property is readable; if the property has the write qualifier, then the property is writeable. We use the following code to loop through all the qualifiers for the property, looking for one with the name write. If we find one, that means that the property is writeable, and we change the value of blnWriteable to True:

For Each objQualifier in ObjClassProperty.Qualifiers_
    If objQualifier.Name = "write" Then
        blnWriteable = True
    End If
Next

We echo whether the property is read-only or read-write, and then call it quits. When we run this script on a Windows XP computer we get back output like this:

This is a string property.
This property is read-write.

Because the property is read-write you now know that you can change it using a script.


How Can I Determine the Number of Items Generated by the Split Function?

Hey, Scripting Guy! I know I can use the Split function to split a line into individual items. But how can I tell how many “splits” I ended up with; that is, how many individual items did I generate from a single line?

-- SA

Hey, SA. For those of you who are relative newcomers to scripting (or those of you who’ve never used the Split function) let’s start off with a brief tutorial. As SA notes, the Split function lets you take a text string of one or more items (with individual items separated by a common delimiter, such as a comma) and then create an array of individual items. For example, suppose we have this line of text:

Dopey,Sneezy,Doc,Grumpy,Sleepy,Happy,Bashful

By running the Split function and telling the function that the comma is our delimiter (that is, the character that separates the individual items) we can easily separate this string into its constituent parts:

Dopey
Sneezy
Doc
Grumpy
Sleepy
Happy
Bashful

All in all a very handy little function, especially when working with file paths, Active Directory AdsPaths, and anything else where the parts are often-times every bit as important as the whole. (For example, the Split function can take a file path like C:\Scripts\MyScript.vbs and separate out the drive, folder, and file name for us.)

All well and good. But SA would like to know how many items were found in an individual text string; for example, we don’t want to know the names of the Seven Dwarves, we just want to know that seven dwarves were listed in the string.

Note. At the risk of bragging, we might point out that the Scripting Guys – who are often mistaken for the Seven Dwarves – knew all seven names without looking them up. And here’s something for you trivia buffs: Dopey (the dwarf most often confused with the Scripting Guys) was originally named Deafy. Strange but true!

So how can we determine the number of items found in the string? Well, we just use a simple little script like this:

strNames = "Dopey,Sneezy,Doc,Grumpy,Sleepy,Happy,Bashful"
arrNames = Split(strNames, ",")
Wscript.Echo Ubound(arrNames) + 1

In line 1 we assign the names of our seven dwarves (each name separated by a comma) to a variable named strNames. In line 2 we call the Split function and indicate that the delimiter is the comma. That’s why you see “,” as the second parameter passed to Split; the first parameter, of course, is the string we want Split to work against. This gives us back an array named arrNames, with each item in the array representing the name of one of the dwarves. (Incidentally, the name arrNames is purely arbitrary; you can use any variable name you want.)

That brings us to line 3:

Wscript.Echo Ubound(arrNames) + 1

What we’re doing here is echoing the Ubound (upper bound) value of our array, plus 1. What does that mean? Well, in an array each item is automatically assigned an index number: the first item is assigned index number 0, the second item is assigned index number 1, and so on. In memory, our array looks something like this:

Index Number | Item
0 | Dopey
1 | Sneezy
2 | Doc
3 | Grumpy
4 | Sleepy
5 | Happy
6 | Bashful

The Ubound function will always return the index number of the last item in the array; in this case, Ubound returns a 6. The Ubound value will always be one less than the number of items in the array; remember, we actually have seven items in our array. This occurs because the first item in the array is assigned an index number of 0; if the first item was number 1 then the Ubound value would also represent the total number of items in the array. But that’s OK: all we have to do is take the Ubound value, add 1 to it, and we’ll have our total number of items. Case closed!

Oh, and one other thing: yes, we will strongly discourage Peter from singing a scripting-related version of Someday My Prince Will Come during his next webcast. You have our word on it.


How Can I Display More Than 1,023 Characters in a Custom Message Box?

Hey, Scripting Guy! I need to create a custom alert box that has over 1,000 characters in it. The message box used by Wscript.Echo can hold all the characters, but I can’t change the message box title. I can change the title of a message box used by the VBscript MsgBox function, but it won’t hold all my characters. Can you help?

-- KB

Hey, KB. Talk about a no-win situation. As you’ve discovered, the VBScript MsgBox function can display a maximum of 1,023 characters; if you have a string larger than that, any characters past the first 1,023 will be cut off. The Wscript.Echo method, by contrast, can display more than 1,023 characters. That’s great, except for one thing: with MsgBox you can change the title of the actual message box, but with Wscript.Echo your message box will always be titled Windows Script Host.

Incidentally, if you want to see what we’re talking about, try running this little test script. The test script creates a string consisting of 1,023 periods followed by the letter X. (In other words, a message that makes absolutely no sense, which puts it on a par with most of these columns.) We then display the message box using both the MsgBox function and the Wscript.Echo method.

Note. Of course, Wscript.Echo displays a message box only when the script is run under Wscript; otherwise the message will be written to a command window.

Here’s the test script:

Set objShell = CreateObject("Wscript.Shell")
For i = 1 to 1023
   strMessage = strMessage & "."
Next
strMessage = strMessage & "X"

MsgBox strMessage
Wscript.Echo strMessage

Take a look at the message box that appears when we use the MsgBox function; notice that the X (character number 1,024) doesn’t show up. You might note as well that this message box doesn’t even have a title; that’s because we didn’t give it one:

Now here’s the same message displayed using Wscript.Echo. Notice that this time the X (character number 1,024) is displayed; unfortunately, though, we’re stuck with the message box title Windows Script Host:

So what’s the answer to our dilemma? As it turns out, the answer is to use the Windows Script Host Popup method. Popup can display as many characters as Wscript.Echo plus it allows us to specify a custom title for our message box. It’s the best of both worlds.

Let’s show you a sample script that uses the Popup method, and then we’ll explain how it works:

Const
OK_BUTTON = 0
Const AUTO_DISMISS = 0

Set objShell = CreateObject("Wscript.Shell")

For i = 1 to 1023
strMessage = strMessage & "."
Next
strMessage = strMessage & "X"

objShell.Popup strMessage, AUTO_DISMISS, "My Message Box", OK_BUTTON

We begin by defining a pair of constants – OK_BUTTON and AUTO_DISMISS – and set the value of each of these constants to 0. Later on in the script we’ll use OK_BUTTON to indicate that we want a message box that uses only an OK button, and we’ll use AUTO_DISMISS to indicate that we want the message box to remain onscreen until the user clicks OK. Does that mean we could create message boxes that use buttons other than just OK, and that we could create message boxes that can automatically dismiss themselves? You bet it does; for more information, see the Windows Script Host documentation on MSDN.

Next we create an instance of the WSH Shell object, the object that actually contains the Popup method. We then use these lines of code to create our string of 1,023 periods plus the letter X:

For i = 1 to 1023
   strMessage = strMessage & "."
Next
strMessage = strMessage & "X"

All that’s left now is to call the Popup method and display a message box containing our entire string, a message box that also has the custom title My Message Box:

objShell.Popup strMessage, AUTO_DISMISS, "My Message Box", OK_BUTTON

As you can see, the Popup method takes four parameters:

· strMessage, the text we want to display.

· AUTO_DISMISS, the constant that tells Popup to keep the message box displayed until the user clicks OK.

· “My Message Box”, the custom title for the message box.

· OK_BUTTON, the constant that tells Popup to include only an OK button in the message box.

When we run the script we should see a message box that not only displays all 1,024 characters, but has a custom title as well:

Just what the doctor ordered.


How Can I Exit a For Each Loop?

Hey, Scripting Guy! Is there any way to exit a loop in a script?

-- MW

Hey, MW. You know, when we started writing this column we intended it to be a simple little thing that answered those basic questions so many beginning scripters have:

· How can I determine the current date in a script? (Use VBScript’s Date function.)

· How can I echo messages to the command window instead of in a message box? (Run the script under the CScript scripting host.)

· How can I tell whether a variable holds a numeric value? (Use the IsNumeric function.)

Ah, but you know how it goes: soon more and more people were telling us how much they liked the column, and soon we began answering questions that allowed us to show off our scripting skills. Eventually we got so full of ourselves that we no longer answered questions like, “How can I show a dialog box that people can use to enter information?” (use VBScript’s InputBox function) but instead we only answered complicated and esoteric questions, questions like, “How can I write a script that will exorcise the ghost of Greta Garbo?” Interesting and useful, but often-times a bit beyond what the beginning scripter was ready to tackle.

Note. Scoff if you will, but to date none of the Scripting Guys has ever been haunted by the ghost of Greta Garbo.

As you probably all know, after a wild, non-stop night of partying with supermodels you can’t help but wake up the next morning feeling a little remorseful. And the same thing is true here. We know that we’ve been neglecting a good portion of our audience – the beginning scripter –and we’ve vowed to never let that happen again. Because of that, every now and then we’ll be sure the answer one of the more basic scripting questions; promise. And just to prove it, today we’ll tell you how you can exit a For Each loop. (Obviously something people are interested in, because we’ve gotten several questions on this very topic.)

Did you just hear a noise? Sounded like a ghost or something….

OK: exiting a loop. Let’s say you have an NT 4 domain and you want to determine whether a particular user account exists in that domain. If you were running Active Directory this would be a no-brainer: you could just search for that account and have an answer is seconds. But this is NT 4 and no ADO provider exists for NT 4; that means you can’t search the accounts database on an NT 4 domain.

So how can you tell whether or not a user account exists in an NT 4 domain? Well, one way to do this is the good old-fashioned, brute force way: you retrieve all the accounts and check each one. And the easiest way to do that is in a For Each loop:

Set objComputer = GetObject("WinNT://fabrikam,domain")

For Each objItem in objComputer
    If objItem.Name = "kenmyer" Then
        Wscript.Echo "Account found."
    End If
Next

There’s nothing wrong with this approach, and – depending on your needs – you might have no choice but to methodically slog your way through the entire collection of accounts. The only problem is that this can be very slow; on a test domain with about 30,000 accounts it took well over 2 minutes for our script to complete. If the account you’re looking for happens to be the very last account in the collection there’s not much you can do about that. But what if the account you’re looking for happens to be the very first item in the collection? In that case you’re going to immediately find what you’re looking for but then have to wade through 29,999 more accounts just so you can complete the For Each loop.

Well, unless you explicitly exit the loop that is:

Set objComputer = GetObject("WinNT://fabrikam,domain")

For Each objItem in objComputer
    If objItem.Name = "kenmyer" Then
        Wscript.Echo "Account found."
        Exit For
    End If
Next

In our first script we checked each account to see if it had the name kenmyer; it if did we echoed back the message “Account found.” We do the same thing here, but with one difference: after echoing the message we then call the Exit For statement. Exit For immediately takes us out of our loop; if kenmyer happens to be the first account found we’ll never look at any of the other 29,999 accounts. That’s it: call Exit For and you are out of the loop. (We Scripting Guys seem to always be out of the loop, but that’s a different story.)

If you’d like to see the effects of Exit For, run this script, which simply writes the numbers 1 through 1,000 to the screen:

For i = 1 to 1000
    Wscript.Echo i
Next

Now try this script, which calls the Exit For statement if the variable i equals 3:

For i = 1 to 1000
    Wscript.Echo i
    If i = 3 Then
        Exit For
    End If
Next

See the difference? If you can write code like that you’ll never have to worry about the ghost of Greta Garbo ever again.

You sure you didn’t hear that? Sounds like chains rattling….

Exit For works in either a For Each or a For Next loop. But what if you need to exit a Do loop? No problem: just call Exit Do. What if you want to exit a subroutine? Just call Exit Sub. To exit a function call Exit Function. In other words, there’s no reason to ever again find yourself trapped in a never-ending loop; if you want out, just Exit.


How Can I Extract a Series of Characters Found in the Middle of a String?

Hey, Scripting Guy! I have a series of strings that look something like this: xxxxyyyyyyxx. How can I extract just the y’s?

-- JB

Hey, JB. And, yes, it is a Friday. But, hey, it’s just a coincidence that we happened to pick an easy question on a Friday, the day we’re itching to get out of here and go home and spend the weekend watching football and basketball. Besides, we feel it’s important to tackle these easier questions from time-to-time to better help those readers who are new to scripting.

Boy, that’s so convincing we almost believe it ourselves.

But enough about us. Let’s talk about the code instead:

strPhrase = "xxxxyyyyyyxx"
strExtract = Mid(strPhrase, 5, 6)
Wscript.Echo strExtract

As you can see, there’s not much to this script. We start out by assigning the string xxxxyyyyyyxx to a variable named strPhrase. As you noted, there’s a pattern to your series of strings: each string starts out with four characters we don’t care about, followed by six characters we do care about, followed by two other characters we don’t care about. Our job is simple: grab those six characters out of the middle of the string.

So how do we do that? We do that by using the Mid function:

strExtract = Mid(strPhrase, 5, 6)

As you can see, we pass the Mid function three parameters:

· strPhrase. This is the string we want to parse.

· 5. This is the character position we want to start grabbing characters from. We want to start with character 5 (skipping the first 4 characters), so we pass 5 as the second parameter.

· 6. The number of characters we want to grab.

In other words, starting with character 5, we want to grab 6 characters out of the middle of the string and store them in a variable named strExtract. And what happens when we echo back the value of strExtract? Why, we get this, of course:

yyyyyy

Keep in mind that we’re dealing solely with numbers here; we’re not looking for the actual string yyyyyy. With that in mind, consider this script:

strPhrase = "abcdcreateef"
strExtract = Mid(strPhrase, 5, 6)
Wscript.Echo strExtract

strPhrase = "ghijdeletekl"
strExtract = Mid(strPhrase, 5, 6)
Wscript.Echo strExtract

Here’s what we get when we run this script:

create
delete

Again, we started at character 5, and grabbed a total of 6 characters. Simple, huh?

And so it’s Friday and we’ve already finished the Hey, Scripting Guy! column. Gee, what do you think we should do now …?


How Can I Generate Random Numbers Using a Script?

Hey, Scripting Guy! Our auditors have suggested that we randomly pick projects to examine and ensure that each project was completed and signed off per company policy. Because these projects are numbered sequentially I thought maybe we could write a script that would randomly select the project numbers for us to audit. Can you help us with that?

-- PD

Hey, PD. You know, if you were looking for organized, consistent, and reliable we’d have to say, “No, sorry, we can’t help you.” But seeing as how you’re looking for random and unpredictable, well, you came to the right place.

As it turns out, VBScript has a built-in function (with the catchy name Rnd) designed solely to return random numbers. Let’s take a look at a script that generates 5 random numbers, each one between 1 and 100. We’ll then explain how the script works:

intHighNumber = 100
intLowNumber = 1

For i = 1 to 5
    Randomize
    intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)
    Wscript.Echo intNumber
Next

As you can see, we begin by assigning values to two different variables: intHighNumber (the highest number in our range) and intLowNumber (the lowest number in our range). What if we wanted to generate random numbers between 37 and 956? No problem: we’d simply adjust the values of intHighNumber and intLowNumber:

intHighNumber = 956
intLowNumber = 37

Next we set up a For Next loop that loops around 5 times. Inside the loop we find the following three lines of code:

Randomize
intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)
Wscript.Echo intNumber

The first line – consisting solely of the Randomize statement – is very important. This function uses the system time to provide a “seed” value to the Rnd function. Because the system time will always be different this means that the Rnd function will be more likely to generate a truly random number. In fact, try running this script, one that doesn’t use the Randomize statement:

intHighNumber = 100
intLowNumber = 1

intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)
Wscript.Echo intNumber

No matter how many times you run this you’ll get the same “random” number. (On our test computer, we get 71 each time.)

Our next line of code actually generates the random number:

intNumber = Int((intHighNumber - intLowNumber + 1) * Rnd + intLowNumber)

The Rnd function has an algorithm it uses to generate random numbers; all we have to do is plug our two variables (intHighNumber and intLowNumber) into the equation. In addition, we use the Int function to ensure that we get back an integer rather than a decimal number. It’s an odd-looking little line of code, but you don’t have to worry about it; just plug the variables into the right spots and have at it.

That’s pretty much it; we echo back the returned number and then loop around and generate the next random number. When we’re all done we should get back a list of numbers similar to this:

82
15
92
32
13

By the way, could you use this approach to generate numbers for playing the lottery? Yes. Would that make it more likely that you’d pick the winning numbers? Well, let’s put it this way: the Scripting Guys still have to come in to work each and every day. You can decide for yourself whether we’d do that had we written a script that would win us the $100 million PowerBall jackpot.


How Do I Get Rid of Extraneous Spaces in a String?

Hey, Scripting Guy! I know that I can use the Split command to separate a sentence like this – VBScript is fun! – into an array of individual words. However, what happens if I have a sentence like this: VBScript is fun! I can’t use a blank space as the delimiter because I might have lots of blank spaces. And I can’t use a specific number of blank spaces as the delimiter, because the number might vary. Any suggestions?

-- SC

Hey, SC. You might find this difficult to believe, but we Scripting Guys actually have standards, at least when it comes to this column. We get scores of questions each day, and we can’t respond to each one. So how do we decide which questions get published? Well, first we look for questions that seem reasonably easy to do. Here’s a hint: if your question includes lots of ands – “And I want the script to do this and I want the script to do that and ....” – well, then there’s much less chance it will find its way to the top of the pile. We realize that that’s a bit unfair, but for better or worse this column is a very small part of our daily responsibilities. Thus we can’t spend too much time on any one question.

The second thing we look for is mass appeal: will the answer to this question be of interest and of use to most of our audience. Again, that’s not always fair, but questions dealing with “mainstream” technologies will usually win out over more esoteric questions. Sorry.

To tell you the truth, though, we’re not sure if your script fits either category; we decided to answer it simply because we found it interesting. After all, how the heck do you get rid of all those extraneous spaces, and thus turn VBScript is fun! into VBScript is fun! ? Here’s the solution we came up with:

The problem here is not that we have spaces in the string; if that was the case, we could just use VBScript’s Replace function to get rid of all the spaces. However, we want a few spaces in the string; in fact, we want to separate each word with a space. We just want to get rid of all the extra spaces. In the end, we decided to go ahead and use the Replace function, replacing all multi-space instances with a single space. Thus if we found 7 consecutive blank spaces, we’d replace those with a single space.

Pretty simple, except for one problem. The Replace function needs a specific string to search for; you can’t just tell it, “OK, search for multi-space instances and replace them with a single space.” Instead, you have to use code similar to this, which replaces seven blank spaces with one space:

strStarter = Replace(strStarter, "       ", " ")

That’s great, except that we don’t know how many blank spaces to search for; on top of that, while we might have 7 consecutive blank spaces in the string, we might also have 5 consecutive blank spaces in that same string. How do we deal with that?

We dealt with it using this script. Here’s the code, which we’ll explain in a second:

strStarter = "VBScript                     is                  fun!"

intStarter = Len(strStarter)
For i = intStarter to 2 Step -1
    strChars = Space(i)
    strStarter = Replace(strStarter, strChars, " ")
Next

arrStarter = Split(strStarter, " ")

For Each strUnit in arrStarter
    Wscript.Echo strUnit
Next

What we ended up doing was saying, “OK, we have to search for strings consisting of blank spaces, but we don’t know how many blank spaces might be in one of these strings.” That was a bit of a stumbling block until we realized, hey, let’s say the string has a total of 37 characters in it. That means the most consecutive blank spaces we could find would be 37 (assuming the string was nothing but blank spaces). Therefore, we can start by searching for 37 consecutive blank spaces; if we find them, we replace them with a single blank space. We then search for 36 consecutive blank spaces, then 35, then 34. We continue working our way down until we search for 2 blank spaces, and replace those with a single space. At that point, we should have eliminated all the extraneous blank spaces.

Pretty slick, huh? And, surprisingly enough, it’s easy to do. Notice in our script we start by assigning a string to the variable strStarter. We then use this code to determine how many characters are in strStarter:

intStarter = Len(strStarter)

Again, let’s say there are 37 characters. What we need now is a loop that will start at 37 and work its way down to 2. And guess what? That’s exactly what this loop does:

For i = intStarter to 2 Step -1
    strChars = Space(i)
    strStarter = Replace(strStarter, strChars, " ")
Next

The loop starts at the number of characters in the string, then uses the Step -1 parameter to work its way down to 2. What’s Step -1? Well, by default, a For Next loop works its way up one number at a time. For example, in this loop i starts off at 1, and keeps incrementing by 1 until it gets to 10:

For i = 1 to 10

In our loop, we’re starting with the big number (37), and working our way down to 2, decrementing the value of i by 1 each time. Make sense? And that’s what Step -1 does; it basically subtracts 1 each time the loop runs.

Inside the loop itself we do two things. First, we have to create a string consisting of i number of blank spaces. Fortunately, we can do that with one line of code thanks to the Space function:

strChars = Space(i)

Second, we need to see if the substring consisting of i number of blank spaces can be found anywhere in strStarter. If it can, we need to replace it with a single blank space. And that’s what this code does:

strStarter = Replace(strStarter, strChars, " ")

From there we continue looping until we’ve checked for 2 consecutive blank spaces. We then exit the loop, use the Split command to divide the string into an array of individual words, and then – just to show you that everything worked – we echo back those individual words.

Now, this isn’t totally foolproof; for example, what if you had a few extraneous spaces in front of the word VBScript? We didn’t bother with situations like that, but you can always use VBScript’s LTrim and RTrim functions to get rid of leading and trailing spaces. If you’d like more information about string manipulation, check out this portion of the Microsoft Windows 2000 Scripting Guide .

We also don’t know the context in which a string like VBScript is fun! might arise. We suspect you might encounter strings like this when trying to read a fixed-width log file. If that’s the case, the code above will work, but you might find it easier to use ADO (ActiveX Database Objects) to parse the file instead.


How Can I Grab a URL From the Clipboard and Then Open That Web Site in a Browser?

Hey, Scripting Guy! How can I grab a URL from the clipboard and then open that Web site in a browser?

-- CL

Hey, CL. Interesting question, or, should we say, questions. That’s because there’s really two issues here. The first one is easy: can I use a script to open a specified Web site? As you probably know, the answer to that one is a resounding yes. Here’s a sample script that stores the Script Center’s URL in a variable named strURL. The script then creates an instance of the WSH Shell object, and uses the Run method to open up your default Web browser and navigate to the specified URL:

strURL = "http://www.microsoft.com/technet/scriptcenter/default.mspx"
Set objShell = CreateObject("Wscript.Shell")
objShell.Run(strURL)

The second question is a bit trickier: can I grab information off the clipboard using a script? It turns out that the answer to this one is also yes, although you have to get at the clipboard through the backdoor.

Neither WSH nor VBScript are able to interact with the clipboard: neither one allows you to copy data to the clipboard and or to paste data from the clipboard. Internet Explorer, on the other hand, can interact with the clipboard. (Hey, Internet Explorer can do anything!) So, we’ll just let IE do the work for us. If you want to grab data from the clipboard, you can use code similar to this:

Set objIE = CreateObject("InternetExplorer.Application")
objIE.Navigate("about:blank")
strURL = objIE.document.parentwindow.clipboardData.GetData("text")
objIE.Quit
Wscript.Echo strURL

What we’re doing here is creating an instance of Internet Explorer and opening it to a blank page. Note that you won’t actually see this instance of IE; that’s because we didn’t set the Visible property to TRUE. Instead, everything happens in the background.

We then use the clipboardData.GetData method to get the text that happens to be sitting on the clipboard and store it in the variable strURL; that’s what this line of code does:

strURL = objIE.document.parentwindow.clipboardData.GetData("text")

We dismiss this instance of IE (objIE.Quit) and then echo the value that we retrieved from the clipboard.

Give this a try: copy some text to the clipboard and then run the script. You should get back a message box consisting of that text you just copied to the clipboard.

Now it’s just a matter of putting the two script halves together to make a whole. Here’s a script that grabs a URL from the clipboard and then opens that Web site in your default Web browser:

Set objIE =
CreateObject("InternetExplorer.Application")
objIE.Navigate("about:blank")
strURL = objIE.document.parentwindow.clipboardData.GetData("text")
objIE.Quit

Set objShell = CreateObject("Wscript.Shell")
objShell.Run(strURL)

So maybe that wasn’t so bad after all. And as an added bonus, this script doesn’t limit you to just opening Web sites. Suppose you have a file path like C:\Scripts\ScriptLog.txt on the clipboard. Run the script, and the file will open up in Notepad (or whatever application you have associated with .txt files). If you have the path to a .doc file on the clipboard, the script will open that document in Microsoft Word. It’s really more of a general purpose file opening script than it is a Web site-only opening script.


How Can I Remove Blank Spaces Before and After a String Value?

Hey, Scripting Guy! We have a custom in-house application that writes data to a log file. The problem is that the data often has a bunch of blank spaces before and after it. How can I remove all the blank spaces that come before and after my data?

-- KS

Hey, KS. You might find this hard to believe, but the Scripting Guy who writes this column occasionally has problems fitting into the modern world. For example, everyone in the world has a cell phone … everyone except you-know-who. Everyone in the world takes one look at a high-definition TV set and immediately plops down $5,000 to buy one … well, almost everyone. Everyone in the world has seen at least one episode of The Sporanos, Lost, or Survivor, right? Um, next question please. You get the idea: the poor guy is so stuck in the past that he even wears his baseball hat forwards instead of backwards!

That’s a good idea: the next time a traveling freak show comes to Redmond we’ll give them a call and let them know about this guy.

Needless to say, it’s easy to make fun of this poor little guy (as well as wonder how someone who doesn’t have a GPS navigation system manages to get through the day). But now thanks to your question, KS, this out-of-place Scripting Guy suddenly has a purpose in life. As it turns out, hundreds of years ago, back when dinosaurs ruled the earth and phones all had cords, this Scripting Guy was introduced to computers the hard way: by having to learn dBase III. (Yes, we know. But people were cruel back in those days.) And, for some reason, a common problem with the dBase III applications he had to deal with was this: invariably data would end up looking like your log file (for illustration purposes, pretend the asterisk marks the end of the data):

Ken Myer          *
                    Pilar Ackerman     *
     Gail Erickson     *

Note. Why did the data invariably look like this? Who knows? Back then data with extraneous blank spaces was simply accepted as being the way of the universe; instead, the big debate was whether the sun revolved around the earth or vice-versa.

A question our Scripting Guy has decided to reserve judgment on, at least for the time being.

The important point is that, back in the good old days, our Scripting Guy actually served a useful purpose: he knew how to write code that could remove extraneous spaces from the beginning and the end of a string. And now, for one brief moment at least, his life once more has meaning. Here’s a script that will open a text file, remove excess blank spaces from the beginning and end of each line, and then save the modified lines back to the file:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strLine = Trim(strLine) & vbCrLf
    strText = strText & strLine
Loop

objFile.Close

Set objFile = objFSO.OpenTextFile("c:\scripts\test.txt", ForWriting)
objFile.Write strText
objFile.Close

This script starts out by defining a pair of constants: ForReading and ForWriting. Because of the quirks of the FileSystemObject (the scripting object used to work with text files) we have to open the log file twice to accomplish our task: once to read from the file and once to write the modified values back to the file. These constants simply tell the script which operation – reading or writing – we want to perform. (That’s one of the quirks we were talking about: you can open a file for reading or for writing, but you can’t do both at the same time.)

After defining the constants we create an instance of the FileSystemObject and then use the OpenTextFile method to open the file C:\Scripts\Test.txt for reading. That brings us to this block of code:

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    strLine = Trim(strLine) & vbCrLf
    strText = strText & strLine
Loop

What we’re doing here is using the ReadLine method to read the file line-by-line. When we first enter the Do Until loop we read the first line in the text file and store that value in a variable named strLine. That means strLine will equal this:

Ken Myer          *

Now comes that vintage programming magic you’ve been waiting for. In the next line of code we use the Trim function to remove all the blank spaces before and after our string (we also tack on a carriage return-linefeed to make sure that this value will be written as a separate line in the text file):

strLine = Trim(strLine) & vbCrLf

As you probably already guessed, the Trim function automatically removes extraneous blank spaces before and after a string. That means that strLine is now equal to this (remember, the asterisk isn’t really there, we’re just using it to indicate the end of the data):

Ken Myer*

Cool, huh? We then add the value of strLine to a variable named strText, a variable we use to keep track of all the text file lines we trim:

strText = strText & strLine

And then we loop around and repeat the process with the next line in the text file, a cycle which continues until we’ve run out of lines to read (or, in more technical terms, until the AtEndOfStream property is true).

The rest is easy. We close the file Test.txt and then immediately reopen it, this time for writing. With the file open for writing we call the Write method to write the value of strText to the file, close the file and, not coincidentally, call it a day. If you open the file Test.txt now, you’ll see the data has been trimmed nice and neat:

Ken Myer*
Pilar Ackerman*
Gail Erickson*

We’d like to see your fancy-schmancy Blackberry device or cappuccino machine do that!

Incidentally, there are two related functions you might find useful. LTrim trims excess spaces only from the beginning (the left side) of a string. In other words, you end up with data like this:

Ken Myer          *
Pilar Ackerman     *
Gail Erickson     *

Conversely, RTrim trims excess black spaces from only the end (right side) of a string:

Ken Myer*
                    Pilar Ackerman*
     Gail Erickson*

There you have it, KS. And remember, if you ever have a question about something no modern human being is likely to know how to do (like, say, change the channel without using the remote), well, now you know where to go for help.


How Can I Show Users a Dialog Box That Only Lets Them Select Folders?

Hey, Scripting Guy! In previous columns you’ve shown us how to present users with a dialog box that enables them to select files. Is there any way to show them a dialog box that only lets users select folders?

-- LP

Hey, LP. You know, you’ve come to the right place: the Scripting Guys have just what the doctor ordered.

Actually, we have the BrowseForFolder method, which is part of the Windows Shell object. If that’s the sort of thing your doctor is prescribing you might think very seriously about getting a second opinion.

But while we’d be a bit suspicious of a doctor who’s prescribing the BrowseForFolder method we have no qualms about prescribing this ourselves. Let’s show you a sample script, and then we’ll talk about how it works:

Const WINDOW_HANDLE = 0
Const OPTIONS = 0

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.BrowseForFolder _
    (WINDOW_HANDLE, "Select a folder:", OPTIONS, "C:\")

If objFolder Is Nothing Then
    Wscript.Quit
End If

Set objFolderItem = objFolder.Self
objPath = objFolderItem.Path

Wscript.Echo objPath

We begin by defining a pair of constants: WINDOW_HANDLE and OPTIONS. The WINDOW_HANDLE constant represents a numeric ID that needs to be assigned to the dialog box we’re going to display; for scripts this value should always be 0. Setting OPTIONS to 0 means we’re going to display a very simple dialog box, one that limits users to selecting from a list of folders. Alternatively, we could have set OPTIONS to &H10&. In that case our dialog box would include a text area where users could type a folder path.

After defining our constants we create an instance of the Shell.Application object and then use this code to display a browse-for-folder dialog box:

Set objFolder = objShell.BrowseForFolder _
    (WINDOW_HANDLE, "Select a folder:", OPTIONS, "C:\")

As you can see, we merely call the BrowseForFolder method, passing four parameters:

· WINDOW_HANDLE, which, as we noted, is the numeric ID assigned to the dialog box window.

· The text string Select a folder:, which will serve as the instructional message displayed in the dialog box.

· OPTIONS, the constant representing the options used in constructing the dialog box.

· C:\, which serves as the root folder for our dialog box. The dialog box will open in C:\ and will not allow you to select any file locations higher up in the tree (for example, you can’t select My Computer). Had we set the root folder to C:\Scripts then the user would be allowed to select only the folder C:\Scripts and any of its subfolders.

That code will result in a dialog box similar to this appearing on screen:

(If you’re wondering, yes, you’ve seen this dialog box before. Many Windows applications make use of this same method and same dialog box.)

At this point our script pauses, waiting for the user to either select a folder and click OK or to click Cancel. When the user does one of these two things the dialog box is dismissed and the actions are stored in the object reference objFolder.

So how do we know whether the user selected a folder and clicked OK or whether the user simply clicked Cancel? That’s what this block of code is for:

If objFolder Is Nothing Then
    Wscript.Quit
End If

This code checks our object reference (objFolder) to see if it happens to be equal to a real object (that’s what the Nothing keyword is for). If objFolder is equal to Nothing that means the user clicked Cancel; in that case we simply use Wscript.Quit to exit the script. If objFolder is not equal to Nothing then objFolder must refer to a real object; therefore the script continues on its merry way.

The next two lines of code are required due to a quirk in the Shell object:

Set objFolderItem = objFolder.Self
objPath = objFolderItem.Path

When a user selects a folder and clicks OK they get back an instance of the Shell Folder object. For some reason, however, you can’t do anything with a Shell Folder object; if we want to retrieve the path to the selected folder we have to use a FolderItem object instead. (Why? We have no idea.) Therefore our first line of code uses the Self method to return a FolderItem object that happens to be identical to our Folder object. The second line of code then stores the Path to that FolderItem object in a variable named objPath. A bit awkward, but it works.

Finally, we echo back the path to the selected folder and we’re done:

As we noted, our sample dialog box uses C:\ as the root folder and won’t let you select folders located anywhere else on the computer. Sometimes that’s good; that forces users to choose from a certain set of folders. At other times, however, you’d like to give users the opportunity to select any folder located anywhere on the file system. Is that possible?

You bet it is. We won’t go into the details of this modified script, but this one sets My Computer as the root folder:

Const
MY_COMPUTER = &H11&
Const WINDOW_HANDLE = 0
Const OPTIONS = 0

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(MY_COMPUTER)
Set objFolderItem = objFolder.Self
strPath = objFolderItem.Path

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.BrowseForFolder _
(WINDOW_HANDLE, "Select a folder:", OPTIONS,
strPath)

If objFolder Is Nothing Then
Wscript.Quit
End If

Set objFolderItem = objFolder.Self
objPath = objFolderItem.Path

Wscript.Echo objPath

As you might expect, the resulting dialog box gives you plenty of options to choose from:

Just what you needed. And, hopefully, not what the doctor ordered.


How Can I Truncate Names to a Maximum of 16 Characters?

Hey, Scripting Guy! How can I truncate names to a maximum of 16 characters?

-- BN

Hey, BN. Brace yourself: it’s time for another trip down memory lane. When one of the Scripting Guys was going to college, he took a temporary summer job with the Green Giant Corporation, working in the office that oversaw the asparagus harvest in eastern Washington. At that time Green Giant had a clunky old computer system that was used to record the amount of asparagus – and thus the amount of money owed – to all the harvesters. This system had one problem, however (well, actually, it had more than one problem, but …): it had been set up to accept a maximum of 10 characters as a last name, and many of the workers had last names longer than that (e.g., Myer-Ackerman). Each time a data entry person entered a name longer than 10 characters the entire system would lock up, and their workstation would often have to be rebooted. (This wall all in the days before Microsoft Windows.)

Now, this Scripting Guy was anything but a Scripting Guy back then; in fact, his computer experience was largely limited to playing Zork on a Commodore 64. Nevertheless, as the lone college boy in the office he was given the task of trying to fix the data entry program (the database itself was off limits, and for good reason). His job was to make sure that any name longer than 10 characters was truncated before the program tried to save that name to the database.

In other words, something very similar to what you need to do. Back then our Scripting Guy was working in some proprietary offshoot of BASIC, but his solution was to use the Left function to take just the first 10 letters of a name and call that the worker’s last name. And now, 20-some years later, guess what the solution is to your problem?

strName = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
strName = Left(strName, 16)
Wscript.Echo strName

The classic solutions never go out of style, do they? This script picks up where we left off 20 years ago: it assigns a big long string (in this case, the alphabet) to a variable named strName:

strName = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

That brings us to this line of code:

strName = Left(strName, 16)

With this line of code we’re assigning a new value to the variable strName. And what is that new value? It’s going to be the first 16 characters of the existing value of strName. That’s the purpose of the Left function: it starts with the first character in the string and counts out the next 16 characters, like so:

ABCDEFGHIJKLMNOP QRSTUVWXYZ

Those 16 characters, and only those 16 characters, get assigned to the variable strName. We then echo back the new value of strName, and we get this:

ABCDEFGHIJKLMNOP

Cool. That works great, and it doesn’t matter whether strName starts off with 26 characters of 2600 characters: in the end, only the first 16 characters will get used. Ah, but what happens if a string has less than 16 character? For example, is this script going to fail, because it tries to grab 16 characters and there aren’t 16 characters?

strName = "ABCD"
strName = Left(strName, 16)
Wscript.Echo strName

Fortunately the answer is no: this script works just fine. If a string has less than 16 characters the Left function simply takes whatever characters it can find and then assigns that value to strName. In this case, strName will be equal to ABCD, the only 4 characters in the string.

Now, back to reminiscing about the good old days. Did we ever tell you about the time this Scripting Guy was sitting in his office at Green Giant when a rattlesnake slithered in? It’s true. He was sitting there, trying to figure out how to limit last names to 10 characters when -- oops; we’re out of time for today. We’ll have to get back to that story some other time.


Why Doesn't My LIKE Query Work on Windows 2000?

Hey, Scripting Guy! I’m trying to use a LIKE query in my WMI scripts. The query works fine on Windows XP computers, but I can’t get it work on Windows 2000 computers. Do you know what I’m doing wrong?

-- GS

Hey, GS. Actually you’re not doing anything wrong. You’re using a script similar to this, which uses the LIKE operator to search for all the files on a computer whose file name includes a ~ (these are almost always temporary files):

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile where FileName Like '%~%'")
For Each objFile in colFiles
    Wscript.Echo objFile.Name
Next

And you’re right: this works great on Windows XP and Windows Server 2003, and we encourage people to take advantage of this new capability; it can be incredibly useful. However, those are the only two platforms which support the LIKE operator; this script won’t work on Windows 2000 (or Windows NT 4.0 or Windows 98) because those platforms don’t support the LIKE operator. And, before you ask, there are no plans to issue an update to older versions of Windows so that they will support this capability. Sorry; we wish there were such plans, too.

So is there a workaround for this problem? Well, kind of, but it’s not a particularly good one. About the best you can do is return a list of all the files on a computer, and then use the VBScript function InStr to see if the tilde appears in any of the file names. The following script does just that, although to ensure that the script runs a bit faster we added a WHERE clause that limits the search solely to the Scripts folder. Otherwise, the script would return a list of all the files on the computer, something we don’t want to do in a testing situation:

strComputer = "."
Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService.ExecQuery _
    ("Select * from CIM_DataFile WHERE Path = '\\Scripts\\'")
For Each objFile in colFiles
    If InStr(objFile.Name,"~") <> 0 Then
        Wscript.Echo objFile.Name
    End If
Next


How Can I Add a Right-to-Left Text Box to a Web Page?

Hey, Scripting Guy! How can I add a right-to-left text box to a Web page?

-- B

Hey, B. To begin with, we should note that we aren’t entirely sure that we know what we’re talking about here. (Good point: that’s a disclaimer that could be added to all of our columns, couldn’t it?) We are aware of languages – Arabic languages and Hebrew, for example – that are read from right-to-left; however, we aren’t exactly proficient in any of those languages. (And, as our editor continually points out, we aren’t all that good when it comes to English, either.)

Nevertheless, we think (hope) that this is what you had in mind:

<SCRIPT LANGUAGE="VBScript">
    Sub TestSub
        Msgbox ScriptArea.Value
    End Sub
</SCRIPT>

<body>
    <textarea name="ScriptArea" rows=5 cols=70 dir="rtl"></textarea><p>
    <input id=runbutton  type="button" value="Run Script" onClick="TestSub">
</body>

Believe it or not, this is even easier than it looks; after all, much of the code is there in order to: 1) create a button (which you might not need in your Web page or HTA), and 2) to add a subroutine that runs when the button is clicked. As it turns out, the part you really care about consists of nothing more than a single HTML tag:

<textarea name="ScriptArea" rows=5 cols=70 dir="rtl"></textarea>

What we’re doing here is adding a multi-line text box control (or a <textarea>) named ScriptArea; we’re also sizing the text box so it displays 5 rows, with 70 columns per row. Nothing too fancy there. But take a look at this additional property assignment: dir="rtl". That’s the cool part. The dir property determines the reading order of a text box. If dir is set to rtl then the reading order is right-to-left, which is what you were looking for. If dir is set to ltr (or if the dir property is omitted altogether) then the reading order is set to left-to-right. Believe it or not, that’s all we have to do to create a right-to-left text box.

Here’s what the text box looks like when added to an HTA (HTML Application):

Not bad, if we do say so ourselves. (Or, if you’re reading from right-to-left: dab toN.) Type the information into the text box and click the Run Script button; with any luck, a message box will appear echoing back the contents of the text box.

Now, we know what you’re thinking: yes, the text is right-justified and, yes, the periods are in an unusual place. However, the text doesn’t actually read right-to-left. True: that’s because we’re typing in English, which uses the so-called Latin alphabet. Text will not be displayed right-to-left when using a Latin alphabet. But things will be very different if you type using Arabic, Farsi, Hebrew, or any other language that does read from right-to-left. (Try it and see.)

Note. You say you don’t have any idea what an HTA is, and you don’t really understand what it means to add an HTML control? Then you need to check out the HTA Developers Center here in the Script Center.

Because we have some time to kill (no baseball game tonight), we thought we’d toss in some bonus code, just for the heck of it. The following Web page/HTA has two buttons on it: not only does it have the Run Script button but it also has a button labeled Switch Direction. When you click the Switch Direction button the following subroutine runs:

Sub SwitchSub
    If ScriptArea.dir = "ltr" Then
        ScriptArea.dir = "rtl"
    Else
        ScriptArea.dir = "ltr"
    End If
End Sub

As you can see, this subroutine checks the value of ScriptArea’s dir property. If dir is equal to ltr, then the value is changed to rtl; if the value is not equal to ltr (meaning it must be equal to rtl) then the value is changed to ltr. To put all that in somewhat plainer English, the button simply toggles ScriptArea between right-to-left and left-to-right. Click the button once and the text box reading direction will be left-to-right; click it a second time, and the reading direction switches back to right-to-left. Etc. etc.

Here’s the complete code:

<SCRIPT LANGUAGE="VBScript">
    Sub TestSub
        Msgbox ScriptArea.Value
    End Sub

    Sub SwitchSub
        If ScriptArea.dir = "ltr" Then
           ScriptArea.dir = "rtl"
        Else
           ScriptArea.dir = "ltr"
        End If
    End Sub
</SCRIPT>

<body>
    <textarea name="ScriptArea" rows=5 cols=70 dir="rtl"></textarea><p>
    <input id=runbutton  type="button" value="Run Script" onClick="TestSub">
    <input id=switchbutton  type="button" value="Switch Direction" onClick="SwitchSub">
</body>


How Can I Add an Edit Command to the Context Menu for an HTA?

Hey, Scripting Guy! How can I add an Edit command to the context menu for an HTA? That way, any time I right-click an HTA file I can click Edit and then open the file in Notepad.

-- TC

Hey, TC. You know, one of the Scripting Guys once had a friend who got a summer job baking pies in a local bakery. Admittedly, that sounds like the absolute perfect job and it was, except for one thing: this friend never actually got to eat the pies; because the pies were sold whole he didn’t get to cut himself a slice every now and then. As it turned out this was the worst possible job: perpetually surrounded by the aroma of freshly-baked pies, yet unable to eat a single bite.

We Scripting Guys can empathize with this poor pie-baking friend. After all, every day we write new scripts, yet we rarely get to use any of these scripts ourselves. We’ve written scripts for managing Active Directory, yet, for some strange reason, they won’t let us manage the Microsoft Active Directory. (Go figure.) We’ve written scripts for managing DNS servers, but we’re not allowed to put up our own DNS servers. We’re written scripts for creating charts in Excel, but they won’t let us create charts in Excel.

Well, OK, they’ll let us do that; we just don’t have anything worth charting.

But this task is different. We use HTAs all the time, and we invariably right-click an HTA and then click Edit; in turn, that opens the HTA up in Microsoft Word (or Microsoft FrontPage, depending on what we have installed on that particular machine), which is not our preferred editor for HTAs. Like you, we’d just as soon use Notepad, which means that the Scripting Guys could also benefit from a script that would add an Edit command to HTA files. You know, a script similar to this:

Const HKEY_CLASSES_ROOT = &H80000000

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "htafile\Shell\Edit\Command"
objRegistry.CreateKey HKEY_CLASSES_ROOT,strKeyPath

strValue = "%SystemRoot%\system32\NOTEPAD.EXE %1"
objRegistry.SetExpandedStringValue HKEY_CLASSES_ROOT,strKeyPath,vbNullString,strValue

If you want to add a command to the context menu for an HTA file (and obviously you do) you first need to add a new key to the registry; in particular, you need to go to HKEY_CLASSES_ROOT and add the key htafile\Shell\Edit\Command. That’s what we do in the first half of the script. We define a constant named HKEY_CLASSES_ROOT and set the value to &H80000000; that tells the script which registry hive to work with. We then connect to the WMI service on the local computer (although this script can also be run against remote computers), binding to the StdRegProv class in the root\default namespace.

Pretty easy so far, right? We then use this line of code to assign the path (within HKEY_CLASSES_ROOT) to the new registry key to a variable named strKeyPath:

strKeyPath = "htafile\Shell\Edit\Command"

Once we’ve done that we can then use the CreateKey method to create our new key. To do this we simply call CreateKey and pass it two variables: the constant HKEY_CLASSES_ROOT and the variable strKeyPath. Or, to be more precise:

objRegistry.CreateKey HKEY_CLASSES_ROOT,strKeyPath

What’s that? What if you already have a registry key named htafile\Shell\Edit\Command? Hey, no problem. If the key already exists nothing bad will happen; the script will simply skip the CreateKey method and move to the next line of code. No error occurs if the registry key already exists.

And, yes, that is kind of nice, isn’t it?

What we have to do now is assign the actual command to be carried out (that is, “Open this file in Notepad”) to the default value of the Command key. That’s not especially hard but it is a tiny bit tricky. So maybe we should explain to you how it works.

As you know, we want HTA files to have a context menu similar to VBScript files: we want to be able to right-click an HTA file, click Edit, and then have the HTA open up in Notepad. To be honest, we weren’t 100% sure how to do that, so we simply took a peek at the default value for the VBSFile\Shell\Edit\Command key. That looks like this:

%SystemRoot%\system32\NOTEPAD.EXE %1

Will that work for HTA files as well? Hey, you’ll never know until you try, right? With that in mind, we assigned that same string to a variable named strValue:

strValue = "%SystemRoot%\system32\NOTEPAD.EXE %1"

Now we just need to assign the value of this variable to the default value for htafile\Shell\Edit\Command. Before we do that we need to keep two things in mind. First, you might have noticed that the environment variable %SystemRoot% is embedded in our command. That’s good: it means that we can enter the path to Notepad without having to know whether the Windows directory is C:\Windows or D:\Winnt or something else altogether. However, this also means that we have to configure the default value with a REG_EXPAND_SZ data type. A registry value with this data type will automatically “expand” an environment variable with the actual physical path; in other words, it will change %SystemRoot%\system32\NOTEPAD.EXE into something like C:\Windows\system32\NOTEPAD.EXE.

Is it going to be hard to give the default value a REG_EXPAND_SZ data type? Come on: if it was going to be hard do you really think the Scripting Guys would try it? All we have to do is make sure that, when we assign the value, we use the SetExpandedStringValue method.

Good point. Several of you have wondered why we keep using the generic phrase “default value” rather than using the name of the registry value we need to modify. There’s actually a good reason for that: the default value for a registry key doesn’t have a name. (It shows up as (Default) in RegEdit, but that’s just a placeholder value and is not its name.) So how can we access a registry value that doesn’t have a name? Fortunately, that turns out to be fairly easy, too: we simply don’t specify a name when we call the SetExpandedStringValue method. Take a look at the line of code where we call SetExpandedStringValue:

objRegistry.SetExpandedStringValue HKEY_CLASSES_ROOT,strKeyPath,vbNullString,strValue

Notice the four parameters: the constant HKEY_CLASSES_ROOT; the variable strKeyPath; the VBScript constant vbNullString; and the variable strValue. The secret to working with the default value for a registry key lies in using vbNullString (which simply represents a Null value) instead of a value name. When you specify vbNullString in place of the value name then you will automatically be connected to the default value.

Isn’t WMI wonderful?

Actually WMI is wonderful, but we don’t really have time to chat today. For some reason, we have a hankering for a big piece of blackberry pie.

Make that two big pieces of blackberry pie. With maybe a slice of chocolate cream pie for dessert.


How Can I Allow Users to Type Text Into an HTA Table?

Hey, Scripting Guy! I’m using an HTA as a data entry online form. The easiest way for me to format the HTA is to use a table; however, I can’t figure out how to let people type text into some of the cells in that table. How can I do that?

-- EN

Hey, EN. You know, Saturday night the Scripting Guy who writes this column was watching the football game between the University of Washington and UCLA – What’s that? Who won that game? Well, um, we don’t remember right off hand ….

Besides, that’s not important. What’s important is that every other commercial during the broadcast (and, if you’ve ever watched college football on TV, you know that there are plenty of commercials) was for a new service where you can “text-message beautiful girls any time of the day or night.” (As one of beautiful girls exclaimed, “Wow, I can receive your message even in this noisy nightclub!”) To be honest, it wasn’t totally clear to the Scripting Guy who writes this column whether this was one service running different commercials or rival services competing head-to-head. However, he was struck by the tagline to one of these ads, a tagline which went something like this:

“Text-message girls for fun. Or for more than fun.”

In case you’re wondering, the Scripting Guy who writes this column didn’t text-message any beautiful girls, even though they could receive his messages in a noisy nightclub. (Noise apparently being more of a hindrance to text-messaging than the Scripting Guy who writes this column knew.) Admittedly, that was due in part to the fact that he has no idea how to send a text-message. However, he also had concerns about what would happen if things didn’t go well. Could he file a compliant with the text messaging company:

“You said it would be fun to text-message beautiful girls.”

“No, we said it would be more than fun. Obviously more than fun isn’t fun. If it was, we’d just say that you could send text messages for fun and be done with it.”

That was way too complicated for the Scrpting Guy who writes this column, so he just watched the Husky-UCLA game.

Which, come to think of it, wasn’t all that much fun, either.

Note. Does the Scripting Guy who writes this column have any regrets about not text-messaging beautiful girls Saturday night? To be perfectly honest, he does: he feels a little sorry for those poor girls, girls who are probably still sitting in that noisy nightclub, hoping to hear from him. Hopefully, they’ll get over it.

Editor’s Note: In case you’re wondering, all those girls not only got over it, but they’re grateful. Restraining orders can be a bit of a hassle.

But hey, who needs beautiful girls or text-messaging anyway? After all, if we want to have some real fun, we’ll throw together some code that enables you to type text into a table cell in an HTA:

<SCRIPT LANGUAGE="VBScript">
    Sub ShowName
        Msgbox NameBox.InnerText
    End Sub
</SCRIPT>

<table width="100%" border>
  <tr>
    <td width="50%" valign="top">Name</td>
    <td width="50%" valign="top"><div id="NameBox" contenteditable></div></td>
  </tr>
</table>

<p>

<input class="button" type="button" value="Show Name" onClick="ShowName">

Our HTA consists of three elements:

· A function named ShowName which, when clicked, pops up a message showing the value that was typed into the table cell.

· A table featuring two cells: a cell labeled Name, which is just that (a label), and an editable cell where the user can type his or her name.

· A button which, when clicked, calls the function ShowName.

In other words, we have an HTA that looks like this:

In order to explain how this code works we’re going to do something a little different: we’re going to start at the end of the script and then work our way back towards the beginning. And yes, we know: quite often our explanations seem like they were written backwards. But this time we’re doing that on purpose.

We were right, weren’t we: this is way more fun than hanging out with beautiful girls, isn’t it?

The last (but hardly the least) of our HTA elements is a standard HTML button, one that – as noted – calls the function ShowName each time it gets clicked. The HTML tagging for our button is no more complicated than this:

<input class="button" type="button" value="Show Name" onClick="ShowName">

In addition to the button, our HTA also includes a simple two-cell table, a table we create using this bit of tagging:

<table width="100%" border>
  <tr>
    <td width="50%" valign="top">Name</td>
    <td width="50%" valign="top"><div id="NameBox" contenteditable></div></td>
  </tr>
</table>

If you’ve ever created a table using HTML then those tags should look pretty familiar to you. (If you haven’t created a table using HTML then you should take a look at our HTA Developer’s Center.) Of course, if you’ve ever created a table using HTML you also know that tables are read-only: you can’t type text directly into a table cell. And guess what? You’re absolutely right: you can’t type text directly into a table cell.

But that doesn’t matter to us, because we aren’t going to type text directly into the table cell; instead, we’re going to cheat a little. Take a look at the code that configures the first cell in the table:

<td width="50%" valign="top">Name</td>

Nothing too fancy there: it’s a cell that spans half the table width (width=”50%”) and has text aligned with the top of the cell (valign=”top”). In addition, the cell includes the word Name already typed in for you.

Now, compare that with the code that configures the second cell in the table:

<td width="50%" valign="top"><div id="NameBox" contenteditable></div></td>

See the difference? Instead of including some pre-defined text (like Name), we instead use a <DIV> tag for the cell contents:

<div id="NameBox" contenteditable></div>

What’s a <DIV> tag? Well, a <DIV> is simply a named portion of an HTML page or HTA; by defining a <DIV> and giving it an id (in this case, the id NameBox) we can refer to and manipulate this portion of our page programmatically. But notice that we also included a second parameter when defining our <DIV>:

contenteditable

That’s the secret right there: the contenteditable property, when present, makes the <DIV> editable; in other words, that one little parameter enables the user to type data into that portion of the HTA. And because the <DIV> is located within a table cell that means that users can type information into that table cell. (Although, technically, they’re typing into the <DIV> and not into the cell itself. But, hey, we’re supposed to be having fun here; let’s not quibble over technicalities.)

See how easy that was? Would you like to have a second field in your HTA, one for typing in the user’s phone number? No problem. In the following code snippet we’ve added a second table row (the <TR> tag) and we’ve:

· included a <DIV> in cell 2 of that row, a <DIV> we named PhoneBox.

· added the contenteditable parameter in order to make this <DIV> editable.

Here’s our revised HTML:

<table width="100%" border>
  <tr>
    <td width="50%" valign="top">Name</td>
    <td width="50%" valign="top"><div id="NameBox" contenteditable></div></td>
  </tr>
    <td width="50%" valign="top">Phone Number</td>
    <td width="50%" valign="top"><div id="PhoneBox" contenteditable></div></td>
  </tr>
</table>

You can keep adding additional table rows and editable cells until the cows come home.

Note. So what happens when the cows do come home? Then you’ll have to stop adding additional table rows and editable cells.

So does this really work? Of course it does. Here’s our original HTA; note the name in the table cell, a name that doesn’t appear anywhere in our code (which must mean that we were able to type it in ourselves):

You know, you’re right: that’s not just fun, that’s more than fun.

Ah yes, we had a feeling someone was going to ask that: how do you retrieve the value that was typed into the table? As it turns out, that’s what the subroutine ShowName is for:

Sub ShowName
    Msgbox NameBox.InnerText
End Sub

Again, nothing very fancy here: we’re simply using the Msgbox function to echo back the value of NameBox’s InnerText property. (InnerText returns just the text typed into a container, stripping out any HTML formatting codes that might also appear within that container.) Assuming we typed Ken Myer into the table cell (and we did) that’s going to result in a message box that looks like this:

To tell you the truth, that might be a little too much fun, at least for the Scripting Guys.

That should get you started, EN. And don’t worry, the Scripting Guys will continue to watch a lot of TV, the better to keep you up-to-date in all the latest TV commercials. For example, the other night the Scripting Guy who writes this column saw an ad with this tagline:

“The only thing you can expect is the unexpected.”

Of course, technically the only thing that you can’t expect is the unexpected: if you could expect it, well, then it wouldn’t be unexpected, would it? But we suppose the tagline “The only thing you can expect is the expected” doesn’t carry quite the same punch.


How Can I Alternate Row Colors in an HTA?

Hey, Scripting Guy! How can I alternate row colors (using the green/white style) in an HTA?

-- MK

Hey, MK. You know, we almost didn’t answer you question. Not because it wasn’t a good question, but simply because – like all the questions we get – we received this one in email. Based on what happened recently at Radio Shack, the Scripting Guys are a little leery about reading their email these days. And for good reason.

Note. Actually, we like reading our email: it’s heartening to see how many people care about the Scripting Guys. For example, nearly every message we get starts out “What is wrong with you guys?” We’d just like to say that we’re all doing fine. But thank you for asking; it brightens our day to know that you worry about us like that!

Of course, having gone ahead and read your email, we don’t have much choice except to try and answer it, do we? (Hmmm, maybe that’s another reason why we shouldn’t read our email ….) Correct us if we’re wrong (note to the people who do the hiring – and firing – at Microsoft: the Scripting Guys are never wrong), but it sounds like you want to dynamically create a table in an HTML Application (HTA). But not just any table; instead, you want a table where the rows are displayed in alternate colors (green, then white, then green, then white, then ….). Can you do that? You bet you can. And while there are probably a number of different ways to accomplish that feat we chose this approach:

<html>

<SCRIPT Language="VBScript">

    Sub CreateTable
        strHTML = "<table width='100%' border>"

        For i = 1 to 11
            x = i Mod 2
            If x = 0 Then
                strHTML = strHTML & "<tr>"
            Else
                strHTML = strHTML & "<tr bgcolor='lightgreen'>"
            End If
            strHTML = strHTML & "<td width='100%'>&nbsp;</td>"
            strHTML = strHTML & "</tr>"
        Next

        strHTML = strHTML & "</table>"
        TableArea.InnerHTML = strHTML
    End Sub

</SCRIPT>

<body>
    <input id=runbutton type="button" value="Create Table" name="run_button" onClick="CreateTable"><p>
    <span id=TableArea></span>
</body>

</html>

What’s that? Explain how this all works? Sorry; we don’t have time for that. Besides, what are they going to do if we don’t explain how this all works: fire us?

On second thought, maybe we should explain how this all works. To begin with, the HTA is about as simple as we could make it. For example, the body of the HTA consists of just two items: a button that, when clicked runs a subroutine named CreateTable; and a span with the ID TableArea. As you can probably guess, that’s the portion of the HTA window where we’ll create our table.

Here’s what the <BODY> tag, in all its limited glory, looks like:

<body>
    <input id=runbutton type="button" value="Create Table" name="run_button" onClick="CreateTable"><p>
    <span id=TableArea></span>
</body>

The true excitement takes place in the subroutine CreateTable, where we dynamically create the green-and-white striped table. In order to do that we need to construct the table in memory and then write the completed table to the HTA. Step 1 in that process is to assign the starting tag for the table to a variable named strHTML:

strHTML = "<table width='100%' border>"

Now comes the fun part. For demonstration purposes we’re going to create a table consisting of 11 blank rows. With that in mind, we begin by setting up a For Next loop that runs from 1 to 11:

For i = 1 to 11

We then have this unusual line of code:

x = i Mod 2

As it turns out, the Mod operator divides two numbers (in this case, it divides the counter variable i by 2) and then returns the remainder. (Strange but true.) In other words, the first time we run through our loop the counter variable i is equal to 1. Therefore, this line of code divides 1 by 2, which results in zero with a remainder of 1, and stores the remainder (1) in the variable x.

Why the heck would we do that? Well, we want to alternate row colors: odd-numbered rows will be green, even-numbered rows will be white. One really easy way to determine whether a number is even or odd is to divide it by 2 and take a peek at the remainder. If the remainder is 0 then we have an even number; if the remainder is anything but 0 then we have an odd number. Seeing as how the Mod operator returns just the remainder we can determine whether a number (and thus a row in the table) is odd or even simply by checking to see if x is equal to 0:

If x = 0 Then

Suppose we do have an even number; what then? Well, if the remainder is equal to 0 we simply tack on code for creating a new table row to the variable strHTML:

strHTML = strHTML & "<tr>"

And if the remainder is not equal to 0, meaning we have an odd number? In that case we also tack on code to create a new table row; the only difference is that, this time around, we include the bgcolor parameter and set the background color of the row to lightgreen:

strHTML = strHTML & "<tr bgcolor='lightgreen'>"

See how that works? For even-numbered rows we create a new row without specifying a background color; for odd-numbered rows we create a new row and set the background color to lightgreen. The net effect: we’ll end up with alternating rows of green and white.

After creating the new row we use this code to create a new table cell (a cell containing a blank space, as indicated by the &nbsp construction), then use the </tr> tag to close off the row:

strHTML = strHTML &
"<td width='100%'>&nbsp;</td>"
strHTML = strHTML &
"</tr>"

With row number 1 finished we then loop around and repeat the process until we’ve added 11 rows to our table. That brings us to these two lines of code:

strHTML = strHTML & "</table>"
TableArea.InnerHTML = strHTML

In the first line we add the </table> tag to the variable strHTML; that simply indicates that the table is now complete. We then use the second line of code to set the InnerHTML property of the TableArea span to the value of strHTML (strHTML, of course, contains all the HTML tagging required to create a green-and-white striped table). The moment we do that our HTA should look like this:

Which is pretty much what we wanted it to look like.

Note. Yes, we know: <SPAN> tags and InnerHTML properties; all that stuff can be terribly confusing if you’re new to HTAs or to dynamic HTML. In that case you might take a peek at some of the articles in the HTA Developers Center.

There you have it, MK; that should be enough to get you started. As for us, we need to buckle down and get to work. Sure, it’s easy to say “The Scripting Guys? Microsoft would never fire the Scripting Guys.” But that’s what they said about Clippy the Paperclip, too. If Microsoft is willing to fire Clippy, well, it’s hard to believe that anyone’s job could be safe around here. If you need us we’ll be in our office. Working.

Yes, yes: for once.


How Can I Cause a Script to Run Inside an HTA Any Time a User Presses ENTER?

Hey, Scripting Guy! How can I cause a script to run inside an HTA any time a user presses ENTER?

-- JJ

Hey, JJ. Before answering your question we should state, for the record, that the Scripting Guys are unanimously opposed to the recent decision by the International Astronomical Union (IAU) to remove Pluto from the list of planets. (OK, technically the Scripting Guy who writes this column didn’t actually ask any of the other Scripting Guys for their opinion. But seeing as how the Scripting Guy who writes this column is always right, well, there’s very little chance they’d disagree with him, is there?)

Admittedly, some of this displeasure is personal in nature: having gotten an A in elementary school science, in part because he could name all nine of the planets, the Scripting Guy who writes this column is afraid someone will retroactively re-score all his tests and give him a B+ instead. More important, however, there’s little doubt that the IAU (which made this decision based on a vote in which only 5% of its members participated) are picking on Pluto simply because it’s smaller than all the other planets. What kind of message is that sending to the rest of the universe? To tell you the truth, IAU, we’re not impressed; why don’t you tell Jupiter or Saturn that they aren’t planets? Go ahead; we dare you.

That’s what we thought.

Besides, if we feel the need to get rid of a planet, we should be giving Uranus the boot, not Pluto. Not only does Uranus have a name that leaves every English-speaking grade school boy convulsing on the floor with laughter, but Uranus can’t even stand up straight. Instead, it actually floats through space lying on its side, effectively having an East and a West pole rather than North and South poles. Come on, IAU: you call that a planet? Sheesh.

Venus? Listen, don’t get us started on Venus.

Oh, well. In theory, we suppose that some of you might be more interested in knowing how to run a script any time a user presses the ENTER key than you are in hearing the Scripting Guys ramble on and on about the planet Pluto – excuse us: about the dwarf planet Pluto. If that’s the case then this little block of code should be of interest to you:

<SCRIPT Language="VBScript">
    Sub RunScript
        If window.event.Keycode = 13 Then
            Msgbox "You pressed the Enter key."
        End If
    End Sub
</SCRIPT>

<body onkeypress="RunScript">
</body>

Note. Yes, we know: it’s not a very long script, is it? But that’s OK; unlike certain astronomical associations, the Scripting Guys won’t discriminate against something just because it’s not very big.

As you can probably tell, this is one of the least-useful HTML Applications (HTAs) you’ll ever see; that’s because it doesn’t actually do anything other than demonstrate how you can trap and respond to the ENTER key being pressed. But we figured that would be good enough: having seen how the basic idea works you should be able to transfer the concept to an HTA that does a little bit more than just pop up a message box each time you press ENTER. All you’ll have to do is replace our code (a single line that displays a message box) with your code. Is that OK with everyone? Excellent.

So how do we trap and respond to the ENTER key being pressed? To begin with, you might have noticed that the body of HTA consists entirely of a beginning and an ending <body> tag. Before you dismiss that, however, take a closer look at the onkeypress event added to the <body> tag:

<body onkeypress="RunScript">

What does onkeypress do? It does this: each time a user presses a key on the keyboard this event triggers a subroutine named RunScript. (And because the onkeypress event is attached to the <body> tag this happens regardless of which control, if any, has the focus at the time someone presses a key.)

Ah, good point: the only key we really care about is the ENTER key, isn’t it? To tell you the truth, we don’t know a straightforward way to trigger a subroutine if the ENTER key (and only the ENTER key) is pressed. Therefore we took the easy way out and we call the subroutine whenever any key on the keyboard gets pressed.

But don’t panic: that doesn’t mean a message box is going to pop up each time a user presses a key on the keyboard. Instead, take a look at the first line in the subroutine:

If window.event.Keycode = 13 Then

What we’re doing here is grabbing the window.event object (an object created any time an event – a key press, a mouse click, etc. – occurs in the HTA) and then examining the value of the Keycode property. It should come as no great shock that the Keycode property represents the Unicode key code associated with the keyboard key that triggered the event. Here we’re checking to see if the Keycode property is equal to 13, which just happens to be the value assigned to the ENTER key. If the Keycode equals 13 that means the user pressed ENTER; in turn, we display a message box to that effect. If the Keycode is anything but 13 we do nothing at all. Technically the subroutine gets called any time a key (any key) gets pressed on the keyboard. As far as the user experience goes, however, nothing ever happens unless the ENTER key gets pressed. To the user it looks like the subroutine is triggered only when he or she presses the ENTER key.

Sure, it’s a bit of a workaround. But as long as it performs the task we need it to perform, well, what difference does it make?

So how did we know that a Keycode value of 13 meant that ENTER had been pressed? We didn’t; that’s why we looked up Internet Explorer’s ISO Latin-1 Character Set. This is a good resource if you’re interested in responding to other keys and keystrokes; however, we’d encourage you to use it as soon as possible. After all, you never know when the IAU will decide that a, G, 6, w, and @ are no longer valid keys on the keyboard.

Anyway, JJ, this should get you started with your own HTA that runs a subroutine whenever someone presses ENTER. And, yes, you’re absolutely right: the planet Mercury was named after the Roman god of thieves. And the Roman god Saturn? He’s best known for eating his own children.

Just something we thought you – and the International Astronomical Union – should know.


How Can I Center an HTA on the Screen?

Hey, Scripting Guy! How can I center an HTA on the screen?

-- BH

Hey, BH. Well, we have an answer for you, although it’s not perfect: we can resize (if need be) and center your window, but there will be a momentary flash on the screen when all this happens. It’s not too terribly distracting and the net effect is what you want: the HTA will be centered on screen. We were hoping to make this process a little smoother, but this approach will have to do for now.

Here’s the code for our sample HTA. (To try this out, copy the code, paste it into Notepad, and then save the file with a .hta file extension.) The part we care about (and the only part that really does much of anything) is the Window_Onload subroutine, a subroutine which automatically runs any time the HTA is loaded or refreshed:

<html>
<head>

<title>Centered HTA</title>

<HTA:APPLICATION
     ID="objHTA"
     APPLICATIONNAME="Centered HTA"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT Language="VBScript">
    Sub Window_Onload
        strComputer = "."
        Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
        Set colItems = objWMIService.ExecQuery("Select * From Win32_DesktopMonitor")
        For Each objItem in colItems
            intHorizontal = objItem.ScreenWidth
            intVertical = objItem.ScreenHeight
        Next
        intLeft = (intHorizontal - 800) / 2
        intTop = (intVertical - 600) / 2
        window.resizeTo 800,600
        window.moveTo intLeft, intTop
    End Sub
</SCRIPT>

<body></body>
</html>

Start this HTA and – after a brief flash – the window will be resized to 800 pixels by 600 pixels, and will be centered onscreen.

Good question: what is going on here? Well we start off with some standard WMI code that connects to the WMI service on the local computer and then queries the Win32_DesktopMonitor class.

Note. We’re assuming that you have only one monitor connected to your machine. If you have multiple monitors you might have to add a WHERE clause that ensures you will retrieve the screen height and width of the primary monitor.

After connecting to the Win32_DesktopMonitor class we then use these two lines of code to determine the current screen dimensions:

intHorizontal = objItem.ScreenWidth
intVertical = objItem.ScreenHeight

We’ve already decided, in advance, that we want our HTA window to be 800 pixels wide by 600 pixels high. Therefore we can use this code to figure out where the top left-hand corner of the window needs to be positioned:

intLeft = (intHorizontal - 800) / 2
intTop = (intVertical - 600) / 2

As you can see, we’re taking the screen width (stored in the variable intHorizontal) and subtracting 800 pixels (the horizontal size of the HTA window). Let’s assume we have a monitor resolution of 1024x768. Subtracting 800 from 1024 leaves us with 224: that tells how us how much wider the screen is than our HTA window. To center the window, we just need to make sure we have 112 pixels on each side; that’s why we divide 224 (intHorizontal – 800) by 2.

We then repeat the process for the screen height. On our sample monitor, that gives us 768 – 600, or 168. Dividing that by two leaves us with 84 pixels above and below the window.

And that leaves us with two final chores. First, we resize the window to 800 pixels by 600 pixels:

window.resizeTo 800,600

Next, we position the window accordingly. That’s what we do here:

window.moveTo intLeft, intTop

All we’re doing is calling the moveTo method and moving the HTA window so that the top left-hand corner is located 112 pixels in from the left and 84 pixels down from the top.

Incidentally, we put the resizeTo method near the bottom of the subroutine for educational purposes: we wanted to talk about resizeTo and moveTo at the same time. However, you’ll get a slightly less-distracting flash if you start the subroutine off by resizing the window:

Sub Window_Onload
    window.resizeTo 800,600
    strComputer = "."
    Set objWMIService = GetObject("Winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery("Select * From Win32_DesktopMonitor")
    For Each objItem in colItems
        intHorizontal = objItem.ScreenWidth
        intVertical = objItem.ScreenHeight
    Next
    intLeft = (intHorizontal - 800) / 2
    intTop = (intVertical - 600) / 2
    window.moveTo intLeft, intTop
End Sub

It’s not a big deal, but it helps a little. And we’ll continue to hunt for ways to eliminate the flashing. (Why is that so difficult? Well, ideally, we’d hide the HTA window until it has been resized and moved, something you can do with Internet Explorer. However, we haven’t found a way to do that with HTAs. But we haven’t given upon that.)


How Can I Change the Color of the Scrollbars in an HTA?

Hey, Scripting Guy! How can I change the color of the scrollbars in an HTA?

-- IR

Hey, IR. How many times have you thought to yourself, “Gee, the Scripting Guy who writes that column is so smart; how come he isn’t rich?” (OK, true, you’ve probably never thought that. But just play along, OK?) Well, it turns out that there’s a very simple explanation for this: he’s too far out-of-touch with the modern world.

For example, last night the Scripting Guy who writes this column read about a company that converts your physical mail (you know, the kind that the post office delivers) to email. If you’re like most people, apparently you don’t have time to open up the mailbox and go through your mail. Well, thanks to this new company – which expects to soon become a multi-billion dollar company – that’s something you’ll never have to worry about. Instead of having mail delivered to your home or office, you can have your mail forwarded to this company. In turn, trained representatives will open each and every envelope, scan the items into the computer, and then send the scanned images to you via email. You’ll never have to touch a grimy old mailbox again!

It’s safe to say the Scripting Guy who writes this column would never have come up with an idea like that, not in million years. After all, he still trundles down to the mailbox every day, retrieves his own mail, and then – we hope you’re sitting down here – he opens his own envelopes and reads the actual physical contents of each letter! And he never, ever digitizes that mail! The flyer for the local hardware store? The solicitation from the Art Museum? The invitation to learn about real estate opportunities or to buy 1 get 1 free at the Mexican restaurant (Monday through Thursday, 12:00 PM to 2:00 PM only)? Gone, all gone. What is he thinking?!?

Note. To tell you the truth, he’s thinking, “Why in the world would I pay someone $20 a month (the subscription rate to this new service) to digitize all my mail and send it to me via email?” Sadly, it’s thinking like that which is helping to hold back the march of progress.

But you know what they say: it’s always easy to teach an old dog new tricks. (Or something like that; we weren’t paying very close attention when they said whatever it is they said.) With that in mind, rather than type in the code that shows you how to change the color of the scrollbars in an HTA we decided to get with the times, take a screenshot of our solution, and then present you with a digitized version of the script. Here you go, IR:

What’s that? Well, we’re not saying that it’s easy to read … did you try squinting your eyes a little? Give that a try.

You’re right; that just makes it worse, doesn’t it?

OK, so maybe our business model needs a little more work; among other things, we haven’t exactly figured out how to get people to pay $20 a month for the chance to get copies of scripts they won’t be able to read. With that in mind, and while we reassess the business plan, here’s an old-fashioned way of looking at the code:

<STYLE>
  BODY {scrollbar-face-color:red; scrollbar-arrow-color:blue; scrollbar-track-color:green }
</STYLE>

Yes, we know: it’s painful to have to read things in the old-fashioned way, isn’t it? On the bright side, though, at least there isn’t much to read here. As you can see, all we’re doing is defining a style for the body of our HTA; in particular we’re assigning values to three different properties:

· Scrollbar-face-color, which sets or retrieves the color of the scroll box and scroll arrows of a scroll bar.

· Scrollbar-arrow-color, which sets or retrieves the color of the arrow elements of a scroll arrow.

· Scrollbar-track-color, which sets or retrieves the color of the track element of a scroll bar.

Best of all, the syntax for assigning a value to one of these properties is pretty simple: you just reference the property name followed by a colon followed by the assigned value (e.g., scrollbar-face-color:red). If we want to assign values to more than one property all we have to do is separate those individual properties using semicolons.

And not some kind of digitized, virtual semicolon. These are real semicolons, the kind that Ernest Hemingway used to use.

Note to the Scripting Editor. When you get back into town, make sure that Ernest Hemingway actually used semicolons. Also, see if you can out who Ernest Hemingway is.

Of course, some of you might be concerned that, having shown you how to set the preceding three scrollbar properties there’s now nothing left for you to learn on your own. Perish the thought! Here are some additional scrollbar properties that we haven’t worked with:

· Scrollbar-3dlight-color, which sets or retrieves the color of the top and left edges of the scroll box and scroll arrows of a scroll bar.

· Scrollbar-base-color, which sets or retrieves the color of the main elements of a scroll bar, which include the scroll box, track, and scroll arrows.

· Scrollbar-darkshadow-color, which sets or retrieves the color of the gutter of a scroll bar.

· Scrollbar-highlight-color, which sets or retrieves the color of the top and left edges of the scroll box and scroll arrows of a scroll bar.

· Scrollbar-shadow-color, which sets or retrieves the color of the bottom and right edges of the scroll box and scroll arrows of a scroll bar.

And what colors can you set these properties to? For starters, how about the colors found in the Cascading Style Sheets reference documentation?

Meanwhile, the Scripting Guy who writes this column has come up with a newer, and even better, idea: he’ll have people’s email forwarded to him, then he’ll print out those emails and pop them into the regular mail. Tired of getting too much email and too much spam? No problem; with this new service you’ll never get any email! To sign up for our introductory price of $19.95 a month, just send your credit card number to scripter@microsoft.com.

Note. This is the spot where the Scripting Editor would usually note, “Please do not send your credit card number to scripter@microsoft.com.” Because the Scripting Editor is out of town, we’ll take care of that for her: “Please do not send your credit card number to scripter@microsoft.com.” Send cash instead.


How Can I Clear a List Box in an HTA?

Hey, Scripting Guy! How can I clear a list box in an HTA?

-- AK

Hey, AK. For those of you who aren’t familiar with the term, an HTA – among other things – is a way to use Internet Explorer to provide a graphical user interface for your scripts. (If that still doesn’t answer your questions, don’t despair. Instead, stay tuned for April 11th when we debut our new HTA Developers’ Center.) With an HTA (short for HTML Application) you can make your system administration scripts look – and run – like real applications, applications complete with radio buttons, text boxes, check boxes, and, yes, list boxes.

So suppose you do have a list box, one with a bunch of items (technically known as options) already in it. In other words, something that looks like this:

Suppose that you want to remove all of those options, leaving you with a nice, clean, empty list box. How can you do that?

To begin with, as far we know there’s no single command that can clear all the options in a list box. But that’s OK; even if we can’t clear all the options at once, we can clear the options one at a time using the RemoveNode method. All we have to do is iterate through the all the options in the list box and then delete each one. We can do that by adding a subroutine (in this case, a subroutine named ClearListbox) to our HTA:

Sub ClearListbox
    For Each objOption in AvailableOptions.Options
        objOption.RemoveNode
    Next
End Sub

Note. This subroutine assumes that the list box we want to clear is named AvailableOptions.

As you can see, we create a For Each loop that loops through all the Options in the list box named AvailableOptions. For each option we find (and, remember, the options correspond to the items in the list box), we call the RemoveNode method to remove that option. By the time the For Each loop is done, we will have found – and removed – every item in the list box.

The only other thing we need to do is add a button to our HTA; this button, when clicked, will call the ClearListbox subroutine. Here’s some HTML that will add a button like this to our HTA:

<input id=runbutton  class="button" type="button" value="Clear Listbox"
name="run_button"  onClick="ClearListbox"><p>

That’s all we have to do. If we run our revised HTA and click the Clear Listbox button, all the options in the list box will be removed, and our HTA will look like this:

Breath-taking, isn’t it?

If you’d like to try this out for yourself, here’s code that will bring up an HTA with a list box pre-populated with 8 items. Click Clear Listbox and all 8 items will be deleted. To use this sample code, copy it, paste it into Notepad, and then save it with a .hta file extension (for example, listbox.hta). Then just double-click the file in My Computer or Windows Explorer, and see what happens.

Here’s the HTA code:

<html>
<head>
<title>Clear a Listbox</title>
</head>

<SCRIPT Language="VBScript">

Sub ClearListbox
    For Each objOption in AvailableOptions.Options
        objOption.RemoveNode
    Next
End Sub

</SCRIPT>

<body bgcolor="buttonface">

<select size="8" name="AvailableOptions" style="width:400" >
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
<option value="4">Option 4</option>
<option value="1">Option 5</option>
<option value="2">Option 6</option>
<option value="3">Option 7</option>
<option value="4">Option 8</option>
</select>

<p>
<input id=runbutton  class="button" type="button" value="Clear Listbox"
name="run_button"  onClick="ClearListbox"><p>

</body>
</html>


How Can I Clear a Text Box in an HTA?

Hey, Scripting Guy! How can I clear a text box in an HTA?

-- CB

Hey, CB. You know, it’s been awhile since we’ve answered a question about HTAs (HTML Applications). And we could probably get away with that except for the fact that a month or so ago we debuted the HTA Developer’s Center, something which implied that we were going to try and do a better job of supporting people interested in writing HTAs. So have we done a better job of supporting people interested in writing HTAs? You know, that’s a very important question. Too bad we don’t have time to answer it….

But look on the bright side: today, at least, we are supporting people interested in writing HTAs. So let’s see if we can figure out how to programmatically clear a text box.

We’re assuming you have an HTA vaguely similar to this; in other words, you have a text box where users can enter data. You also have a button that makes it very easy for users to erase that data and start over again; all they have to do is click the Clear Text button and any text in the text box will be erased. In our simplified version the HTA looks like this:

There’s just one catch: how do we erase the text any time a user clicks the button?

Believe it or not that’s one of the easiest things you’ll ever do in scripting. In HTML each text box has a property named Value, which represents whatever happens to be in the text box at the moment. For example, in our sample HTA the textbox has the value This text box has text in it. If we want to get rid of that text all we have to do is set the Value property to nothing. Assuming our text box is named MyTextBox this line of code will do the trick:

MyTextBox.Value = ""

That’s all there is to it. If we wanted to change the text so it read “Please enter your name here:” we’d use this line of code:

MyTextBox.Value = "Please enter your name here:"

If you’d like to try this out on your own here’s the complete code for our sample HTA. Just copy the following into Notepad and then save the file with a .hta file extension:

<head>
<title>HTA Test</title>
<HTA:APPLICATION
     APPLICATIONNAME="HTA Test"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<script language="VBScript">
    Sub ClearText
       MyTextBox.Value = ""
    End Sub
</script>

<body>
    <input type="text" name="MyTextBox" size=40 value="This text box has text in it"><p>
    <input type="button" value="Clear Text" name="run_button" onClick="ClearText">
</body>

And before you ask, yes, this same approach can also be used to clear any text typed into a text area. That’s right, with today’s Internet special you get two questions answered for the price of one. Let’s see any other daily scripting-related question-and-answer column found on the Internet beat that deal.


How Can I Clear an HTA Window?

Hey, Scripting Guy! How can I clear an HTA window?

-- DF

Hey, DF. Well, to tell you the truth, if you’d ever seen at least one of the Scripting Houses you wouldn’t be asking us if we knew how to clear a window; it’s pretty obviously some of us don’t. (In our defense, though, if we keep the window clean then birds have a terrible habit of flying headlong into it. Which, of course, is also why we don’t vacuum the rug very often: we don’t want birds flying headlong into the rug, either.)

We’re actually going to answer your question by showing you two scripts: one script that clears the entire window and another that clears only part of the window. Why are we going to do that? Well, we can’t tell you that just yet; that would spoil the surprise, wouldn’t it?

Let’s start off with a simple HTA, one that features a button that, when clicked, clears the entire HTA window:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="Clear Window"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT LANGUAGE="VBScript">
    Sub ClearWindow
        document.body.InnerHTML = ""
    End Sub
</SCRIPT>

<body>
    <input id=runbutton type="button" value="Clear" onClick="ClearWindow">
</body>

You’re right: there isn’t much to this. We start off by specifying a title for our window, using this line of code:

<title>HTA Test</title>

We then have some optional lines of code that are used to configure various properties of the HTA itself. We won’t discuss those in any detail today; for more information you might check out the HTA Developers Center or the Scripting Week 3 webcast Going Beyond the Command Line with HTML Applications.

As you can see, the body of the HTA consists of a single element: a button labeled Clear:

<input id=runbutton  type="button" value="Clear" onClick="ClearWindow">

Click that button, and a subroutine named ClearWindow will fire up.

And what happens inside that subroutine? Not much; in fact, all we do is set the InnerHTML property of the document.body object to nothing (“”):

document.body.InnerHTML = ""

And that, in turn, clears the HTA window. You want to clear an HTA window? Then just set the document.body.InnerHTML property to an empty string.

But wait, don’t go yet: we need to emphasize that this really clears the HTA window. For example, here’s what our HTA looks like before we click the button:

And here’s what it looks like after we click the button:

The window’s been cleared all right: in fact, everything has been erased, including our Clear button. There will likely be times when you want everything – including your interface elements – to be erased. However, there will also be times when you just want certain items – say, data retrieved from a previous script – to be erased while other items (such as buttons) are left alone. So let’s show you a variation of this script, one that erases only a portion of the HTA window.

Note. Of course, you can always restore the user interface by pressing F5, thus refreshing the HTA window.

Here’s the revised code:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="Clear Window"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT LANGUAGE="VBScript">
    Sub ClearWindow
        DataArea.InnerHTML = ""
    End Sub
</SCRIPT>

<body>
    <input id=runbutton  type="button" value="Clear" onClick="ClearWindow">
    <p>
    <span id=DataArea>This is the data area.</span>
</body>

For the most part we’ve done two things here. First, we added a <span> tag and given this span the id DataArea. A span, as you probably know, is a named-area of the HTA that we can dynamically modify; for example, using a script we can add text (and HTML formatting) to the span.

Or, we can clear the span of all text and HTML formatting. That’s the second change we made to our HTA: we’ve rewritten the ClearWindow subroutine so that instead of clearing the entire window it clears only the DataArea span:

Sub ClearWindow
    DataArea.InnerHTML = ""
End Sub

Why did we do that? Well, here’s what our revised HTA looks like when it first loads:

As you can see, our span (located just below the button) features the words This is the data area.

Now, here’s what the HTA looks like after we click the button:

Ah, now you get it: we cleared the DataArea, but left the window unchanged. That means our UI elements (in this simple example, that’s just the Clear button) are still there and still available for use.

And, yes, that also means that no birds will fly headlong into this HTA, either. One less thing to worry about!


How Can I Create a Custom Dialog Box?

Hey, Scripting Guy! How can I create a custom dialog box that presents the user with two options, and then runs the appropriate subroutine based on the option they chose?

-- AM

Hey, AM. Well, today is another day, which is good; after all, yesterday was anything but a banner day here in the scripting world, at least not when it comes to important things like baseball and softball. Yesterday morning, for example, the Scripting Editor and her softball team were shellacked 14-2 in a game called after 4 innings due to the “mercy rule.” That was tough: the Scripting Editor is a tad bit competitive, so she wasn’t too pleased about getting beat. On the bright side, however, she did have a fantastic view of the game, particularly of all those ground balls that whizzed by her as she stood in the spot where a real second baseman would normally stand. Fortunately, she wasn’t hurt; she didn’t get close enough to any of those ground balls to actually get hit.

Note to the Scripting Editor. Just a little good-natured sports humor there. You know how much we all really love you.

Editor’s Note. The part about not getting close to those ground balls isn’t true; the Scripting Editor has the bruised shin to prove it. And the part about the good-natured sports humor, well … the Scripting Editor is well aware of what the rest of the Scripting team thinks of her. Rest assured the feeling is mutual.

Later that day the Scripting Son and his baseball team were shellacked in both ends of a doubleheader, 7-0 and 9-1. The Scripting Son didn’t have any ground balls whiz by him; unfortunately that’s about the only thing that went right for him and the team. (If you’re not a baseball fan we can tell you this much: getting a combined total of 5 hits in two games is not the recommended way to win a doubleheader.)

The net result? During the day the Scripting Editor was in a bad mood, so she didn’t have much to say and pretty much went the entire day without bugging the Scripting Guy who writes this column. That night, the Scripting Son was in a bad mood, so he didn’t have much to say and pretty much went the entire evening without bugging the Scripting Guy who writes this column.

You know, come to think of it, maybe it wasn’t such a bad day after all.

Of course, when things are at their darkest there’s only one recourse: you must put your faith in a higher power. You know, a higher power like a Scripting Guy who writes a column that explains how you can create a custom dialog box that presents a user with multiple options and then runs the appropriate subroutine based on the option they chose:

Set objExplorer = CreateObject("InternetExplorer.Application")

objExplorer.Navigate "file:///c:\scripts\test.htm"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 250
objExplorer.Visible = 1

Do While (objExplorer.Document.All.ButtonClicked.Value = "")
    Wscript.Sleep 250
Loop

strSelection = objExplorer.Document.All.ButtonClicked.Value

objExplorer.Quit

Wscript.Sleep 250

Select Case strSelection
    Case "Option 1"
        Wscript.Echo "You selected Option 1."
    Case "Option 2"
        Wscript.Echo "You selected Option 2."
    Case "Option 3"
        Wscript.Echo "You selected Option 3."
    Case "Cancelled"
        Wscript.Quit
End Select

We’ll explain how this script works in just a moment; before we do that, however, we have a few other things to talk about. First, neither VBScript nor Windows Script Host provides a built-in method for creating custom dialog boxes; you can kind of, sort of create custom message boxes, but even then you’re limited to using a finite set of buttons. Therefore, we need to cheat a little in order to do this: we need to create an HTML file that displays our options, and then have our script call this HTML file rather than call a custom dialog box. In fact, that’s what this line of code is for:

objExplorer.Navigate "file:///c:\scripts\test.htm"

But more on that in a minute. For now, let’s take a look at that HTML file (C:\Scripts\Test.htm):

<SCRIPT language="VBScript">
Sub OKClicked
If UserOption(0).Checked
Then
ButtonClicked.Value = "Option 1"
End If
If UserOption(1).Checked
Then
ButtonClicked.Value = "Option 2"
End If
If UserOption(2).Checked
Then
ButtonClicked.Value = "Option 3"
End If
End Sub

Sub CancelClicked
ButtonClicked.Value
= "Cancelled"
End Sub
</SCRIPT>

<BODY>
<input type="radio" name="UserOption" checked=True>Option 1<BR>
<input type="radio" name="UserOption">Option 2<BR>
<input type="radio" name="UserOption">Option 3<P>
<input type="Button" value="OK" onClick="OKClicked">
<input type="Button" value="Cancel" onClick="CancelClicked">
<input type="hidden" name="ButtonClicked">
</BODY>

What we’ve got here is an HTML file that displays three radio buttons; that’s what these HTML tags do:

<input type="radio" name="UserOption">Option 1<BR>
<input type="radio" name="UserOption">Option 2<BR>
<input type="radio" name="UserOption">Option 3<P>

In case you’re wondering, we decided to go with radio buttons for one simple reason: that gives us a little more flexibility (adding additional radio buttons makes for a cleaner user interface than adding a bunch of additional push buttons). Do you need just two options? That’s fine; simply delete the tag for the third radio button (the one labeled Option 3). Need four options? That’s also fine; just copy the tag for Option 3, paste it to the end of list, and change the label to Option 4. Pretty simple stuff when you get right down to it.

Note. OK, so if you haven’t done much work with HTML maybe this isn’t all that simple. In that case, you might want to check out the HTA Developers Center for an introduction to working with HTML.

In addition to our radio buttons we also have a pair of regular old buttons, one labeled OK and the other labeled Cancel:

<input type="Button" value="OK" onClick="OKClicked">
<input type="Button" value="Cancel" onClick="CancelClicked">

As you can see, if and when someone clicks the OK button we’re going to run a subroutine named OKClicked; if and when someone clicks the Cancel button we’re going to run a subroutine named CancelClicked. And don’t worry; we’ll discuss those two subroutines in just a second.

Before we can do that, however (and yes, there are a lot of twists and turns to today’s column, aren’t there?) we need to discuss this line of code:

<input type="hidden" name="ButtonClicked">

What we’re doing here is adding a hidden control to our HTML page: the control won’t be visible onscreen, but we can still do things like assign it a value. Why do we need a hidden control on our page? Well, technically we don’t; we could have used a different approach in solving this problem. However, using a hidden control makes this whole task much easier to carry out. We have a hidden control that has no value. As you’ll see, our script periodically checks the value of this hidden control. As soon as a value does get assigned to the control the script knows that we’ve made a selection in our “dialog box” and then goes ahead and takes action based on that selection.

Which, again, is something we’ll explain in just a moment.

Let’s assume that a user has started your script, something which causes the “dialog box” to appear onscreen. He or she selects one of the radio button options and then clicks OK. What happens at that point?

Well, as we noted, clicking the OK button causes the OKClicked subroutine to run. Inside that subroutine we simply execute a series of If Then statements to determine which radio button was selected (or, in HTML parlance, which button was checked):

If UserOption(0).Checked Then
    ButtonClicked.Value = "Option 1"
End If

If you’re wondering how this works, all our radio buttons have the same name (UserOption); that’s how we ensure that the buttons work together as a group. In turn, each individual button in a group is automatically assigned an index number: the first button in the group is button 0, the second button is button 1, and so on. All we’re doing here is saying this: If the first button is checked then assign Option 1 to the value of our hidden control (ButtonClicked). We then repeat this same bit of code for each possible selection.

What if the user clicks the Cancel button? Well, in that case we run a different subroutine (CancelClicked) and assign the value Cancelled to the hidden control

Sub CancelClicked
    ButtonClicked.Value = "Cancelled"
End Sub

And that’s basically it for the HTML file. Now – finally! – let’s turn our attention to the script, which starts out by creating an instance of the InternetExplorer.Application object:

Set objExplorer = CreateObject("InternetExplorer.Application")

After we’ve created an instance of Internet Explorer we then use the Navigate method to open the file C:\Scripts\Test.htm; we configure the size of the Internet Explorer window (hiding the tool bar and status bar while we’re at it); then we make the whole thing appear on screen by setting the Visible property to 1:

objExplorer.Navigate "file:///c:\scripts\test.htm"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 250
objExplorer.Visible = 1

At this point we start doing some real work. To begin with, we have to make sure the script pauses until the user makes a selection in the HTML file. With that in mind we use this line of code to periodically check the value of the ButtonClicked control:

Do While (objExplorer.Document.All.ButtonClicked.Value = "")
    Wscript.Sleep 250
Loop

If ButtonClicked has no value (that is, if the value equals “”, an empty string) we simply pause the script for 250 milliseconds and then try again. This continues until the user finally clicks either the OK or the Cancel button.

Note. What if the user simply closes the Internet Explorer window? That’s going to generate an error and the script will terminate. If you’d prefer that the script come to a more graceful termination you could include some error handling to check for and respond to that condition. But that’s more than we wanted to do today.

As soon as the value of the ButtonClicked control has changed we store that value in a variable named strSelection, then call the Quit method to dismiss our instance of Internet Explorer:

strSelection = objExplorer.Document.All.ButtonClicked.Value

objExplorer.Quit

From there we pause for 250 milliseconds (giving Internet Explorer a chance to close), then use the following Select Case block to take action based on the user’s selection:

Select Case strSelection
    Case "Option 1"
        Wscript.Echo "You selected Option 1."
    Case "Option 2"
        Wscript.Echo "You selected Option 2."
    Case "Option 3"
        Wscript.Echo "You selected Option 3."
    Case "Cancelled"
        Wscript.Quit
End Select

Obviously these aren’t particularly exciting actions, but, needless to say, you can replace our code with pretty much anything you want.

And there you have it, AM. It’s a bit of a workaround and it’s not quite as elegant as creating a true custom dialog box. But it works.

For now, though, it’s time to head to Stanwood High School and another Scripting Son doubleheader. Will it be another quiet night at the Scripting Home? We’ll soon find out.

And no, we won’t discuss the Scripting Editor’s team and its chances of redeeming itself. After all, even a higher power is helpless in the face of some things.


How Can I Create an HTA that Doesn’t Have a Close Button in the Title Bar?

Hey, Scripting Guy! How can I create an HTA that doesn’t have a Close button in the title bar?

-- AK

Hey, AK. You know, you’ve presented us with a great moral and ethical quandary. After all, suppose we answer this question: suddenly the world might be inundated with HTAs that couldn’t be closed. We have this vision of unclosable, unstoppable HTAs sweeping unchecked throughout the land, gobbling up any and all system resource in their paths. And we, the Scripting Guys, would be responsible for that. We just don’t think we could live with such a thing on our consciences.

But, then again, what difference will it make to have one more thing on our consciences? With that in mind, here’s an HTA that can’t be closed; the only way to get rid of it is to terminate the Mshta.exe process (or have the Army Air Corps try to shoot it down off the top of the Empire State Building):

<html>
<head>
    <title>Test</title>
</head>

<HTA:APPLICATION
    SysMenu="no"
>

<body>
    <p>This HTA has no Close button.</p>
</body>
</html>

And when you run the thing, you should see something that looks like this:

So how did we get rid of the Close button (not to mention the Minimize and Maximize buttons) yet still keep the window title bar? That’s easy: all we did was set the value of the SysMenu property to no:

<HTA:APPLICATION
    SysMenu="no"
>

That’s it: setting the value of SysMenu to no (which must be done inside the HTA:APPLICATION tag) will get rid of the Close button, the Minimize and Maximize buttons, and the system menu. You can still move the window around (just click on the title bar and drag), but there’s no way to close the window.

And, yes, our consciences are bothering us a little. (Although that’s probably because we drank the last of the coffee but were too lazy to make a new pot.) Here’s a revised HTA that doesn’t have a Close button in the title bar; however, it does have a button in the body of the HTA labeled Exit. Click that button, and a subroutine named ExitProgram will close the HTA window for you:

<html>
<head>
    <title>Test</title>
</head>

<HTA:APPLICATION
    SysMenu="no"
>

<SCRIPT LANGUAGE="VBScript">
    Sub ExitProgram
        window.close()
    End Sub
</SCRIPT>

<body>
    <input id=runbutton type="button" value="Exit" onClick="ExitProgram">
</body>
</html>

In this case, we end up with an HTA that looks like this:

All things considered, it’s probably best to have some way to gracefully dismiss the HTA. But that’s up to you. And your conscience.

Note. If you’ve read through this entire column and are still thinking, “HT-whats?” then you might want to check out the information in the HTA Developers Center, or – even better – view the Scripting Week 3 webcast on creating HTAs.


How Can I Determine Which Text has been Selected in an HTA?

Hey, Scripting Guy! How can I determine which text has been selected in an HTA?

-- DO

Hey, DO. You know, most people think that writing the Hey, Scripting Guy! column every day is one of the most glamorous and exciting jobs in the world. And it probably would be if it wasn’t for the fact that the Scripting Guy who writes this column is so lazy. For example, lots of times he selects questions that he knows can be scripted. He usually doesn’t have a script on-hand that carries out the task in question, but he knows that it’s possible to write such a script. That’s an efficient way to do things, but not a particularly exciting way to do things.

This question was a little different, however. How can you determine which text has been selected in an HTA? Heck, we weren’t even sure if you can determine which text has been selected in an HTA. But it turns out that you can:

<html>
<head>
    <title>HTA Test</title>
</head>

<SCRIPT LANGUAGE="VBScript">
    Sub ShowSelection
        Set objSelection = Document.Selection.CreateRange()
        Msgbox objSelection.text
    End Sub
</SCRIPT>

<body>
     <textarea name="ScriptArea" rows=5 cols=40></textarea><p>
     <input id=runbutton  type="button" value="Show Selection" onClick="ShowSelection">
</body>

What we have here is a simple little HTA that looks something like this:

What’s that? How could you not be impressed? Well, try this. Type some text in the text area and then highlight a portion of that text:

Now click the button labeled Show Selection. With any luck at all you’ll get a message box telling you which text has been selected:

You’re right: that’s a bit more like it.

So how does this all work? Well, we start off by creating an HTA that includes a text area and a button. We won’t discuss the details behind creating the HTA itself; if you need a little background information check out the HTA Developers Center or our Scripting Week 3 webcast on HTAs . Today we’re just going to focus on the subroutine that runs when you click the button:

Sub ShowSelection
    Set objSelection = Document.Selection.CreateRange()
    Msgbox objSelection.Text
End Sub

That’s right: just two lines of code. First we create a new TextRange object; that’s done by calling the CreateRange() method, which happens to be part of the Document.Selection object:

Set objSelection = Document.Selection.CreateRange()

As it turns out, one of the properties of the TextRange object is the Text property. As the name implies, this property returns the text that happens to be found in this particular TextRange. To return the text currently selected in the HTA all we have to do is display the value of the Text property:

Msgbox objSelection.Text

Incidentally, you aren’t limited to selecting text in a text box or a text area. Select any text within this HTA and see what happens:

<html>
<head>
    <title>HTA Test</title>
</head>

<SCRIPT LANGUAGE="VBScript">
    Sub ShowSelection
        Set objSelection = Document.Selection.CreateRange()
        Msgbox objSelection.text
    End Sub
</SCRIPT>

<body>
    <p> Here is some text in an HTA. Select any portion of the text and then
        click the Show Selection button.</p>
  <input id=runbutton  type="button" value="Show Selection" onClick="ShowSelection">

</body>

Pretty cool, huh? You know, maybe this is the most glamorous and most exciting job in the world.

Nah ….


How Can I Display a Default Message If Nothing Has Been Typed Into a Text Box?

Hey, Scripting Guy! Sometimes you go to a Web page and you’ll see a text box that includes some instructions, something like “Please type a description in this box.” When you click on the text box, those instructions disappear; however, if you click on the box, don’t type in a description and then click somewhere else, then the instructions reappear. How can I do that same thing in an HTA?

-- AJ

Sorry; the Scripting Guy who writes this column is a little out of breath this morning; that’s because he was in a race around 6:30 AM, and hasn’t fully recovered. And in case you’re wondering, that’s a race he wasn’t expecting to be in, a race he didn’t realize he even was in, and a race in which he was soundly and decisively defeated.

Sometime in the past month or so the Scripting Guys began starting off each day by eating a ceremonial doughnut. The ritual works like this. Each morning, on his way to work, the Scripting Guy who writes this column stops at the store and buys doughnuts. And each morning the remaining team members eat those doughnuts. Do any of these remaining team members ever buy doughnuts? Do any of these remaining team members at least help pay for these doughnuts? Let’s put it this way: do any of these remaining team members ever do anything?

Note. Yes, this is exactly like the story of the Little Red Hen. “Who will help me buy these doughnuts?” says the Little Red Hen who writes this column. No answer. “Who will help me pay for these doughnuts?” says the Little Red Hen who writes this column. “Now, who will help me eat these doughnuts?” says the Little Red Hen who writes this column.

You can figure out the rest on your own.

At any rate, this morning the Scripting Guy who writes this column was grabbing a couple of doughnuts out of the doughnut case when he noticed an older woman taking a muffin out of the muffin case. (He noticed her because, for some reason, it’s very unusual for him to ever see anyone who’s older than he is. That hasn’t always been the case, but over the past few years something seems to have changed.)

Having grabbed his two doughnuts the Scripting Guy who writes this column began making his way towards the only open cash register. As he did so, he noticed that the muffin lady was scurrying to catch up to and then pass him. Unfortunately, she wasn’t particularly fast, and the Scripting Guy who writes this column was able to keep pace with her, even though he was moseying along in his typical fashion. (In his defense, however, he was on his way to work; it goes without saying that he was in no hurry whatsoever.)

As they approached the checkout stand, the lady took a desperate lunge and cut in front of the Scripting Guy who writes this column, a Scripting Guy who had to stop abruptly to make sure he didn’t crash into the woman. The muffin lady paid for her purchase and left. The Scripting Guy who writes this column then did the same thing, calculating that the muffin lady had saved nearly 30 seconds by getting to the checkout stand ahead of him.

And yes, that means that the Scripting Guy who writes this column could have saved 30 seconds of his own valuable time by forcing the issue and trying to beat the muffin lady to the checkout stand. But the Scripting Guy who writes this column lives such an exciting, fun-filled life that he decided he could afford to waste 30 seconds. And so he did.

Of course, that doesn’t mean that the Scripting Guy who writes this column can always afford to waste 30 seconds out of his life. After all, that might not leave time for his other daily rituals, like the one in which he tells people how to create a text box that displays a default message, causes that message to disappear when the text box gets the focus, then causes that message to reappear if the user doesn’t end up entering a new value in the box.

Not sure what that actually means? Then copy the following code, paste it into Notepad, save the resulting file as an HTML Application (HTA), and then give it a try for yourself:

<SCRIPT LANGUAGE="VBScript">

Sub TextBoxEnter
    If TextBox1.Value = "Please enter a value here." Then
        TextBox1.Value = ""
    End If
End Sub

Sub TextBoxLeave
    If TextBox1.Value = "" Then
        TextBox1.Value = "Please enter a value here."
    End If
End Sub

</SCRIPT>

<body>
    <input type="text" name="TextBox1" size="50" value="Please enter a value here."
     onFocus = "TextBoxEnter" onFocusOut="TextBoxLeave"><p>
    <input type="text" name="TextBox2" size="50">
</body>

Good idea: let’s take a closer look at the code and see if we can figure out how this HTA works. To begin with, it’s a very simple little HTA; for one thing, the body consists of just two controls (a pair of text boxes). One text box – which we named TextBox2 – is just a plain old text box; that’s why the HTML tagging is so concise:

<input type="text" name="TextBox2" size="50">

The other text box, which we named TextBox1 (and no, we didn’t hire a consultant to help us; we came up with these names all by ourselves) is a little fancier:

<input type="text" name="TextBox1"
size="50" value="Please enter a value here."
onFocus = "TextBoxEnter" onFocusOut="TextBoxLeave">

So what is all that fancy stuff? For starters, we need to have a default value assigned to this text box; when you first open the HTA we want the text box to display the message Please enter a value here. That’s why we added the value parameter, like so:

value="Please enter a value here."

In addition to having a default value, we also need this text box to respond to two different events: onFocus, an event triggered any time someone clicks onto or otherwise selects the text box; and onFocusOut, an event triggered any time someone clicks out of or otherwise moves the focus away from the text box. When either of those events occur we need to run the appropriate subroutine; each time the text box gets the focus we want to run a subroutine named TextBoxEnter, and each time the text box loses the focus we want to run a subroutine named TextBoxLeave.

Note. OK, for those subroutine names we did have to hire a high-priced consulting firm. But usually we come up with our own names for everything.

Which explains how we ended up with family members and pets that have names like the Scripting Son and the Scripting Dog.

As you might expect, all the fun stuff occurs inside those two subroutines. Let’s start out by looking at the subroutine that runs any time the text box gains the focus:

Sub TextBoxEnter
If TextBox1.Value = "Please enter a value here."
Then
TextBox1.Value = ""
End If
End Sub

By default, our text box displays the message Please enter a value here. That’s fine; after all, that’s what we want the text box to display. However, we don’t want to always display that message; if someone clicks inside the text box we’d like that message to disappear. (Otherwise they’d have to select the message, delete it, and then start typing in a value.) So how do we make the message disappear? That’s easy; we simply set the value of the text box to an empty string:

TextBox1.Value = ""

Note, however, that we don’t set the value of the text box to an empty string any time that control gets the focus; instead, we “reset” the text box only if the message “Please enter a value here.” happens to be displayed. Why do we do that? Well, suppose someone enters a value like the following into the text box:

Pharmaceutical Research Division, Fabrikam Corp

As you can see, the period was left off the end; the value typed into the box should have really been this:

Pharmaceutical Research Division, Fabrikam Corp.

But that’s no big deal; after all, the user can just click at the end of the text box and then type in the missing period. Or, to be a little more precise, they can do that as long as we don’t automatically delete the value of the text box each time the text box regains the focus. That’s why we don’t automatically reset the value of the text box; we do that only if the default value (“Please enter a value here.”) is displayed. Otherwise any time someone clicked in the text box we’d erase everything they’d previously typed into the box. That might be OK. But, then again, it might not be.

Now, what happens when the text box loses the focus? This is what happens:

Sub TextBoxLeave
    If TextBox1.Value = "" Then
        TextBox1.Value = "Please enter a value here."
    End If
End Sub

In effect, our onFocusOut subroutine is a mirror image of the onFocus subroutine. This time around, we check to see if the value of TextBox1 is equal to an empty string. If it is, that means that nothing at all has been typed into the text box. As a result, we set the value of the box to the default message: Please enter a value here. That’s ensures that we never have a truly empty text box; if the box is empty we redisplay the default message. And what do we do if something other than the default value has been typed into the text box? Nothing; as long as the text box has something in it we simply leave the value alone. Combined, these two subroutines should provide the very behavior you were looking for.

Now, to tell you the truth, the Scripting Guy who writes this column wasn’t all that distraught about the muffin lady having beaten him to the checkout stand; in fact, he really didn’t pay much attention to it. Apparently, however, the muffin lady did give it some thought; as the Scripting Guy who writes this column left the store the muffin lady was waiting for him. “I’m sorry I raced in front of you,” she said. “That was very rude of me. I should have let you go ahead of me.”

She should have let him get ahead of her? You know, if you’ve ever wondered how far the Scripting Guy who writes this column has fallen that should answer your question: little old ladies are now apologizing for being too fast and not letting him get ahead of them.

Gosh, if little old ladies are passing him by that means that the Scripting Editor might soon be passing him by as well. Now there’s a scary thought ….


How Can I Dynamically Add Controls to an HTA?

Hey, Scripting Guy! How can I dynamically add controls to an HTA?

-- TR

Hey, TR. Dum-de-dum-dum. Dum-de-dum-de – oh, wait : you were talking to us, weren’t you? Sorry; we saw the word “dynamically” and figured this question must have been addressed to someone else. When people hear the words Scripting Guys they think of all sorts of things, but “dynamic” usually isn’t one of them.

And no, TechNet won’t allow us to print the words people do associate with the Scripting Guys.

OK, now that we know that we’re supposed to answer the question we better get down to business. First of all, we should note that the real answer to your question is, “It depends.” After all, the exact method for dynamically adding controls to an HTA depends on what type of control you want to add and what you want those controls to do. (For example, do you want a script to run when the control is clicked?) Therefore, we’re going to show you a somewhat-generic method for adding dynamic controls to an HTA and for determining whether or not that control has been clicked. That probably won’t solve your actual problem, but, with any luck, it should be enough information to get you started.

We should also note that if you have no idea what we’re talking about, HTA is short for HTML Application. Among other things, HTAs utilize the Internet Explorer object model as a way to provide a graphical user interface for a script; that means you can create a script that looks like a real application, complete with radio buttons and drop-down lists and what-have-you. For more information, check out our HTA Developers Center.

And now, back to our regularly-scheduled program.

Our sample HTA is pretty simple. It consists of a window with a single button, labeled Add Button. Each time you click Add Button, a new button is added to the screen; if you click any of those new buttons, a subroutine will report back the ID of the button that was clicked. When you first start up the HTA it looks something like this:

No, it’s not very fancy at the moment. But don’t worry: we’re not done yet.

As you can see, the code for our HTA isn’t much fancier:

<script Language="VBScript">

    intButtonCount = 1

    Sub AddButton
        strHTML = DataArea.InnerHTML
        strHTML = strHTML & "<input id=" & intButtonCount & " type=" & Chr(34) & "button" & Chr(34) & _
            " value= " & Chr(34) & "Button " & intButtonCount & Chr(34) & " onClick=" & Chr(34) & _
                "NewButtonSubroutine" & Chr(34) & ">"
        DataArea.InnerHTML = strHTML
        intButtonCount = intButtonCount + 1
    End Sub

    Sub NewButtonSubroutine
        Set objButton = window.event.srcelement
        strID = objButton.id
        Msgbox "You clicked Button ID " & strID
    End Sub

</script>

<body>
    <input id=addbutton type="button" value="Add Button" onClick="AddButton">
    <span id=DataArea></span>
</body>

So how exactly does this all work? Let’s break the HTA down into subsections and see if we can answer that question. For starters, take a look at the <body> tag. As you can see, there isn’t much there; all we have is the HTML code for adding a button, along with this tag:

<span id=DataArea></span>

That’s not much, but it’s important. The <span> tag sets aside a named area of our HTA; as you’re about to see, once we have a named area we can then dynamically add items to that area. That’s step 1 in the process: in order to add new controls to an HTA you need a designated area within the HTA where controls can be added. Make sense? Good.

Oh: note that we’ve given this spot the ID DataArea. We’ll need to use that ID any time we refer to this portion of the HTA.

Next, let’s take a look at the AddButton subroutine; this is the subroutine that gets called each time we click Add Button. As you might expect, this is also the subroutine that dynamically adds controls each time the button is clicked (and thus each time the subroutine is called).

The first thing we do inside this subroutine is assign the value of DataArea’s InnerHTML property to a variable named strHTML:

strHTML = DataArea.InnerHTML

The InnerHTML property represents all the HTML coding contained within the DataArea span. For all intents and purposes, that means information about all the controls, text, images, etc. within that span is now stashed away in the variable strHTML.

That brings us to this nightmarish line of code:

strHTML = strHTML & "<input
id=" & intButtonCount & " type=" & Chr(34) &
"button" & Chr(34) & _
" value= " & Chr(34) & "Button "
& intButtonCount & Chr(34) & " onClick=" & Chr(34)
& _
"NewButtonSubroutine"
& Chr(34) & ">"

Yes, we know what it looks like. But let’s explain what’s going on here and see if that helps any. All we’re doing is assigning a new value to strHTML: this time it’s getting the existing value of strHTML (all the current controls, text, images, etc.) plus HTML code for adding a brand-new button. The portion of the code for adding a brand-new button looks like this:

"<input id=" &
intButtonCount & " type=" & Chr(34) & "button"
& Chr(34) & _
" value= " & Chr(34) & "Button "
& intButtonCount & Chr(34) & " onClick=" & Chr(34)
& _
"NewButtonSubroutine"
& Chr(34) & ">"

Believe it or not, that gobbledygook resolves to the code required to add a new button to the HTA. As you probably know, the HTA tag for a button looks like this:

<input id=1 type="button" value="Button 1" onClick="NewButtonSubroutine">

Those elements – and only those elements – are present in our crazy line of code. However, we did have to make a few modifications:

· The HTA tag for a button includes a number of double quotes (e.g., type=”button”). In VBScript, all string values must be enclosed in double quotes, and putting double quotes within double quotes can be tricky. Therefore, we simply replaced the double quotes with their ASCII equivalent: Chr(34). See all the Chr(34) notations in our code? Those are just double quotes. If you see “type=” & Chr(34) & “button” you can simply read that as type=”button” (which is exactly how the script reads it).

· If we want to be able to distinguish between buttons then each button needs to have a unique ID. To do that we defined a global variable named intButtonCount at the beginning of our <script> tag, and assigned intButtonCount the value 1. Each time we add a new button we then increment intButtonCount by 1. What does that mean? That means our first new button will have an ID of 1, our second new button will have an ID of 2, etc. That’s why the button ID is assigned the value of intButtonCount. Like so: id=" & intButtonCount.

· We also want each button to have unique label. Therefore, we set the value (label) of each button to the word Button plus the button ID. That’s what happens here: " value= " & Chr(34) & "Button " & intButtonCount & Chr(34).

Got all that? If not, don’t fret. Instead, just slowly walk through the line of code piece-by-piece, substituting the current value of intButtonCount as needed, and sticking in double quote marks any time you see Chr(34). Sooner or later it should start making sense. If it doesn’t substitute in this line of code, which adds a button without an ID, a label, or an onClick event:

strHTML = strHTML & "<input type=" & Chr(34) & "button" & Chr(34) & ">"

That button won’t look like much and it won’t actually do anything, but once you understand how we created that very simple button you can start adding additional parameters such as value, id, and onClick.

After we’ve assigned a new value to strHTML we then set DataArea’s InnerHTML property to that new value:

DataArea.InnerHTML = strHTML

The net effect? We’ll have a new, dynamically-created button within our HTA. Click Add Button a few more times and you’ll have all sorts of new buttons on the HTA:

Etc.

Now, what about being able to distinguish between buttons 1, 2, and 3? Well, each button we add is configured to call the same subroutine (NewButtonSubroutine) any time someone clicks the button. That subroutine looks like this:

Sub NewButtonSubroutine
    Set objButton = window.event.srcelement
    strID = objButton.id
    Msgbox "You clicked Button ID " & strID
End Sub

Inside the subroutine we use the first line of code to create an object reference to the window.event.srcelement object. This object represents the item that fired the event triggering the subroutine. In other words, if we click button 2, then the window.event.srcelement is equal to button 2.

As soon as we have an object reference we can assign the button ID to a variable named strID; after that we can use the Msgbox function to echo back the ID of the button that was clicked:

Not very fancy, but you get the idea.

As we noted, this probably doesn’t fully answer your question, TR, but we hope it gets you headed down the right path. If you need more specific information, just let us know.

And remember: if you want to make sure we see your email, don’t use words like dynamic. Instead, use a subject line like Hey, You Lousy, Good-for-Nothing Bums. You can bet that email will get to us, even if it’s not specifically addressed to us!


How Can I Dynamically Change the Background Color of an HTA?

Hey, Scripting Guy! How can I dynamically change the background color of an HTA?

-- CN

Hey, CN. You know, last summer (Fourth of July, 2005) an errant firework shot by a neighbor accidentally caught one of the Scripting Guys’ garage on fire. (Because it was shot by a neighbor we assume it was an accident. Had it been shot by one of the other Scripting Guys, well ….) Although no one was hurt, the garage was totaled. Forced to rebuild said garage from scratch, the Scripting Family decided to turn it into a family room. (Admittedly, they don’t like having to park the Rolls Royce outside instead of in the garage. But hey, if the Rolls gets wet they’ll just buy a new one.)

We won’t bother recapping the saga of the Contractor Who Couldn’t Be Bothered to Contract; suffice to say that the job that would be done “in three weeks, guaranteed” took six months. After six months of lethargy, punctuated by an occasional hour or two of work, the very last thing that needed to be done was to paint the interior. “What color would you like us to paint it?” asked the contractor. “And remember, we have to paint everything the same; the insurance company will only pay for one color.”

“In that case we’ll take white,” replied the Scripting Guy.

“What shade of white?” asked the contractor.

“Um, I don’t know,” said the Scripting Guy. “Just plain old white. Any shade of white will be fine.”

A couple weeks later the Scripting Guy came home from work to discover that – miracle of miracles – the painter (Big Larry) had actually come out and painted the family room. That was the good news. The bad news: he painted the entire room – ceiling, walls, door frames, you name it – yellow.

Not being much of an artist, the Scripting Guy had had no idea that yellow was a shade of white. Which, apparently, it is.

The moral of the story: a color-changing script would have come in pretty handy right about then. Unfortunately, the Scripting Guy didn’t have a color-changing script that would work on a family room; all he had was a color-changing script that would work on an HTML Application (HTA):

<SCRIPT LANGUAGE="VBScript">
    Sub TestSub
        document.body.bgColor="red"
    End Sub
</SCRIPT>

<body>
    <input id=runbutton type="button" value="Run Script" name="run_button" onClick="TestSub">
</body>

As you can see, there really isn’t much to this HTA. The user interface, such as it is, consists solely of a button labeled Run Script; when you click that button, it triggers a subroutine named TestSub:

<input id=runbutton type="button" value="Run Script" name="run_button" onClick="TestSub">

And what does this subroutine do? Why, it changes the background color of the HTA to red:

Sub TestSub
    document.body.bgColor="red"
End Sub

Pretty simple, huh? All we’re doing is grabbing the document.body object and then setting the value of the bgColor property to red. Would you rather set the background color to aquamarine? No problem:

document.body.bgColor="aquamarine"

How about lavenderblush (you know, you can never go wrong with lavenderblush):

document.body.bgColor="lavenderblush"

You can even – alas – set the background color to yellow:

document.body.bgColor="yellow"

Note. Looking for a list of all the color values you can pass, by name, to the bgColor property? Look no further than the HTML and DHTML Reference on MSDN.

Incidentally, CN, you noted in your email that you tried changing the background color by modifying the InnerHTML property of the HTA body; however, you weren’t able to get that to work. Why not? To answer that question, let’s take a look at a simple little <body> tag, one that sets the background color to red:

<body bgColor="red">
    <b>This is the body of the document.<b>
</body>

As you can see, you specify the background color as part of the <body> tag itself. Is that important? You bet it is: that’s because the InnerHTML property only applies (in this case) to the stuff that comes between the beginning and ending <body> tags. Our beginning tag is this:

<body bgColor="red">

Our ending tag is this:

</body>

That means that the InnerHTML property will affect only this portion of the HTA:

<b>This is the body of the document.<b>

Using the InnerHTML property we can add buttons and check boxes and images and do all sorts of fun things. However, we can’t change the background color, assign a background picture, or do anything else to the <body> tag itself. We can do that only by directly accessing the properties of the document.body object. That’s why modifying the InnerHTML property didn’t do anything to change the background color.

Note. As a matter of fact, there is a property named OuterHTML, a property that can be used to completely replace an HTML object, including the starting and ending tags. However, the document.body object doesn’t support the OuterHTML property.

As long as we’re on the subject, here’s a nifty little HTA. We won’t explain the code today, but when you click the button in this HTA a color picker dialog box appears on screen. Select a color, click OK, and the background color of the HTA will change to the color you selected in the dialog box (really; give it a try and see for yourself). Here’s the HTA code:

<SCRIPT LANGUAGE="VBScript">
    Sub TestSub
        intColor=dlgHelper.ChooseColorDlg
        If intColor <> 0 then
            document.body.bgColor=intColor
        End If
    End Sub
</SCRIPT>

<body>
    <OBJECT id=dlgHelper CLASSID="clsid:3050f819-98b5-11cf-bb82-00aa00bdce0b" width="0px" height="0px"></OBJECT>
    <input id=runbutton type="button" value="Run Script" name="run_button" onClick="TestSub">
</body>

And here’s where you can find more information on the Dialog Helper object that makes this all possible.

As for the Scripting Guy’s family room, he didn’t bother to tell the contractor that yellow wasn’t actually a shade of white; having taken six months to finally get the job done he had no desire to wait another six months for Big Larry to get around to repainting the room. Instead, the Scripting Guy left part of the room yellow, painted a couple walls a reddish-color, and painted the trim and a few odds-and-ends white (the real white). Surprisingly enough, it turned out really nice, which means that this story has a happy ending.

Or at least as happy an ending as you can get when you’re dealing with contractors and insurance jobs.

More Information. For more on creating HTML Applications, see the HTA Developers Center.


How Can I Dynamically Hide and Show a Control in an HTA?

Hey, Scripting Guy! How can I dynamically hide and show a control in an HTA?

-- R L-K

Hey, R L-K. To tell you the truth, we’re a little distracted today. Admittedly, we’re a little distracted pretty much every day. However, today is different: today the Scripting Guy who writes this column in sitting on a powder keg that threatens to destroy the Scripting Guys forever!

We’ve probably never mentioned this before, but the Scripting Guy who writes this column has a son who’s wrapping up his junior year in high school. (From here on we’ll call him … let’s see .. how about “The Scripting Son”?) In the past year he’s gotten a steady stream of letters from colleges and universities from around the world, all of them encouraging him to explore the wonderful educational opportunities they have to offer. Drexel University, Pepperdine, Linfield College, Old Dominion: you name it, and there’s a good chance he’s heard from them.

Now that was all well and good, at least until yesterday, when he received a letter from – cue the ominous music – the University of Cambridge in Cambridge, England!

Interesting note. In the year 2009 the University of Cambridge will be celebrating its 800th anniversary. Although this has not been confirmed, we have reason to believe that Scripting Guy Peter Costantini – who was a member of the first graduating class at Cambridge – will be asked to deliver the commencement address.

So what’s wrong with the University of Cambridge? Nothing. Well, except for this: Cambridge and Oxford University are bitter rivals. And that’s a problem, because Scripting Guy Dean Tsaltas is currently pursuing a Master’s degree in computer science at Oxford. If the Scripting Son enrolls at Cambridge while Dean is still enrolled at Oxford, well, we shudder to think of what might happen.

Note. If you’re thinking, “Oxford University? Never heard of it; is that even an accredited college?” then you must work for the Microsoft department that initially turned down Dean’s request for tuition reimbursement, not believing that there is such a place as Oxford University.

You can just imagine the pressure we’re under; the tension is so thick in Scripting Guys headquarters that we could cut it with a knife. (Or we could if they’d actually allow us to have knives.) But still, duty calls, and as a graduate of the University of Washington (go Huskies!) the Scripting Guy who writes this column knows that, when the going gets tough, the tough show you how to dynamically hide and show a control in an HTA:

<SCRIPT Language="VBScript">
Sub ToggleButton
If HereThere.style.visibility="hidden" Then
HereThere.style.visibility="visible"
Else
HereThere.style.visibility="hidden"
End If
End Sub
</SCRIPT>

<input type="button"
value="Button" name="HereThere"><P>
<input type="button"
value="Click to Hide/Show Button" onClick="ToggleButton">

What we have here is a very simple little HTA. (Yes, yes, we know: at either Oxford or Cambridge they’d create super-complicated HTAs. But remember, the Scripting Guy who writes this column graduated from an American university.) The body of our HTA contains two buttons. One (cleverly labeled Button) is the control we’re going to hide and show; the other (labeled Click to Hide/Show Button) is the button we’ll click any time we want to hide or show the other button. The HTML tagging for the two buttons looks like this:

<input type="button" value="Button"
name="HereThere"><P>
<input type="button" value="Click to Hide/Show Button" onClick="ToggleButton">

And the HTA itself looks like this:

Note. Can you copy the preceding graphic and use it as your desktop wallpaper? Sure; help yourself.

That’s it for the body of the HTA. The only other piece to this scripting puzzle is a subroutine named ToggleButton. Each time we click the Click to Hide/Show Button button we call this subroutine; as you probably have already figured out, that’s what the onClick="ToggleButton" parameter is for:

<input type="button" value="Click to Hide/Show Button" onClick="ToggleButton">

So what does this subroutine do? Nothing. Nothing at all.

Oh, wait; that’s not true. What it actually does is first check to see if the Button button (which we gave the name HereThere) is visible on screen; that’s done by determining whether the Visibility attribute is set to hidden:

If HereThere.style.visibility="hidden" Then

If the Visibility attribute is hidden that means that the button is hidden; therefore we execute this line of code, which changes the value of the Visibility attribute to visible and, not coincidentally, causes the button to appear on screen:

HereThere.style.visibility="visible"

What if the Visibility attribute isn’t equal to hidden? In that case the button must be visible on screen; therefore we change the value of the Visibility attribute and, in turn, hide the button:

HereThere.style.visibility="hidden"

Will that really cause the button to disappear from our HTA window? See for yourself:

But don’t worry; it’s not gone for good. Just click Click to Hide/Show Button and the button will come back. You could keep clicking away all day long, repeatedly hiding and showing the other button. And we’re not just saying that; that’s pretty much how we’ve spent our morning.

Of course, you’re not limited to doing this only with buttons. You can also hide (and show) other controls. The one tricky part: unlike buttons, the labels of other controls might not be anchored to the control. Thus you might hide, say, a checkbox yet still have the label left onscreen. Likewise, you might have multiple controls you need to show or hide. In that case, your code could get a bit long, and a tad bit complicated.

Unless, of course, you put all of those controls in a <SPAN> and then hide and show the <SPAN>:

<SCRIPT Language="VBScript">
Sub ToggleButton
If HereThere.style.visibility="hidden"
Then
HereThere.style.visibility="visible"
Else
HereThere.style.visibility="hidden"
End If
End Sub
</SCRIPT>

<span id=HereThere>
<INPUT type="checkbox"
ID="box1">Option 1
<INPUT type="checkbox"
ID="box2">Option 2
<INPUT type="checkbox"
ID="box3">Option 3
<INPUT type="checkbox"
ID="box4">Option 4
</span>
<p>
<input type="button" value="Click to Hide/Show Button" onClick="ToggleButton">

Give that a try and you’ll see what we mean.

As for the Scripting Guys, now that we think about it this whole Oxford-Cambridge thing is probably “much ado about nothing.” (To quote William Shakespeare, who attended King Edward VI Grammar School in Stratford.) After all, what do you think the odds are that the Scripting Guy who writes this column is going to pay to send his son to the University of Cambridge in Cambridge, England? A snowball’s chance in where? Exactly.


How Can I Dynamically Link Two Drop-Down Lists?

Hey, Scripting Guy! How can I configure a pair of dropdown lists in an HTA so that, when I change the selection in the first list, the options available in the second list also change?

-- PC

Hey, PC. Before we get going today, we’d like to make it clear that the Scripting Guy who writes this column isn’t the least bit tired. Sure, he and the Scripting Son went to the midnight showing of The Simpsons Movie last night, which meant that the Scripting Guy who writes this column didn’t get to bed until 2:15 AM, then had to get up less than four hours later in order to get to work and write this column. But why would that make him tired? Sure, staying up all night might make some old guy tired. But the Scripting Guy who writes this column? He’s not the least bit … um, what was that word again? Right. Tired. He’s not the least bit tired.

Not the bit least.

Um, not the least bit.

At any rate, seeing as how the Scripting Guy who writes this column is bright-eyed and bushy-tailed, and seeing as how he did remember to put on pants this morning (hold on a second … yes, he did), let’s see what we can do to address PC’s question. Which was … it’s on the tip of our tongue … oh, right. PC has an HTML Application (more familiarly known as an HTA) that features a pair of drop-down lists. When he makes a selection in the first list, he’d like the options in the second list to dynamically change in response to that choice. How can he do that? We have absolutely no idea. Good night; see you in the morning.

Wait a second; don’t go. It turns out that we have at least one idea as to how he can do this:

<Script Language="VBScript">
    Sub RunScript
        For Each objOption in DynamicOptions.Options
            objOption.RemoveNode
        Next

        If PickCategory.Value = "Characters" Then
            arrValues = Array("A", "B", "C", "D", "E")
        End If

        If PickCategory.Value = "Numbers" Then
            arrValues = Array("1", "2", "3", "4", "5")
        End If

        For Each strValue in arrValues
            Set objOption = Document.createElement("OPTION")
            objOption.Text = strValue
            objOption.Value = strValue
            DynamicOptions.Add(objOption)
        Next
    End Sub
</Script>

<select size="1" name="PickCategory" style="width:100px" onChange="RunScript">
    <option value="Characters">Characters</option>
    <option value="Numbers">Numbers</option>
</select>

<P>

<select size="1" name="DynamicOptions" style="width:100px">
    <option value="A">A</option>
    <option value="B">B</option>
    <option value="C">C</option>
    <option value="D">D</option>
    <option value="E">E</option>
</select>

As you can see, there’s really not too much to this particular HTA. (If you aren’t sure what an HTA even is then you might want to take a peek at our HTA Developers Center.) For example, the body of our HTA contains only a pair of drop-down lists. The first drop-down list is named PickCategory and includes just two options:

· Characters (the default value)

· Numbers

The premise here is simple: the user is going to select one of these two options. If the user selects Characters then, on-the-fly, we’ll display a series of letters as the individual options available in our second drop-down list (which we christened DynamicOptions). If the user selects Numbers, we’ll display a series of numbers as the options available in the second drop-down list. Each time we change the value of PickCategory we’ll run a subroutine named RunScript; hence the parameter onChange="RunScript":

<select size="1" name="PickCategory" style="width:100px" onChange="RunScript">
    <option value="Characters">Characters</option>
    <option value="Numbers">Numbers</option>
</select>

That second drop-down list, as we noted, is named DynamicOptions; when the HTA opens it displays, by default, a series of characters as the drop-down options (if you aren’t familiar with the HTML tagging for creating a drop-down list then take a look at one of our HTA tutorials). The complete tagging for this drop-down list looks like this:

<select size="1" name="DynamicOptions"
style="width:100px">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="E">E</option>
</select>

Of course, these default options aren’t set in stone; instead, they’re going to change each time the user makes a new selection in the PickCategory drop-down. How? Don’t worry; we’ll tell you how.

Zzzzz …

Sorry. Just resting our eyes.

Now, where were we? That’s odd; we have absolutely no idea what we were just talking about. But that’s all right; let’s just go ahead and talk about the subroutine RunScript, the subroutine that runs any time the user makes a new selection in the PickCategory drop-down list.

Zzzzz …

Um, so what does happen in the RunScript subroutine, you ask? To begin with, we use this block of code to remove all the existing options from the DynamicOptions drop-down:

For Each objOption in DynamicOptions.Options
    objOption.RemoveNode
Next

Why bother with this? That’s easy: each time we make a change in the first drop-down list we want to completely replace all the options shown in the second list. The easiest way to do that is simply to remove all the existing options and then start adding new options from scratch.

Speaking of which, our next step is to determine the actual value that was selected in drop-down list 1, then begin the process of assigning the appropriate set of options to drop-down list 2. (To that end, the first thing we’ll do is put all those options into an array named arrValues). The following block of code determines whether or not the value Characters was selected in our first drop-down; if so, it then assigns a series of letters to arrValues:

If PickCategory.Value = "Characters" Then
    arrValues = Array("A", "B", "C", "D", "E")
End If

Meanwhile, this block of code assigns a series of numbers to arrValues, but only if Numbers was selected in drop-down 1:

If PickCategory.Value = "Numbers" Then
    arrValues = Array("1", "2", "3", "4", "5")
End If

Once we’ve assigned the appropriate set of values to arrValues we then use this block of code to create the new options for drop-down list 2:

For Each strValue in arrValues
    Set objOption = Document.createElement("OPTION")
    objOption.Text = strValue
    objOption.Value = strValue
    DynamicOptions.Add(objOption)
Next

What we’ve done here is set up a For Each loop to loop through all the items in the array arrValues. For each of those items we create an instance of the Option object; this object represents an individual option in a drop-down list. For each instance of the Option object we then assign the value of the array item to both the Text and Value properties:

objOption.Text = strValue
objOption.Value = strValue

In other words, if the first value in our array is A we create a drop-down list option that not only has the label (Text) A but has the same value as well. We then use the Add method to add this new option to drop-down list 2:

DynamicOptions.Add(objOption)

And then we loop around and repeat this process until all the items in the array arrValues have been configured as drop-down list options. Once that’s finished we are done.

And not a moment too soon. Do you realize that, as we write this column, it’s not even 10:00 AM yet? This is going to be a long day.

Incidentally, for those of you who haven’t seen The Simpsons Movie yet, the Scripting Guy who writes this column found it refreshingly-good. There’s no doubt that the Simpsons’ glory days are behind them (no; why would a statement like that make anyone think of the Scripting Guys?); nevertheless, the movie had some pretty entertaining parts to it. For example, the Scripting Guy who writes this column has spent most of this morning singing the Spider-Pig song:

Spider-Pig, Spider-Pig
Does whatever a Spider-Pig does.
Can he swing from a web?
No, he can’t; he’s a pig.
Look out: here comes the Spider-Pig.

Trust us: that’s a good song.

Or at least it is at 2:00 in the morning.


How Can I Dynamically Populate a List Box in an HTA?

Hey, Scripting Guy! In my HTA, I would like to query a computer for a list of local user accounts, then dynamically create a list box containing those accounts. How can I do that?

-- BN

Hey, BN. You know, if you’d ever seen the Scripting Guys in … action … you’d think twice about asking us a question about doing things dynamically. But as long as you stick to things like dynamic list boxes (and forego things like dynamic personalities) we might be able to help.

Let’s start off by showing you the complete code for an HTA that retrieves all the local user accounts and then displays those accounts in a list box, which is populated on-the-fly. To make use of this code, copy it from your browser, paste it into Notepad or another text editor, and then save it with a .hta file extension. And if you’re already lost because you have no idea what an HTA is (hint: it’s short for HTML Application), then you might want to check out our HTA Developers Center, and maybe even peruse our HTA tutorial series.

Here’s the code:

<html>
<head>
<title>Local Users</title>

<HTA:APPLICATION
     ID="objHTAHelpomatic"
     APPLICATIONNAME="LocalUsers"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
     WINDOWSTATE="maximize"
>
</head>

<SCRIPT Language="VBScript">

    Sub Window_Onload
        Set objNetwork = CreateObject("Wscript.Network")
        strComputer = objNetwork.ComputerName
        Set colAccounts = GetObject("WinNT://" & strComputer & "")
        colAccounts.Filter = Array("user")

        For Each objUser In colAccounts
            Set objOption = Document.createElement("OPTION")
            objOption.Text = objUser.Name
            objOption.Value = objUser.Name
            LocalUsers.Add(objOption)
        Next
    End Sub

</SCRIPT>

<body>
    <select size="5" name="LocalUsers">
    </select>
</body>
</html>

The part we care about is the Window_Onload subroutine. (Incidentally, we use this subroutine because Window_Onload automatically runs any time an HTA is started or any time the HTA window is refreshed.) Inside this subroutine we do two things: retrieve a list of all the local user accounts, and then take those user names and put them into a list box. We won’t spend any time today talking about the process for retrieving local user accounts; all we’ll do for now is tell you that we do that using these four lines of code:

Set objNetwork =
CreateObject("Wscript.Network")
strComputer = objNetwork.ComputerName
Set colAccounts = GetObject("WinNT://" & strComputer &
"")
colAccounts.Filter = Array("user")

The part we will talk about today involves taking those user accounts and adding them to the list box. Before doing that we should point out that, when we start this HTA, the list box already exists; there just aren’t any items there. Admittedly, we could have been even more dynamic and created the list box itself on-the-fly. But that was a bit more complicated than we wanted to deal with, and didn’t really seem worth it anyway. In case you’re wondering, here are the HTML tags that create an empty list box:

<select size="5" name="LocalUsers">
</select>

And now, at last, here’s the code that populates that list box with user names:

For Each objUser In colAccounts
    Set objOption = Document.createElement("OPTION")
    objOption.Text = objUser.Name
    objOption.Value = objUser.Name
    LocalUsers.Add(objOption)
Next

What we do here is set up a For Each loop to cycle through the collection of user accounts. For each user account we create an instance of the HTML Option object; each Option object is equivalent to an item in the list box. That’s what this line of code is for:

Set objOption = Document.createElement("OPTION")

We then use these two lines of code to configure the Text and Value properties for the item:

objOption.Text = objUser.Name
objOption.Value = objUser.Name

If you haven’t done a lot of work with HTML, the Text is simply the text that appears in the list box. In the sample list box shown below, Option 2 is not only the user name that appears in the list box, it’s also the Text of the highlighted item:

The Value, meanwhile, is the data reported to a subroutine when a given option is chosen. Text and Value for an option do not have to be identical. We made them identical in this case simply because it makes sense: you want the user name to show up in the list box, and you probably want the user name to be used in your subroutine. (For example, your subroutine likely binds to that particular account, and you’ll need the user name in order to do that.) However, we could have set the Text to be the user name and the value to be, say, an employee ID number (assuming we had that information available to us). That’s up to you.

After configuring the Option object we add the new item to the list box like so:

LocalUsers.Add(objOption)

As you can see, all we do is refer to the list box (LocalUsers) and call the Add method. Along the way, we pass Add a single parameter: the object reference to our Option object. That’s pretty much it: we loop around, repeat the process for all the user accounts in the collection, and we’re done.

Yes, very easy and requires very little coding. But what did you expect? After all, even when the Scripting Guys are being dynamic, we still try to expend as little effort as possible.


How Can I Dynamically Show Available Drive Letters in a List Box?

Hey, Scripting Guy! I’m using an HTA as a drive-mapping application. I’d like to be clever about this and have the available drive letters appear in a list box. How can I dynamically show all the drive letters available on a computer?

-- JT

Hey, JT. You know, just the other day the Scripting Guy who writes this column was lamenting a truly lost weekend for Seattle-area sports fans. Well, we’re happy to report that things are finally looking up. After all, last night the Mariners’ Jeff Weaver – our top free agent acquisition, a “free” agent making more than $8 million this year – won his seventh game of the season, defeating the Oakland A’s 8-7.

And no, that’s not a misprint: 7 wins, and 8 million dollars. Admittedly, that sounds like a lot of money; more than $1 million for each victory. But look at it this way: if Jeff Weaver hadn’t won those 7 games, the Mariners wouldn’t make the playoffs. But with those seven wins, and with that outlay of $8 million, the Mariners will definitely make the – uh, never mind. We’re going to have to rethink our calculations.

Note. It’s obviously not the place of the Scripting Guy who writes this column to tell the Seattle Mariners how to run their team. (Although ….) On the other hand, it cost $8 million for the Mariners to have Jeff Weaver and not make the playoffs. Suppose next year they cut Jeff Weaver and pay the Scripting Guy who writes this column a measly $1 million to take his place. Will the Mariners make the playoffs with the Scripting Guy as part of their pitching rotation? Probably not. But they would save $7 million.

And yes, making $1 million a year would be a drastic pay cut for the Scripting Guy who writes this column. But he’s willing to do that for the chance to play major league baseball.

Hard as this might be to believe, however, Jeff Weaver winning his seventh game of the year isn’t the only exciting news to hit the Seattle area. In addition to that milestone, the whole town has been thrilled to hear about a script that can dynamically display available drive letters in a list box. Could such a script truly exist? Let’s see for ourselves:

<SCRIPT Language="VBScript">
    Sub Window_onLoad
        strComputer = "."

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

        Set colDisks = objWMIService.ExecQuery("Select * from Win32_LogicalDisk")

        Set objDictionary = CreateObject("Scripting.Dictionary")

        For Each objDisk in colDisks
            objDictionary.Add objDisk.DeviceID, objDisk.DeviceID
        Next

        For i = 67 to 90
            strDrive = Chr(i) & ":"
            If objDictionary.Exists(strDrive) Then
            Else
                strDrives = strDrives & strDrive & vbCrLf
            End If
        Next

        arrDrives = Split(strDrives, vbCrlf)

        For Each strDrive In arrDrives
            Set objOption = Document.createElement("OPTION")
            objOption.Text = strDrive
            objOption.Value = strDrive
            AvailableDrives.Add(objOption)
        Next
    End Sub
</SCRIPT>

<body>
    <select size="5" name="AvailableDrives" style="width:100">
    </select>
</body>

Before we launch into the discussion of this script and how it works, we should note that this is not a complete solution for mapping a network drive; all it does it demonstrate how to retrieve the available drive letters and add those drive letters to a list box. Are we going to show you a more complete solution before we call it a day? Maybe, but we aren’t going to tell you right now. After all, if we did that, you’d be tempted to skip directly to that portion of the article, which means you’d miss all the other cool things in today’s column.

Note. Not that there are any cool things in today’s column, mind you. But just in case there are, well, we’d hate for you to miss them.

The HTA itself is pretty simple: it consists solely of a list box named AvailableDrives, a list box that displays five items at a time (that’s what the Size property is for) and is 100 pixels wide. Here’s the HTML tagging for displaying the list box:

<select size="5" name="AvailableDrives" style="width:100">
</select>

And you’re right: this list box doesn’t have any options, does it? (Sort of like the Scripting Guys when it comes to career choices.) But don’t worry; as you might recall, the whole idea of this HTA is to add these options programmatically. And we’re going to do just that, in due time.

Note. What if you aren’t totally sure what we mean when we talk about HTAs, list boxes, and <SELECT> statements? That’s all right; just take a peek at our HTA Developer’s Center for some more information.

OK, so much for displaying a list box onscreen. Now let’s talk about the good stuff: dynamically adding available drive letters to that list box.

We’d like our HTA to automatically load those drive letters each time our little application is opened or refreshed. How do we do that? That’s easy: we put the list box-updating code in a subroutine named Window_onLoad:

<SCRIPT Language="VBScript">
    Sub Window_onLoad

If you’re familiar with HTML programming you know that the Window_onLoad subroutine is automatically executed any time a Web page (or HTA) is loaded or refreshed. What if you’re not familiar with HTML programming? That’s fine: now you know that the Window_onLoad subroutine is automatically executed any time a Web page (or HTA) is loaded or refreshed.

Inside the subroutine, we kick things off by connecting to the WMI service on the local computer. Could we run this HTA against a remote computer? Well, kind of. It’s easy enough to get drive letter information off a remote machine; however, mapping drives on that remote computer is a whole ‘nother story, and one we won’t get into today. (For a hint as to how you might go about doing that, take a gander at this Hey, Scripting Guy! column.

After we connect to the WMI service, we then use the ExecQuery method to return information about all the used drives on the computer, both local drives and mapped network drives:

Set colDisks = objWMIService.ExecQuery("Select * from Win32_LogicalDisk")

What’s that? You’re saying that all we’ve done so far is return a collection of the unavailable drive letters (that is, drive letters already in use)? You’re saying that what we need to do is figure out which drive letters are available? Hmmm, that is a bit of a dilemma, isn’t it? Wonder how the Scripting Guys will work their way out of this one?

And no, we’re not going to show you that, either, at least not right now. Before we can do any of that we need to create an instance of the Scripting.Dictionary object. Once we have an instance of the Dictionary object in hand we then execute this block of code:

For Each objDisk in colDisks
    objDictionary.Add objDisk.DeviceID, objDisk.DeviceID
Next

What are we doing there? What we’re doing there is looping through the collection of disk drives and, for each drive in the collection, adding the drive letter (DeviceID) to the Dictionary, using the drive letter as both the Dictionary key and item.

Note. We know, we know. But you can learn everything you’d ever want to know about the Dictionary object – and then some – in everyone’s favorite (well, second favorite) scripting column, Sesame Script.

When all is said and done, we’ll have a Dictionary object containing keys similar to this, with each key representing a used drive letter:

· C:

· D:

· E:

· M:

· X:

And you’re right: we still don’t have a list of available drive letters, do we? But that’s about to change, thanks to this block of code:

For i = 67 to 90
strDrive = Chr(i) & ":"
If objDictionary.Exists(strDrive) Then
Else
strDrives = strDrives & strDrive
& vbCrLf
End If
Next

Don’t worry; we were just about to explain how this works. To begin with, we set up a For Next loop that runs from 67 to 90. Why 67 to 90? Well, drive letters are limited to the letters C through Z (the letters A and B being reserved from floppy drives). As it turns out the ASCII value for the letter C is 67, and the ASCII value for the letter Z is 90. Setting up a For Next loop that runs from 67 to 90 gives us a sneaky way to loop through all the letters from C through Z.

Inside the For Next loop, we use the Chr function to convert the ASCII value of the loop variable (i) to an actual character; for example, the first time through the loop, i is equal to 67, which means that the Chr function will return the letter C. After converting the ASCII value we then tack a colon onto the end. That’s what this line of code is for:

strDrive = Chr(i) & ":"

In turn, that means that – the first time through the loop – the variable strDrive is equal to this: C:.

Which, now that you mention it, does look an awful lot like a drive letter, doesn’t it?

That brings us to this line of code:

If objDictionary.Exists(strDrive) Then

Here we’re using the Exists method to determine whether or not that first drive letter appears in the Dictionary object. Let’s assume that it does. That means that the drive letter is already in use, and thus not available for drive mapping. Consequently, we don’t do anything at all. (Well, other than circling around and repeating the process with the next value in the loop.)

Now, suppose that the drive letter can’t be found in the Dictionary. (That is, suppose the Exists method returns False.) That can only mean one thing: this drive letter is not in use, and thus is available for drive mapping. Consequently, we use this line of code to add the drive letter (and a carriage return-linefeed character) to a variable named strDrives:

strDrives = strDrives & strDrive & vbCrLf

Got that? By the time we exit the loop, all the available drive letters will be safely tucked away in the variable strDrives.

Now all we have to do is figure out how to get those drive letters into our list box. To that end, our next step is to use the Split function to create an array named arrDrives; as you can see, we do this by splitting the variable strDrive on the carriage return-linefeed character (vbCrLf):

arrDrives = Split(strDrives, vbCrlf)

In turn, that gives us an array consisting of the following items (on our test computer), items that correspond quite nicely to the available drive letters on the computer:

· F:

· G:

· H:

· I:

· J:

· K:

· L:

· N:

· O:

· P:

· Q:

· R:

· S:

· T:

· U:

· V:

· W:

· Y:

· Z:

And now – finally – we can add the available drive letters to the list box (and don’t worry; the script zips by much quicker than the explanation does):

For Each strDrive In arrDrives
    Set objOption = Document.createElement("OPTION")
    objOption.Text = strDrive
    objOption.Value = strDrive
    AvailableDrives.Add(objOption)
Next

All we’ve done here set up a For Each loop to cycle through the collection of available drive letters. For each item in the collection we create an instance of the HTML Option object; each Option object is equivalent to an item in the list box. That’s what this line of code is for:

Set objOption = Document.createElement("OPTION")

We then use these two lines of code to configure the Text and Value properties for the item:

objOption.Text = strDrive
objOption.Value = strDrive

If you haven’t done a lot of work with HTML, the Text is simply the text that appears in the list box. The Value, meanwhile, is the data reported to a subroutine when a given option is chosen. Text and Value for an option do not have to be identical. We made them identical here simply because it makes sense: you want the drive letter to show up in the list box, and you probably want the exact same drive letter to be used in your subroutine.

After configuring the Option object we add the new item to the list box like so:

AvailableDrives.Add(objOption)

As you can see, all we do is refer to the list box (AvailableDrives) and call the Add method. Along the way, we pass Add a single parameter: the object reference to our Option object. And then we loop around and repeat the process with the next item in the array. When that’s finished, all the available drive letters will show up in a list box, a list box that should look something like this:

Now, what about that more-complete solution, the one that actually maps a drive for us? Well, without any further explanation, here’s one way to do that. To use this HTA, select a drive letter, then click the Map Drive button. When the dialog box appears, type in the UNC path to the network drive (e.g., \\atl-fs-01\public). Click OK and two things will happen:

· The drive will be mapped

· The list box will refresh itself (because a previously-available drive letter is no longer available)

Here’s the code:

<SCRIPT Language="VBScript">

    Sub Window_onLoad

        Set objDictionary = CreateObject("Scripting.Dictionary")

        strComputer = "."

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

        Set colDisks = objWMIService.ExecQuery("Select * from Win32_LogicalDisk")

        For Each objDisk in colDisks

            objDictionary.Add objDisk.DeviceID, objDisk.DeviceID

        Next

        For i = 67 to 90

            strDrive = Chr(i) & ":"

            If objDictionary.Exists(strDrive) Then

            Else

                strDrives = strDrives & strDrive & vbCrLf

            End If

        Next

        arrDrives = Split(strDrives, vbCrlf)

        For Each strDrive In arrDrives

            Set objOption = Document.createElement("OPTION")

            objOption.Text = strDrive

            objOption.Value = strDrive

            AvailableDrives.Add(objOption)

        Next

    End Sub

    Sub MapDrive

        If AvailableDrives.Value = "" Then

            Msgbox "Please select a drive letter."

            Exit Sub

        End If

        strDrive = AvailableDrives.Value

        strPath = InputBox("Please enter the network path:")

        If strPath = "" Then

            Exit Sub

        End If

        Set objNetwork = CreateObject("Wscript.Network")

        objNetwork.MapNetworkDrive strDrive, strPath

        Location.Reload(True)

     End Sub

</SCRIPT>

<body>

    <select size="5"
name="AvailableDrives"
style="width:100px"></select><p>

    <input type="button"
value="Map Drive"
onClick="MapDrive">

</body>

As for the Mariners, well, if anyone from the Seattle front office is reading this column (and, to be honest, we’d really prefer that you were reading Baseball Digest instead) just send email to scripter@microsoft.com and make us an offer. We can’t promise to accept that offer; why that would mean giving up a thrilling and rewarding career as a technical writer just for the chance to fly around the country playing baseball. But we’ll definitely take it under consideration.


How Can I Encode Scripts Within an HTA?

Hey, Scripting Guy! How can I encode scripts within an HTA?

-- LL

Hey, LL. You know, when we first read this question we thought, “What’s the big deal? Can’t you encode scripts in an HTML Application (HTA) the same way you encode scripts in an HTML file?” Much to our surprise, it appears as though you can’t encode scripts in an HTA; each time we tried we got back an error message similar to this:

Scripting encoder object ("Scripting.Encoder") failed on C:\Scripts\test.hta

To be honest, we never found a way to encode scripts within an HTA. But that’s OK, because we did find a sneaky way to work around the problem. And we’ll show you that in just a moment.

Before we do that, however, we need to take a second and explain what we mean by encoding scripts. Microsoft has a utility – aptly named the Script Encoder – that can “obfuscate” your script code. For example, suppose you have a script that looks like this:

strComputer = "."

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

   Set colOperatingSystems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

For Each objOperatingSystem in colOperatingSystems
    Msgbox objOperatingSystem.Caption & " " &   objOperatingSystem.Version
Next

Let’s say that, for some reason, you’d prefer that people not be able to read the script code. That’s a problem: after all, anyone who has permission to run a script is going to have permission to open the script up in Notepad and gawk at the code all they want. But here’s what that same script looks like after the Script Encoder gets done with it:

#@~^8AEAAA==@#@&@#@&@#@&,PP,?!8PV+OrU.n.kkW@#@&,PP,~~PkYMZG:2;D+.P{~JcJ@#@&,PP~~,P~P,~U+O,W(Ltq
UnD7k^+,'~!Yr(%+1Y`rArxsosYd)wwrP'PkOD;Wh2!Y+.~LP~J'.KWO'mb:\yJ*@#@&@#@&~P,P~~,PP,~?YP1Gsra+MlOkULUXdYh/,'~G(
L\(U+.\b^R3X+1pEDH~{@#@&~P,P~~,PP,~P,PPvEj+^+1Y~e~W
MWhPqrx2 m6a+DCObxL?HdD+hr#@#@&@#@&P,~P,P~P,P~oKDPACm4PW(%6wDmYr
xLjH/O+s~kP^G^rwn.mYrxTjH/O:k@#@&,P,~P,P~P,P~~,PHkL4K6PK8%ra+M
lOkULUXdYhR;l2ObWx~',J~J,',{@#@&P,PP,P,~P,P~P,P~~,PW(%ra+DmOrxT?H/O+h
j+./bGx@#@&~~,PP~~,P~PgnXY@#@&P,PPAx9~?!4@#@&QYQAAA==^#~@

The script still works, it’s just a bit harder for anyone to glance at the code and plagiarize your work. (And you’re right: even at that it does make more sense than most of the jokes used in Hey, Scripting Guy!)

Now, the important thing to keep in mind is that the script is simply encoded (or obfuscated); it is definitely not encrypted. What does that mean? That means the encoder will hide your script from most people; however, a truly determined hacker – armed with a knowledge of codes or armed with a utility downloaded from the Internet – could crack the code. Among other things, that means that you should never do something like “hide” an Administrator password in a script and assume that the Script Encoder will keep it safe from prying eyes. It won’t. It’s an encoder, not an encrypter, and there’s definitely a difference.

But what about the problem with encoding scripts in an HTA? Well, let’s assume you have an HTA that looks like this (we’ll also assume that you’ve already downloaded and installed the Script Encoder):

<head>
<title>Operating System Version</title>
<HTA:APPLICATION
     APPLICATIONNAME="Operating System Version"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<script language="VBScript">

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

           Set colOperatingSystems = objWMIService.ExecQuery _
               ("Select * from Win32_OperatingSystem")

           For Each objOperatingSystem in colOperatingSystems
               Msgbox objOperatingSystem.Caption & " " & _
                   objOperatingSystem.Version
           Next
    End Sub
</script>

<body>
<input type="button" value="Operating System" name="run_button"  onClick="GetOSVersion">

</body>

If you want to encode the VBScript portion of this HTA the first thing you need to do is use the '**Start Encode** tag to mark the beginning of the section to be encoded. In other words, your <Script> tag needs to look like this, with '**Start Encode** coming right before the very first line of VBScript code:

<script language="VBScript">

'**Start Encode**

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

           Set colOperatingSystems = objWMIService.ExecQuery _
               ("Select * from Win32_OperatingSystem")

           For Each objOperatingSystem in colOperatingSystems
               Msgbox objOperatingSystem.Caption & " " & _
                   objOperatingSystem.Version
           Next
    End Sub
</script>

In theory, you should then be able to encode the script by running Screnc.exe and passing two parameters, the name of the existing HTA file (Test.hta) and the name to be given to the encoded file (Encoded.hta):

C:\Scripts>screnc test.hta encoded.hta

Unfortunately, though, this doesn’t work.

Why? Well, we aren’t really sure. But we do know what does work. All we have to do is change the file extension on the existing file from .hta to .htm; in other words, we simply rename the file Test.htm. The Script Encoder can’t handle HTA files, but it has no problems with HTML files. Now we can start the Script Encoder using syntax like this:

C:\Scripts>screnc test.htm encoded.hta

Are we sure that will work? Don’t tell us you doubt the Scripting Guys! Here’s what Encoded.hta looks like:

<head>
<title>Operating System Version</title>
<HTA:APPLICATION
     APPLICATIONNAME="Operating System Version"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<script language="VBScript.Encode">

'**Start Encode**#@~^8AEAAA==@#@&@#@&@#@&,PP,?!8PV+OrU.n.kkW@#@&,PP,~~PkYMZG:2;D+.P{~JcJ@#@&,PP~~,P~P,~U+O,W(Ltq
UnD7k^+,'~!Yr(%+1Y`rArxsosYd)wwrP'PkOD;Wh2!Y+.~LP~J'.KWO'mb:\yJ*@#@&@#@&~P,P~~,PP,~?YP1Gsra+MlOkULUXdYh/,'~G(
L\(U+.\b^R3X+1pEDH~{@#@&~P,P~~,PP,~P,PPvEj+^+1Y~e~W
MWhPqrx2 m6a+DCObxL?HdD+hr#@#@&@#@&P,~P,P~P,P~oKDPACm4PW(%6wDmYr
xLjH/O+s~kP^G^rwn.mYrxTjH/O:k@#@&,P,~P,P~P,P~~,PHkL4K6PK8%ra+M
lOkULUXdYhR;l2ObWx~',J~J,',{@#@&P,PP,P,~P,P~P,P~~,PW(%ra+DmOrxT?H/O+h
j+./bGx@#@&~~,PP~~,P~PgnXY@#@&P,PPAx9~?!4@#@&QYQAAA==^#~@</script>

<body>
<input type="button" value="Operating System" name="run_button"  onClick="GetOSVersion">

</body>

It’s still a valid HTA; click the button and it will report back the name and version of the operating system installed on the local machine. But to anyone who tries looking at the code it will look like pure gibberish. (Needless to say, the Scripting Guys know gibberish better than anyone!)


How Can I Ensure That a User Enters Only Allowed Characters in a Text Box?

Hey, Scripting Guy! I have an HTA which includes a text box where the user must enter a part number. Part numbers must consist of a certain set of characters; how can I determine if a user entered an invalid character in the part number text box?

-- RR

Hey, RR. You know, with one exception, no one has ever accused the Scripting Guy who writes this column of being a genius. (The one exception, of course, is the Scripting Editor; she thinks that everything that this Scripting Guy does is just plain perfect.)

Editor’s Note: The Scripting Guy who writes this column is obviously having one of his delusional episodes. Don’t worry, it’s not serious; they’re mostly harmless and the Scripting Editor always manages to set him straight.

Still, the Scripting Editor aside, the Scripting Guy who writes this column will occasionally do something dumb. For example, it’s time for yet another out-of-town baseball tournament, this one to be held in beautiful Walla Walla, WA (“The town so nice they named it twice!”). Because of that, the Scripting Guy who writes this column is in a hurry to get today’s column taken care of; that way he can hit the road as quickly as possible. That means that the smart thing for him to do would be to pick a question – and write a column about – a topic he already knows by heart, one that he can sit down and hammer out a script and accompanying article without even thinking about it.

OK, not that he ever thinks too terribly hard about any column he writes. But you know what we mean.

Instead, the Scripting Guy who writes this column picked a question that he knew could be solved, although he wasn’t exactly sure how to go about solving it. Fortunately, with a little diligence and a little effort, along with the fact that he really needs to get going pretty soon, he was able to come up with the following solution:

<SCRIPT LANGUAGE="VBScript">
    Sub VerifyEntry
        Set objRegEx = CreateObject("VBScript.RegExp")
        objRegEx.Global = True
        objRegEx.Pattern = "[^ABCDEFG12345]"

        strSearchString = TextBox1.Value

        Set colMatches = objRegEx.Execute(strSearchString)

        If colMatches.Count > 0 Then
            Msgbox "You entered an invalid character."
            TextBox1.Focus
        End If
    End Sub
</SCRIPT>

<body>
    <input type="text" name="TextBox1" size="50" onFocusOut="VerifyEntry"><p>
    <input type="text" name="TextBox2" size="50">
</body>

We have a few minutes before we have to go, so let’s see if we can explain how this all works. To begin with, the body of our HTA consists solely of a pair of text boxes, text boxes that look identical, but actually have two key differences:

· The names of the text boxes. The first text box (the one we care about) is named TextBox1. The second text box has an even catchier name: TextBox2. And, yes, now that you mention it, these two text boxes were named after the Scripting Guy’s mom and dad. Seemed like a nice anniversary present, don’t you think?

· The events attached to each text box. Textbox2 doesn’t have any events attached to it; Textbox1, on the other hand, features the onFocusOut event. When you work with a control in an HTA or a Web page (for example, when you type something in a text box) that control is said to have the “focus.” When you leave that control (for example, by clicking somewhere else or by pressing the TAB key) the control loses the focus. When the control loses focus, the onFocusOut event fires, and – in this case—results in our HTA running the subroutine VerifyEntry.

Note. If none of that makes any sense to you that’s either because the Scripting Guy who writes this column repeated each and every word (“The column so nice he wrote it twice!”) or, more likely, because you aren’t familiar with HTAs. How can you become familiar with HTAs? Why, by checking out the HTA Developers Center, of course.

As you might expect, any excitement generated by this script will be generated by the VerifyEntry subroutine. In fact, the first thing we do inside this subroutine is very exciting: we create an instance of the VBScript.RegExp object. This, by the way, is a VBScript object that enables us to perform regular expression searches.

Note. What’s that? You’re not familiar with regular expression searches, either? Then you need to check out Dean Tsaltas’ now-classic webcast String Theory for System Administrators.

After creating the regular expression object we next set the Global property to True; that ensures that the script will search the entire value typed into TextBox1. (Technically we don’t need to search the entire value for this script. However, in most cases you will want to search the entire value; we added this line of code so you could see how to do that.) We then specify the Pattern we want to search for:

objRegEx.Pattern = "[^ABCDEFG12345]"

Regular expression syntax is always a bit goofy-looking (at best), so let’s explain what we’ve got here. We’re assuming that RR only allows the following characters to be entered as a part number:

· A

· B

· C

· D

· E

· F

· G

· 1

· 2

· 3

· 4

· 5

By enclosing these characters in square braces we’re able to specify the set of characters that we’re looking for. Suppose we wanted to search for the characters X, Y, or Z? Then we’d use this syntax:

objRegEx.Pattern = "[XYZ]"

Of course, in this case, we don’t want to search for the characters in brackets; instead, we want to search for everything except the characters in brackets. Why? Well, suppose a user enters a part number of AB325@. We don’t need to search for the characters A, B, 3, 2, or 5; it’s perfectly legitimate for those characters to be included in the part number. Instead, we want to search for the @ sign, or for any other invalid character. That’s why our pattern includes the ^ mark in front of the character set; that’s regular expression syntax for, “Search for everything that isn’t included in the character set.” Want to search for X, Y, or Z? Then use this syntax:

objRegEx.Pattern = "[XYZ]"

Want to search for characters other than X, Y, or Z? Then use this syntax:

objRegEx.Pattern = "[^XYZ]"

Try the script a couple times and you’ll see how this works.

After defining the pattern we next need to identify the value we want to search. Because that value happens to be contained in the Value property of TextBox1 we use this line of code to assign the information typed into TextBox1 to a variable named strSearchString:

strSearchString = TextBox1.Value

From there we can call the Execute method and carry out our search:

Set colMatches = objRegEx.Execute(strSearchString)

As we noted a moment ago, what we’re doing here is searching the variable strSearchString for any character that is not part of the specified character set. For each such match (that is, for each character that isn’t part of the pattern) information about the match gets stored in the Matches collection (a collection we named colMatches). If all the characters in the string are valid characters then there won’t be any matches; in that case the value of the Count property will be 0. If an invalid character is found, that means that the collection will include at least one item. Our next step is to check for that very thing by using this line of code:

If colMatches.Count > 0 Then

If the Count is greater than 1 that means that we found at least one invalid character. In that case we execute these two lines of code:

Msgbox "You entered an invalid character."
TextBox1.Focus

In the first line we simply echo back a message box stating that an invalid character was entered. In the second line, we then set the focus back to TextBox1. That essentially forces the user to enter a valid part number before he or she can continue with data entry.

That should do the trick. In the meantime, the Scripting Guy who writes this column is about to become the Scripting Guy who’s driving to Walla Walla. (Although that seems a little pointless, considering the fact that the Walla Walla Corn Maze doesn’t open until September 15th.) But don’t worry: although he’ll be out of town enjoying such attractions as the Nothing New antiques store (because new antiques are just never as much fun as old antiques) the Scripting Guy who writes this column will make sure that there’s a new Hey, Scripting Guy! each and every day. Does that means that he’s such a devoted and dedicated Microsoft employee that he’s willing to work on his vacation? You bet that’s what it means.

Well, that and the fact that he’s hoping to claim that these weren’t actually vacation days after all. Hey, he did write a new column each and every day, didn’t he? That sounds like work to us. (OK, true, it really isn’t much work. But it sounds like work.)


How Can I Export an HTA Table to Excel?

Hey, Scripting Guy! How can I put a button in an HTA that, when clicked, exports an HTML table to Excel?

-- DW

Hey, DW. You know, yesterday afternoon the Scripting Guy who writes this column went to get his hair cut, figuring he needs to look his best for TechEd 2007. (It was only after he got his hair cut that he realized what a futile gesture that was; he should have stopped by the plastic surgeon’s rather than the barbershop.) As he was parking his car, he noticed the following sign in a restaurant window:

SOUP-FREE

This sign intrigued the Scripting Guy who writes this column to no end. His first thought, of course, was that soup was free at this restaurant: order a bowl of soup and they won’t charge you for it. But then he thought, “Maybe this is like a smoke-free restaurant, except that it’s soup-free; maybe you can smoke in there but you can’t order a bowl of soup.” For the Seattle area, that seemed plausible:

“Where do you want to eat tonight?”

“How about that new place in Redmond?”

“Do they serve soup there?”

“I think so.”

“Then forget it; I will never set foot in any restaurant that serves soup! We either find a soup-free restaurant or we stay home.”

At any rate, if anyone out there is looking for a soup-free restaurant, well, just let us know; we think we have one for you.

Note. As intriguing as the SOUP-FREE sign is, it still ranks as only the second-most intriguing sign the Scripting Guy who writes this column has ever seen. But, then again, it’s going to be hard to top the Certificate of Non-Significance.

And no, the Scripting Guy who writes this column doesn’t actually have a Certificate of Non-Significance. Some things are so obvious they don’t need any kind of formal certification.

If it turns out that you are violently opposed to the whole idea of soup, well, we have good news for you: today’s solution is also 100% soup-free. The truth is, we didn’t know of any simple and straightforward way to export an HTML table to Excel (with or without soup). However, we came up with something we thought might be even better: a simple and straightforward way to copy an HTML table and then paste it into Excel. In fact, we came up with this way to copy an HTML table and then paste it into Excel:

<Script Language="VBScript">
    Sub RunScript
        strCopy = MyTable.InnerHTML
        document.parentwindow.clipboardData.SetData "text", strCopy

        Set objExcel = CreateObject("Excel.Application")
        objExcel.Visible = True

        Set objWorkbook = objExcel.Workbooks.Add()
        Set objWorksheet = objWorkbook.Worksheets(1)
        objWorksheet.Paste
    End Sub
</Script>

<body>
    <span id=MyTable>
        <table border="1" width="100%" id="table1">
    <tr>
<td>Cell 1</td>
<td>Cell 2</td>
<td>Cell 3</td>
    </tr>
    <tr>
<td>Cell 4</td>
<td>Cell 5</td>
<td>Cell 6</td>
    </tr>
    <tr>
<td>Cell 7</td>
<td>Cell 8</td>
<td>Cell 9</td>
    </tr>
        </table>
    </span>
    <br>
    <input id=runbutton type="button" value="Run Button" onClick="RunScript">
</body>

What we have here is a very simple little HTA, one that looks a lot like this:

As you can see, the bulk of our little HTA is a table, in this case a nine-cell table (three rows by three columns). Per DW’s email, we’ve put this table inside a <SPAN> tag with the ID MyTable; here’s the code that creates the <SPAN> and initializes the table:

<span id=MyTable>
    <table border="1" width="100%" id="table1">

For those of you new to HTML tagging, a <SPAN> is simply a named area of your document; we can define an area of the document and then stuff that area full of anything we want (in this case, the only thing we’ve put in the <SPAN> is a table.) The cool part, however, is this: as long as we give the <SPAN> an ID we can then manipulate that portion of the document programmatically. That might mean making dynamic changes to that section of the HTA. Or, as is the case here, it might be doing nothing more than grabbing all the information inside the <SPAN>.

The long and short of it? We have a table in our HTA and we have a mechanism for identifying and manipulating that table.

We only have one other element in the HTA body: a button (labeled Run Button) that, when clicked, runs a subroutine named RunScript:

<input id=runbutton type="button" value="Run Button" onClick="RunScript">

As you might expect, RunScript is the subroutine that’s going to grab a copy of the table and somehow get that copy into an Excel spreadsheet.

Good question: how does this subroutine grab a copy of the table and get that copy into an Excel spreadsheet? Let’s see if we can figure that out.

To begin with, we use this line of code to connect to the MyTable <SPAN> and assign the value of the InnerHTML property to a variable named strCopy:

strCopy = MyTable.InnerHTML

What’s the InnerHTML property? That’s basically everything that falls between the beginning <SPAN> tag and the ending </SPAN> tag; that includes both the data found in the table and the HTML tagging for creating and formatting that table. In other words, we now have all the information necessary for re-creating the table stashed away in the variable strCopy. Once that’s done we use this line of code to copy that information to the Windows Clipboard:

document.parentwindow.clipboardData.SetData "text", strCopy

What we’re doing here is calling the SetData method, a method that just happens to belong to the clipboardData object. In addition, we’re passing SetData two parameters:

· "text", which represents the type of data being copied to the Clipboard.

· strCopy, the actual data we want to copy to the Clipboard.

As you probably guessed, after calling SetData our table will be copied to the Windows Clipboard; in turn, that means we’re ready to paste this data into Excel. To that end, we first create an instance of the Excel.Application object and then set the Visible property to True:

Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True

That gives us a running instance of Excel that we can view onscreen. We then take that running instance, create a new workbook, and bind to the first worksheet in that workbook:

Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets(1)

So then how do we paste in our table? Why, by calling the Paste method, of course:

objWorksheet.Paste

Yes, just like a soup-free restaurant, this sounds too good to be true. But give it a try and see for yourself:

Truth be told, this is pretty darn easy; play around with this a little bit and you’ll quickly catch on. And you might have to play around with it by yourself because, for better or worse, we have to call it a day; after all, we need to create a new sign for the Hey, Scripting Guy! column:

SCRIPT-FREE

So does that mean that all the scripts in Hey, Scripting Guy! are free of charge … or does that mean that Hey, Scripting Guy! doesn’t even try to include script code any more? Good question. (Although it is getting harder and harder for us to find room for script code, what with the need to talk about haircuts, soup-free restaurants, and Certificates of Non-Significance.)


How Can I Force a Specified Letter Case in an HTA?

Hey, Scripting Guy! In our company we often use HTAs as data entry forms. For some of our applications case-sensitivity is important; for example, our product numbers are case-sensitive. I know I can write a script to change letter case, but I was wondering if there was a way to force letter case (either all uppercase or all lowercase) at the time people actually do their data entry.

-- SK

Hey, SK. You know, the great thing about this column – oh, wait; that’s a different column. In fact, now that we think about it, we can’t come up with any great things about this column. Be that as it may, however, the interesting thing about this column is that – well, you’re right: that really isn’t all that interesting, is it? Hmmm ….

OK, let’s try this again. You know, the not-so-great and not-all-that-interesting thing about this column (better?) is that we Scripting Guys probably learn more from writing the column than anyone does from reading it. Based on the emails we receive we know that some of you (obviously those who don’t know us very well) assume that the Scripting Guys can answer any scripting question at all, and right off the top of our heads. And you know, come to think of it, maybe we can answer any question that people ask:

“Hey, Scripting Guy! How can I force letter case when people do data entry?”

“Beats us.”

Technically, “Beats us” is an answer. It’s just not a very good answer.

But don’t fret, SK. Following our usual pattern, we did a bit of research and managed to come up with a better answer for you:

<body>
    Box 1 <input type="text" name="Box1" size="30" style="text-Transform:uppercase"><P>
</body>

As you can see, this is perhaps the simplest HTA ever created: there’s nothing to it except for a single text box. But take a look at the HTML tag for that text box:

<input type="text" name="Box1" size="30" style="text-Transform:uppercase">

What we’ve done here is apply a style to the box; in particular, we’ve assigned a value to the text-Transform attribute: style="text-Transform:uppercase". Is there a method behind that madness? You bet there is: setting text-Transform to uppercase automatically converts any text typed into that box to its uppercase equivalent. Best of all, you can do that without having to write any script code.

And, now that you mention it, yes, there are other values you can assign to text-Transform. For one, you can assign the value lowercase to convert all the letters typed into the box to their lowercase equivalent. For another, you can assign the value capitalize to make the first letter in each word uppercase (subsequent letters in the word are not converted; they show up exactly as you type them). Here’s a sample HTA that uses each of these text-Transform values. Take a close look at the letter casing in each text box; all the letters in all three boxes were typed using lowercase. But, as you can see, they don’t all show up as lowercase:

And here’s the HTML tagging we used to create this sample form:

<body>
Box 1 <input type="text" name="Box1"
size="30" style="text-transform:uppercase"><P>
Box 2 <input type="text" name="Box2"
size="30" style="text-transform:lowercase"><P>
Box 3 <input type="text" name="Box3"
size="30" style="text-transform:capitalize"><P>
</body>

Pretty cool, huh?

We might add that although SK didn’t want to use a script to modify letter case you (or even you, the guy in the green) can use a script if you so choose. Here’s a modified HTA that features a button that toggles between lowercase and uppercase:

<Script Language="VBScript">
    Sub CaseChanger
        If Box1.Style.textTransform="lowercase" Then
            Box1.Style.textTransform="uppercase"
        Else
            Box1.Style.textTransform="lowercase"
        End If
    End Sub
</Script>

<body>
    Box 1 <input type="text" name="Box1" size="30"><P>
    <input  type="button" Value="Run" type="button" onClick="CaseChanger" style="text-Transform:lowercase">
</body>

The cool thing here is that it not only toggles the style assigned to the text box, but it dynamically changes the letter case for any text currently in the text box.

We hope that helps, SK. And we hope that you learned something today, because – as usual – the Scripting Guys learned something: we learned that it would be much better if we did our research ahead of time so that we could answer questions off the top of our heads. And so, by golly, we’re going to do that: we’re going to do our homework and learn everything there is to know about scripting.

But not today; today the Scripting Son has a baseball game and we need to leave early. But tomorrow, tomorrow we start.

Well, actually, the NCAA men’s basketball tournament is tomorrow. But next week, next week for sure.


How Can I Get a Script to Close an HTML Page?

Hey, Scripting Guy! In one of my scripts I display an HTML page with information that I want my users to read. When they’re finished reading that page, they should be able to press a key and have the Web page disappear. However, instead of the Web page disappearing they get a message box with this message: “The Web page you are viewing is trying to close the window. Do you want to close the window?” The user then has to click Yes to get rid of the Web page. How can I fix that?

-- RR

Hey, RR. By the way, congratulations: this is the longest question that we’ve ever included in a Hey, Scripting Guy! column. Is there a prize for that? No, but it’s certainly something you can tell your grandkids someday.

Well, we didn’t say that it would impress those grandkids. But if you’re anything like the Scripting Guys (and let’s hope that you aren’t), then you latch on to any achievement, no matter how trivial. After all, if it wasn’t for trivial achievements that don’t impress anyone we’d never be able to write our year-end reviews!

At any rate, we’re assuming that you have an HTML file that looks something like this:

<BODY onkeypress='self.close()'>
Press any key to close this window.<BR>
</BODY>

Admittedly, yours is probably a little fancier, but the idea is the same: you’ve got some sort of Web page with an onkeypress event attached to the <BODY> tag. If this page is the active Window and someone presses any key on the keyboard, then this little script will run:

self.close()

And that little script should close the page. As you noted, however, what happens instead is that the user sees a message box asking if they really do want to close the page. That’s a security measure built into Internet Explorer: it’s designed to prevent someone else from writing a script that shuts down your Web page. In this case, however, it’s preventing anyone, including yourself, from using a script to close the page. (You can, of course, click the Close button in order to dismiss the page. But obviously that isn’t what you want to do.)

So how can you fix this? Well, we actually know of two ways to do this. For one, you can simply change the file extension from .htm to .hta. This changes your plain old HTML page into a – tah-dah! – HTML Application (HTA). HTAs run in a different process and follow a different security model than HTML pages do; because of that, the self.close() method will be able to close an HTA without generating a message box.

That’s a pretty easy fix. Unfortunately, though, it’s not a guaranteed fix; that’s because there are HTML pages that can’t be converted to HTAs (again, due to the fact that they follow different security models). But that’s OK: like we said, we know another way to work around this problem. If you don’t want to or can’t turn your HTML page into an HTA then all you have to do is add a Window_OnLoad subroutine, making your Web page look like this:

<SCRIPT LANGUAGE="VBScript">
    Sub Window_Onload
      window.opener = "x"
    End Sub
</SCRIPT>

<BODY onkeypress='self.close()'>
Press any key to close this window.<BR>
</BODY>

Yes, it is pretty simple; in fact, the subroutine itself consists of just one line of code:

window.opener = "x"

The Opener property is a reference to the window that opened your window. When you pop up your Web page, the Opener property is Null. That’s to be expected: after all, your Web page wasn’t opened from another window. However, in Internet Explorer a script is not allowed to close a page that doesn’t have an Opener. Fortunately, all you have to do is set the value of the Opener property to something (even something meaningless, like x) and your script will then work just fine.

We should point out that our answer is based on your particular scenario: you’re opening up an HTML file from the desktop. It appears that this also works with an HTML page served up by a Web server. However, we only gave that a rudimentary test, so that’s the best we can say: it appears to work in that situation as well. But that’s not so bad: after all, look how far the Scripting Guys have gotten by merely appearing to work!


How Can I Get the Actual Text of a List Box or Dropdown List Option?

Hey, Scripting Guy! In my HTA, how can I get the actual text of a list box or dropdown list option?

-- MM

Hey, MM. You might be familiar with an old Mac Davis song, a song that starts off like this:

Oh Lord it's hard to be humble
When you're perfect in every way

Now, admittedly, the Scripting Guys aren’t perfect in every way. But – all modesty aside – there is one area where we have an absolutely perfect record: our initial response to any Hey, Scripting Guy! inquiry is always wrong.

Your question is a, well, perfect example of that. When you add a dropdown list to an HTA (HTML Application), you use code similar to this:

<select size="1"
name="OptionChooser" onChange="TestSub">
<option value="0"></option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>

As you can see, we our dropdown list includes several options (the actual items that appear in the list), and each option has a unique value. Take the second option, for example. The text that actually appears in the dropdown list is Option 1, but the value (the item typically used by a script) is 1. The value and the text don’t have to be different, but they usually are, and often-times they need to be. For example, in a dropdown list of web sites you might want your text to read Script Center even though the value needs to be the Script Center URL. Thus the value and the text end up different.

Note. If you have no idea what we’re talking about, HTAs provide a way to give your scripts a graphical user interface. If that intrigues you (and it should; HTAs can be very cool) you might want to take a look at the HTA Developers Center and, in particular, our tutorial on creating HTAs.

Generally speaking, the fact that the value and the text are different doesn’t matter much…unless, of course, you need to get at the text of an individual option. Can you do that? Our first reaction was this: no way; not in a million years.

Well, if nothing else at least we preserved our perfect record: as it turns out there is a way to get at the text of an individual option. On top of that, you don’t even have to do anything special to the dropdown list; instead, all you need to do is access the InnerText property of the selected option. Here’s a little subroutine that can retrieve the InnerText property for the option selected in a dropdown list (a list we named OptionChooser):

Sub TestSub
    For Each objOption in OptionChooser.Options
        If objOption.Selected Then
            Msgbox objOption.InnerText
        End If
    Next
End Sub

To get at the value we set up a For Each loop that loops through the Options collection for the dropdown list. (As you probably guessed, the Options collection consists of all the options included in the dropdown list.) For each individual option we check to see if the Selected property is True; that’s what this line of code does:

If objOption.Selected Then

If Selected is True, that means this is the selected option. We then use this line of code to display the value of the InnerText property in a message box:

Msgbox objOption.InnerText

If Selected is False we just loop back around and check the next option in the collection.

And yes, it’s that easy, even if our first reaction was to say “Can’t be done.” If you’d like to try this out for yourself, here’s some HTA code that demonstrates the technique. Copy the code, paste it into Notepad, and then save the file with a .hta file extension:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="HTATest"
     SCROLL="no"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT LANGUAGE="VBScript">

    Sub TestSub
        For Each objOption in OptionChooser.Options
            If objOption.Selected Then
                Msgbox objOption.InnerText
            End If
        Next
    End Sub

</SCRIPT>

<body>

<select size="1" name="OptionChooser" onChange="TestSub">
    <option value="0"></option>
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
    <option value="3">Option 3</option>
</select>

</body>

You know, it is hard to be humble when you’re perfect. For some reason, though, the Scripting Guys have no trouble whatsoever in being humble….


How Can I Highlight a Keyword in an HTA?

Hey, Scripting Guy! I’d like to create an HTA that, upon loading, can open a text file and then display the contents. However, I’d like a particular keyword in those contents to be highlighted. How can I do that?

-- GD

Hey, GD. You know, a lot of people at Microsoft pooh-pooh the work that the Scripting Guys do. “Take that Hey, Scripting Guy! column,” they’ll say. “Big deal. I bet a 4-year-old could write a column that good.” Well, we’d just like to say that those people are wrong. As it turns out, a 4-year-old could write a better column.

Yesterday the Scripting Son had his wisdom teeth removed. (But don’t worry; he didn’t have all that much wisdom to lose in the first place.) In turn, the Scripting Dad had to stop at the pharmacy on his way home to pick up a prescription. While waiting his turn at the counter he ran into the wife and 4-year-old daughter of a guy he used to coach with. As it turned out, the wife and daughter had walked to the drugstore, about a mile or so from their house.

The Scripting Guy and the Coach’s Wife chatted for a moment, with the Coach’s Wife noting that her daughter didn’t really enjoy the walk. “It’s such a nice day I thought a walk would be fun,” said the wife. “But the little one doesn’t seem to agree with me. She kept saying it was too far to walk.”

It was at that very moment that the little one spoke up. “Mommy, can Coach Greg give us a ride home?” she asked. “My legs are bored.”

My legs are bored! The Scripting Guys would kill to be able to write something that good.

Note. That is just a figure of speech, right? I mean, the Scripting Guys wouldn’t really kill just to be able to write a cute phrase, would they? Would they? Hello?

Oh, well; that’s not the first time we’ve been bested by a 4-year-old. We’ll go ahead and answer your question, GD. It won’t be anywhere near as cute and clever as “My legs are bored.” However, it will have a little more scripting code:

<Script Language="VBScript">
    Sub Window_Onload
        Const ForReading = 1

        Set objFSO = CreateObject("Scripting.FileSystemObject")
        Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

        strContent = objFile.ReadAll
        objFile.Close

        strNewText = "<span style='background-color: #FFFF00'>text</span>"
        strContent = Replace(strContent, "text", strNewText)
        DataArea.InnerHTML = strContent
    End Sub
</script>

<body>
    <span id = "DataArea"></span>
</body>

As you can see, we have a very simple HTA here. (If you don’t know what HTAs are, then take a look at our HTA Developers Zone.) This simple HTA consists of two parts: the <BODY> tag and a single subroutine (enclosed, as all good subroutines are, inside the <SCRIPT> tag). It goes without saying that there’s not much to the <BODY> tag; in fact, all we have there is a <SPAN> named DataArea:

<span id = "DataArea"></span>

If you’re new to HTML tagging, a <SPAN> is simply a named area of an HTML file (or an HTA file). By setting aside a portion of the document and giving it a name, we can refer to – and modify – that section of the document using a little scripting code. It should come as no surprise, then, that this is exactly what our subroutine is going to do.

Speaking of our subroutine, you might have noticed that we gave it the name Window_Onload. That’s not a name we picked because it was, say, our grandfather’s name. (In case you’re wondering, no, it wasn’t.) Instead, any subroutine that has the name Window_Onload automatically runs each time the HTA is loaded or refreshed. GD needs an HTA that, on its own initiative, goes ahead and opens a text file each time it gets started. How do we do that? We just put the file-opening code into a subroutine named Window_Onload.

Which, now that we think about it, is exactly what we did. We’d like to see a 4-year-old do that!

Inside our subroutine we start out by defining a constant named ForReading and setting the value to 1; as the name implies, we’ll use this constant when we go to open, and read, the text file. We then use these two lines of code to create an instance of the Scripting.FileSystemObject object and open the file C:\Scripts\Test.txt for reading:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt")

As soon as the file is open we use the ReadAll method to read in the entire file and store the contents in a variable named strContent:

strContent = objFile.ReadAll

And then once we have the contents safely stashed away in memory we go ahead and close the file:

objFile.Close

That part was pretty easy, wasn’t it? And don’t worry: it doesn’t get much harder, either.

We’re assuming that our text file looks something like this:

Here is some sample text taken from the file C:\Scripts\Test.txt. The idea is to take this text
and then highlight each instance of the word text. Like this one: text.

If we wanted to, we could simply set the InnerHTML property of our <SPAN> to the value of strContent; that would result in the contents of the text file being displayed in our HTA. But that’s something a 4-year-old would do. A 74- -- um, we mean a 24-year-old, like the Scripting Guy who writes this column, would go one step further: he would highlight a target word anywhere it appeared in the text. How’s he going to do that? We’re just about to show you.

The InnerHTML property is a very cool property. Why? Because it not only allows you to assign text to a <SPAN> but it also allows you to assign HTML formatting to a <SPAN>. If you want to highlight a single word using HTML tags, one way to do that is to turn that single word into a sort of mini-<SPAN>, then assign that mini-<SPAN> its own background color. For example, suppose we wanted to highlight the word text. In that case, we could use HTML tagging similar to this:

<span style='background-color: #FFFF00'>text</span>

And guess what? That’s exactly what we’re going to do. For starters, we take the preceding HTML tagging and assign it to a variable named strNewText:

strNewText = "<span style='background-color: #FFFF00'>text</span>"

We then use this line of code and the Replace function to search through the value of strContent and replace all instances of the word text with our HTML tagging (which simply displays the word text on a yellow background):

strContent = Replace(strContent, "text", strNewText)

That makes the value of strContent equal to this:

Here is some sample <span style='background-color: #FFFF00'>text</span> taken from the file C:\Scripts\Test.txt.
The idea is to take this <spanstyle='background-color: #FFFF00'>text</span> and then highlight each instance of the word
<span style='background-color: #FFFF00'>text</span>. Like this one: <span style='background-color: #FFFF00'>text</span>.

Now we can go ahead and assign the value of strContent to the InnerHTML property:

DataArea.InnerHTML = strContent

Lo and behold, not only does our HTA display the contents of the file C:\Scripts\Test.txt, but every instance of the target word is highlighted:

Is that cool or what?

We hope that answers your question, GD. Now we just need a socko-boffo ending for today’s column, something along the lines of, “Can we go now? My legs are bored.” Hmmm ….

What’s that? You say you have a suggestion: “Can we go now? Your readers are bored.” Oh, man; that was uncalled for!

On the other hand, we can’t really think of anything better, so we’ll go with that. See you all Monday.


How Can I Include Multiple Subroutines in a Single onClick Parameter?

Hey, Scripting Guy! In your HTA examples, you show us how you can click a button and cause a single subroutine to run. How can I add two or more subroutines to the onClick parameter for a button?

-- FM

Hey, FM. You know, there’s an interesting branch of cognitive psychology known as problem finding; the idea is that your ability to answer a question often hinges on how you ask the question in the first place. For example, you – and several others – want to know how to specify multiple subroutines in the onClick parameter of a button. We’ve seen people try several variations like this:

<input type="button" value="Run Button" onClick="Script_1; Script_2; Script_3">

As you’ve discovered, that doesn’t work.

So let’s put our problem-finding skills to the test and see if we can rephrase this question. (Typically the Scripting Guys don’t need to find problems; problems have a way of finding us.) Here’s the crux of the matter: do we really want to add multiple subroutines to the onClick parameter, or do we just want multiple subroutines to run any time we click the button?

If it’s the latter, then we have an answer for you:

<html>
<head>
<title>Multiple Subroutines</title>

<HTA:APPLICATION
     ID="objHTAHelpomatic"
     APPLICATIONNAME="MultipleSubroutines"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
     WINDOWSTATE="maximize"
>
</head>

<SCRIPT Language="VBScript">

Sub RunScripts
    Script_1
    Script_2
    Script_3
End Sub

Sub Script_1
    Msgbox "This is subroutine 1."
End Sub

Sub Script_2
    Msgbox "This is subroutine 2."
End Sub

Sub Script_3
    Msgbox "This is subroutine 3."
End Sub

</SCRIPT>

<body>
    <input type="button" value="Run Button" onClick="RunScripts">
</body>
</html>

Note. The preceding code is designed to be run from an HTA, an HTML Application. If you want to test the code, just copy the script, paste it into Notepad or some other text editor, and save the file using a .HTA file extension.

If you glance through the code you might notice the HTML tag for the button:

<input type="button" value="Run Button" onClick="RunScripts">

As you can see, we specify only a single subroutine (RunScripts) in the onClick parameter. Ah, but take a look at the code for the subroutine RunScripts:

Sub RunScripts
    Script_1
    Script_2
    Script_3
End Sub

There’s your answer right there. All we do inside this subroutine is call three other subroutines: Script_1, Script_2, and Script_3. That’s how we can run multiple subroutines from a single button click: we don’t put all those subroutines into the onClick parameter, we put them all into the single subroutine called by onClick.

In other words, problem-finding – and cognitive psychology – to the rescue. And to think people told us that graduate school was a huge waste of time and money: at least we got a Hey, Scripting Guy! column out of it!


How Can I Load List Box Options From a Text File?

Hey, Scripting Guy! How can I load list box options from a text file?

-- MV

Hey, MV. You would have to mention “options,” wouldn’t you? When the Scripting Guy who writes this column first joined Microsoft some five years ago, he assumed that he’d work for two or three years, cash in all his stock options, and then spend the rest of his life lazing away on the island of Tahiti. So does the fact that he’s still writing this column mean that he was a bit … naïve … when it came to how much money he’d actually make over those first few years? Oh, heavens no; he continues to write this column simply because he’d much rather work than fritter his time away in some tropical paradise.

Or so he tells himself every morning; it’s about the only way he can wrench himself out of bed.

True story. When he first started here this Scripting Guy worked for a long-time Microsoft employee who was rich: she paid cash for a $1 million house on Lake Washington. Despite that, she came to work every day; she even worked late nights and on weekends. Forget all that stuff about youth being wasted on the young; what about wealth being wasted on people who don’t know how to enjoy it?

Oh, well. For those few of us who actually have to work (which isn’t necessarily the same thing as wanting to work), here’s a very simple little HTA (HTML Application) that, on startup, reads computer names from a text file and then configures each of those computer names as an option in a list box:

<SCRIPT Language="VBScript">
    Sub Window_OnLoad
        ForReading = 1
        Set objFSO = CreateObject("Scripting.FileSystemObject")
        Set objFile = objFSO.OpenTextFile _
            ("C:\Scripts\Computers.txt", ForReading)
        Do Until objFile.AtEndOfStream
            strLine = objFile.ReadLine
            Set objOption = Document.createElement("OPTION")
            objOption.Text = strLine
            objOption.Value = strLine
            AvailableComputers.Add(objOption)
        Loop
        objFile.Close
    End Sub
</SCRIPT>

<body>
    <select size="3" name="AvailableComputers" style="width:250"></select>
</body>

Yes, it is a fairly simple little script, isn’t it? (But, then again, we only said that we had to work; we didn’t say that we had to work hard.) The script consists of two parts: a subroutine (named Window_OnLoad) that runs any time the HTA is started or refreshed, and a simple little list box that displays three options at a time (and has a width of 250 pixels):

<select size="3" name="AvailableComputers" style="width:250"></select>

As you can probably guess, this list box (christened AvailableComputers) is the list box where we’ll add the options. Notice that we start off without any options assigned to AvailableComputers.

That’s all well and good, mind you, but an empty list box isn’t especially exciting. So let’s see what we can do about that. Inside the Window_OnLoad subroutine we start off with code designed to open the text file C:\Scripts\Computers.txt:

ForReading = 1
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile
_
("C:\Scripts\Computers.txt", ForReading)

Incidentally, we’re assuming that the text file consists of nothing but computer names, with one name per line. In other words, we’re assuming Computers.txt looks something like this:

atl-fs-01
atl-fs-02
atl-fs-03
alt-fs-04

This part should be pretty straightforward. We begin by defining a constant named ForReading and assigning it the value 1; we’ll use this constant to tell the script that we want to open the file Computers.txt for reading. We create an instance of the Scripting.FileSystemObject, then call the OpenTextFile method in order to open our text file for reading.

With the file open we set up a Do Until loop that runs until we reach the end of the file (or, more technically, until the AtEndOfStream property is True). We then use this line of code to read the first line in the text file and store the value in a variable named strLine:

strLine = objFile.ReadLine

If you’ve worked with text files before all this should be very familiar to you. (If you haven’t worked with text files, then we suggest you take a look at the Microsoft Windows 2000 Scripting Guide.) But now we’re ready to do something a little more out-of-the-ordinary:

Set objOption = Document.createElement("OPTION")
objOption.Text = strLine
objOption.Value = strLine
AvailableComputers.Add(objOption)

That’s right: these four lines of code add the name of the first computer (i.e., the first line in the text file) to the AvailableComputers list box. To do that we start off by using the createElement method to create a new, “blank” option object (one with the object reference objOption):

Set objOption = Document.createElement("OPTION")

If you’re familiar with HTML then you know that each option in a list box has (at least) two properties: the option Text (the label that actually appears in the list box) and the option Value (the value assigned to that particular option). The Text and Value properties do not have to be the same; for example, you could display a label like Primary Domain Controller in the list box, but assign that same option a Value like atl-dc-01. For this particular HTA we’re fine with the Text and Value being the same; consequently, we use these two lines of code to assign the value of strLine to both of these properties:

objOption.Text = strLine
objOption.Value = strLine

After that all we have to do is call the Add method to add this new option to the AvailableComputers list box:

AvailableComputers.Add(objOption)

And that’s all we have to do; from there we simply loop around and repeat the process for the remaining lines in the text file. When we’re all done we should have an HTA that looks something like this:

A bit on the boring side, and it doesn’t actually do anything. But once you get those computer names into the list box, well, at that point the sky’s the limit.

Or at least it is for those of you who have the time to play around with things like HTAs. As for the rest of us, well, back to the salt mines.

Note. Yes, we know: money can’t buy happiness. But we’d be willing to take that chance.


How Can I Minimize or Hide an HTML Application for 30 Seconds?

Hey, Scripting Guy! How can I minimize or hide an HTA for 30 seconds?

-- MF

Hey, MF. You know, with the possible exception of baseball, the Scripting Guy who writes this column rarely claims to know much about anything. And for good reason: every time he thinks he knows something, well ….

Take your question, for example. Minimize or hide an HTA? No problem, he thought. After all, HTA’s have a property named windowStyle that can be used to configure an HTA to run minimized, maximized, or in a normal window. And if that’s not good enough for you, then you can simply set the Visible property of the HTA to False. Take your pick.

So, sure, take your pick; just don’t expect either choice to solve your problem. As it turns out, HTAs don’t have a Visible property, something this Scripting Guy would have realized had he bothered to think about this question for a second. As for the windowStyle property, well, that property does exist; however, it can be configured only at startup time. After the HTA is up and running you can no longer change the windowStyle property; that means you can’t easily switch between, say, a minimized window and a normal window.

Uh-oh.

In other words, this Scripting Guy is no genius. (Well, the Scripting Editor thinks he is. But he’s really not.)

Editor’s Note: We’re not sure where the Scripting Guy got the impression anyone thinks he’s a genius, let alone the Scripting Editor. Could it have been the baffled looks, or the comments like “What are you talking about?” and “Are you out of your mind?” We’re not sure, but he’s on vacation this week and won’t see this, so if it makes him happy we’ll let him remain in his own imaginary little world. And we’ll know that the next time we say to him “Do you have a brain?” he’s taking it as a compliment.

On the other hand, there is one advantage to being dumb: you’re not smart enough to know when you’re licked. Maybe we can’t minimize or hide an HTA, but we can do this:

<html>

<SCRIPT Language="VBScript">

    Dim intLeft
    Dim intTop

    Sub HideWindow
        intLeft = window.screenLeft
        intTop = window.screenTop
        window.moveTo -2000,-2000
        idTimer = window.setTimeout("ShowWindow", 5000, "VBScript")
    End Sub

    Sub ShowWindow
        window.moveTo intLeft,intTop
        window.clearTimeout(idTimer)
    End Sub
</SCRIPT>

<body>
    <input type="button" value="Hide Window" onClick="HideWindow">
</body>
</html>

If you run this script and click the button labeled Hide Window, the HTA will disappear. However, that’s not because we’ve minimized the window or because we’ve made the application invisible; instead, we’ve simply moved the HTA clear off the screen. After a five-second pause (a time interval you can modify as needed), we restore the window to its previous location. Not bad, huh?

Let’s see if we can figure out how this thing works. To begin with, the body of the HTA contains just one thing -- a single button labeled Hide Window:

<input type="button" value="Hide Window" onClick="HideWindow">

Click this button, and you’ll run a subroutine named HideWindow. That subroutine looks like this:

Sub HideWindow
    intLeft = window.screenLeft
    intTop = window.screenTop
    window.moveTo -2000,-2000
    idTimer = window.setTimeout("ShowWindow", 5000, "VBScript")
End Sub

As you can see, we don’t really do much in this HTA. We start out by assigning values to a pair of global variables named intLeft and intTop:

intLeft = window.screenLeft
intTop = window.screenTop

What we’re doing here is making a note of the upper (window.screenTop) left-hand (window.screenLeft) corner of the HTA window. Why do we do that? That’s easy: by knowing the location of the upper left-hand corner we’ll be able to restore the window to its current spot. After grabbing this information we then move the window off the screen:

window.moveTo -2000,-2000

How do we know this moves the window off the screen? Well, the upper left-hand corner of the screen has the coordinates 0, 0. A window with coordinates -2000, -2000 is going to be way off the screen.

With the window no longer visible this would probably be the spot where you would run whatever code you need the HTA to run. Instead of putting some phony-baloney code in here we simply set a timer that pauses the HTA for 5 seconds (5,000 milliseconds) and then calls a subroutine named ShowWindow:

idTimer = window.setTimeout("ShowWindow", 5000, "VBScript")

Note. Did someone say they didn’t understand how the timer-setting code works? If so, take a look at this Hey, Scripting Guy! column for more information.

And what does the ShowWindow subroutine do? Actually, it does two things. First, it moves the window back to its original location:

window.moveTo intLeft,intTop

And then it clears the timer:

window.clearTimeout(idTimer)

Do we really need to clear the timer? Well, it’s a pretty good idea; otherwise every 5 seconds the timer is going to fire and move the window back to its original position.

Give this a try, MF, and see if it helps; we think it will. And that’s a good point: imagine what a cool solution we could have come up with had we actually know what we were doing.


How Can I Pass Command-Line Variables to an HTA When It Starts?

Hey, Scripting Guy! How can I pass command-line variables to an HTA when it starts?

-- DM

Hey, DM. Ok, we admit it: you almost had us on this one. (Not that being able to stump the Scripting Guys is particularly hard, mind you.) We had no idea whether you could pass command-line variables to an HTA, and even less idea how we could capture those command-line variables. But then we stumbled across the commandLine property and, after a bit of hacking around, came up with a solution.

Note. If you aren’t familiar with the acronym HTA, HTA is short for HTML Application. An HTML Application provides a way for you to wrap up your scripts in a graphical user interface, complete with radio buttons, check boxes, dropdown lists and what-have-you. For more information about HTAs, check out our new HTA Developers Center.

The commandLine property simply reports back the command used to start an HTA. Because HTAs are GUI tools, they are typically started simply by double-clicking the file icon in Windows Explorer or My Computer. Thus the commandLine is nothing more than the path to the HTA file, something like this (note that the double quotes are part of the value):

"C:\Scripts\Cmdline.hta"

Now suppose you type something like this at the command prompt:

c:\scripts\cmdline.hta arg1 arg2

Guess what the commandLine property returns then? That’s right; the entire command string, with the path surrounded by double quotes and the two arguments tacked onto the end:

"c:\scripts\cmdline.hta" arg1 arg2

In other words, we can capture the command-line arguments; as you can see, both arg1 and arg2 are in the value returned by the commandLine property. All we have to do now is figure out how to tease those values out of there.

Our first thought was to remove the double quotes and then use the VBScript Split function to carve the command-line into an array. Our intention was to split the string on each blank space, which would give us an array consisting of the following elements:

c:\scripts\cmdline.hta
arg1
arg2

That worked, but there was a problem. Suppose you had a blank space in the HTA path; for example, suppose our path was C:\Scripts\My HTAs\Cmdline.hta. That “extra” blank space was a problem; if we split the string on each blank space we’d get an array that looked like this:

c:\scripts\my
htas\cmdline.hta
arg1
arg2

Not so good. Therefore, to work around that problem we invented a new rule: we had to surround our command-line arguments with double quote marks. In other words, to start our HTA we had to type something like this from the command prompt:

c:\scripts\cmdline.hta "arg1" "arg2"

Why? Well, this returns a commandLine that looks like this:

"c:\scripts\cmdline.hta"  "arg1" "arg2"

Each argument is now enclosed within double quotes, just like the path is. If we now split on the double quote mark (chr(34)) we’ll get an array that looks like this:

<blank line>
c:\scripts\cmdline.hta
<blank space>
arg1
<blank space>
arg2
<blank line>

In other words, every other line in the array has data of interest to us. Why do the other lines consist of – for all intents and purposes – nothing at all? Well, to mimic the effect replace each " in this sentence with a carriage return-linefeed and see what you get:

"c:\scripts\cmdline.hta"  "arg1" "arg2"

That’s right:

c:\scripts\cmdline.hta

arg1

arg2

Believe it or not, that’s OK. Because we have an array, we can use a For Next loop to cycle through all the items. Furthermore, we can use the Step parameter to loop through every other line. For example, suppose we have the following array:

A
B
C
D

We can start with the second element (or item 1, because the first element in an array is item 0) and set the Step value to 2. That will read the second element (B) and then skip to the fourth element (D). And that’s exactly what we’ll do in our HTA. We’re going to start our For Next loop with item 3. Why item 3? Well, item 3 is the fourth element in the array. We don’t care about any of the first three elements: they consist of a blank line, the path to the HTA file, and a blank space. Element 4 is our first argument, so we’ll start there, then skip to item 6. (Remember, item 5 will just be a blank space.) That’s how we’ll extract just the command-line arguments for the HTA.

Here’s the complete HTA code (remember to save this as a .HTA file and not a .VBS file):

<html>
<head>
<title>Command Line Agruments</title>

<HTA:APPLICATION
     ID="objTestHTA"
     APPLICATIONNAME="Command Line Agruments"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT Language="VBScript">

Sub Window_onLoad
    arrCommands = Split(objTestHTA.commandLine, chr(34))
    For i = 3 to (Ubound(arrCommands) - 1) Step 2
        Msgbox arrCommands(i)
    Next
End Sub

</SCRIPT>

<body>
</body>
</html>

The part we really care about is the Window_onLoad subroutine (a subroutine which automatically runs any time the HTA is started). The first line in our subroutine uses the Split function to create an array from the value returned by the commandLine property, using the double quote mark – chr(34) – as the split character. We then use a For Next loop starting with item 3 and continuing through the last item in the array (the upper bound of the array minus 1). In this script we simply echo back the value of each argument, though obviously once you have that value in hand you can do anything you want with it.

Just remember to surround each argument with double quotes (something you’d have to do anyway if any of those arguments included spaces). If you do that, the script should work as expected. If you don’t… well, if you’re crazy enough to try to include command-line arguments that aren’t surrounded by double quotes all we can do is wish you the best of luck (and tell you that it’s been nice knowing you).


How Can I Populate a List Box With the CNs of All the OUs in My Domain?

Hey, Scripting Guy! How can I populate a list box with the CNs of all the OUs in my domain?

-- AK

Hey, AK. That’s an interesting question, AK, and we’ll do our best to answer it. Of course, we should point out that the United States soccer team has just finished losing to the Czech Republic in the World Cup, and we’re so broken-hearted and disconsolate that we can barely type, let alone write. Therefore, if this column is poorly-written or if it doesn’t make any sense, well, you understand why.

Note. OK, granted, the Scripting Guy who writes this column doesn’t exactly consider himself a soccer fan; in fact, he spent the weekend watching college baseball on TV rather than the World Cup. In fact, he’s just pretending to be broken-hearted; that way he has an excuse in case this turns out to be a less-than-stellar column. (Usually he tries to blame lousy columns on his editor, but she always edits those parts out. But seeing as how she is a soccer fan and she is disconsolate about the U.S. loss, well, maybe he can sneak this past her.)

At any rate, under the most trying of circumstances (like we said, AK, surely you – and our editor – understand), here’s what we came up with:

<SCRIPT Language="VBScript">
    Sub Window_onLoad
        On Error Resume Next

        Const ADS_SCOPE_SUBTREE = 2

        Set objConnection = CreateObject("ADODB.Connection")
        Set objCommand =   CreateObject("ADODB.Command")
        objConnection.Provider = "ADsDSOObject"
        objConnection.Open "Active Directory Provider"
        Set objCommand.ActiveConnection = objConnection

        objCommand.Properties("Page Size") = 1000
        objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

        objCommand.CommandText = _
            "SELECT Name, ADsPath FROM 'LDAP://dc=fabrikam,dc=com'" & _
                " WHERE objectCategory='organizationalUnit' ORDER BY Name"
        Set objRecordSet = objCommand.Execute

        objRecordSet.MoveFirst
        Do Until objRecordSet.EOF
            Set objOption = Document.createElement("OPTION")
            objOption.Text = objRecordSet.Fields("Name").Value
            objOption.Value = objRecordSet.Fields("ADsPath").Value
            AvailableOUs.Add(objOption)
            objRecordSet.MoveNext
        Loop
    End Sub

    Sub ShowOUPath
        Msgbox AvailableOUs.Value
    End Sub
</SCRIPT>

<body>
    <select size="10" name="AvailableOUs" style="width:300"></select><p>
    <input id=runbutton type="button" value="Show OU Path" onClick="ShowOUPath">
</body>

This is a bit longer than most of the scripts you see in this column, so let’s take care of the easy things first. To begin with, the body of the HTA consists of just two items, a list box named AvailableOUs and a button with the label Show OU Path:

<select size="10"
name="AvailableOUs"
style="width:300"></select><p>
<input id=runbutton type="button" value="Show OU Path"
onClick="ShowOUPath">

Note. If you aren’t familiar with HTAs, well, too bad!

Um, we mean, check out the HTA Developers Center here in the Script Center. Sorry; you know, what with the U.S. losing and everything, well, we’re just not ourselves today.

Both of these controls are pretty rudimentary. The button has been configured to run a subroutine named ShowOUPath any time said button gets clicked. Meanwhile, we don’t do much with the list box other than configure it to show 10 items at a time (size="10") and to be 300 pixels wide (style="width:300"). As you can see, we didn’t even specify any items to appear in this list box. That’s by design: after all, we want to dynamically populate the list box with the names of the OUs in our domain.

Note. Yes, you did say you wanted to populate the list box with the CNs of all your OUs. We decided to go with Name simply because the CN is an optional attribute and won’t necessarily be configured for all your OUs. However, we’ll show you how to modify the code to use the CN if you so desire.

No, we’re not doing this because of the World Cup. We’re always nice like that!

As for the subroutine ShowOUPath, all we do there is echo back the ADsPath for the OU selected in the list box (as you’ll see, the corresponding ADsPath is assigned as the value for each OU in the list box):

Msgbox AvailableOUs.Value

That’s not particularly exciting, but it is useful: after all, if you know the ADsPath then you can bind directly to the OU in Active Directory.

That leaves us with the two most important aspects of this code: retrieving the list of OUs from Active Directory, and dynamically adding those OUs to a list box. We’ve included both of these tasks within a single subroutine named Window_onLoad. As you probably know, any time an HTA or Web page includes a subroutine named Winodw_onLoad, that subroutine is automatically invoked any time the HTA/page is loaded or refreshed. By placing these two tasks in Window_onLoad we ensure that each time we start the HTA we’ll conduct an Active Directory search and load an up-to-date set of OU names into the list box.

As we noted, to retrieve the list of OUs we do an Active Directory search; that’s what this big block of code is all about:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
"SELECT Name, ADsPath FROM
'LDAP://dc=fabrikam,dc=com'" & _
" WHERE
objectCategory='organizationalUnit' ORDER BY Name"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Incidentally, we won’t be able to discuss the details behind conducting an Active Directory search; that’s well beyond the scope of our daily column. If you need those whys and wherefores, take a look at our two-part Tales from the Script series Dude, Where’s My Printer?

Instead, the only thing we’ll make particular note of is the query we use to retrieve our information:

objCommand.CommandText = _
    "SELECT Name, ADsPath FROM 'LDAP://dc=fabrikam,dc=com'" & _
        " WHERE objectCategory='organizationalUnit' ORDER BY Name"

All we’re doing here is asking for the Name and the ADsPath of all the objects in the fabrikam.com domain that have an objectCategory equal to organizationalUnit. It probably goes without saying that any object with an objectCategory equal to organizationalUnit will be an OU. Oh: and because a list like this is much easier to deal with when the items are presented in alphabetical order, we’ve tacked on this clause to sort the returned data by name: ORDER BY Name.

Good point: suppose you do want to return the CN rather than the Name. That’s fine; just replace any reference to the Name attribute with the CN attribute instead:

objCommand.CommandText = _
    "SELECT CN, ADsPath FROM 'LDAP://dc=fabrikam,dc=com'" & _
        " WHERE objectCategory='organizationalUnit' ORDER BY CN"

And now – at long last – we get to the heart of the matter: adding the OUs to the list box. After we call the Execute method and run our query against Active Directory we get back a recordset consisting of the Name and ADsPath of each OU in the domain. To add these OUs to the list box we use the following block of code:

Do Until objRecordSet.EOF
    Set objOption = Document.createElement("OPTION")
    objOption.Text = objRecordSet.Fields("Name").Value
    objOption.Value = objRecordSet.Fields("ADsPath").Value
    AvailableOUs.Add(objOption)
    objRecordSet.MoveNext
Loop

What’s going on here? Well, to begin with, we set up a Do Until loop that which runs until we reach the end of the recordset (objRecordset.EOF). Inside that loop we use the createElement method to create a “blank” list box item. For this new list box item we set the Text property to the OU Name and the Value property to the OU ADsPath:

objOption.Text = objRecordSet.Fields("Name").Value
objOption.Value = objRecordSet.Fields("ADsPath").Value

Note. If you’ve decided to use the CN rather than the name, just change the code accordingly: objRecordSet.Fields("CN").Value.

If you aren’t familiar with HTML, the Text property is simply the text that appears in the list box; the Value property is, well, a value associated with the item. Although the Text and the Value can be identical, they don’t have to be. We chose to use the Name for the Text property because that gives us nice, easy-to-read names like this:

· Administration

· Finance

· Human Resources

· Research

· Sales

We then use the ADsPath as the Value; that makes it easy for someone to choose an OU, click a button, and then bind to the selected OU. The ADsPaths will look something like this:

· LDAP://ou=Administration,dc=fabrikam,dc=com

· LDAP://ou=Finance,dc=fabrikam,dc=com

· LDAP://ou=Human Resources,dc=fabrikam,dc=com

· LDAP://ou=Research,dc=fabrikam,dc=com

· LDAP://ou=Sales,dc=fabrikam,dc=com

As you can see, the ADsPath – while important to have – doesn’t make for very good labels within the list box. That’s why we use the Name as the text that appears in the list box.

After assigning values to the Text and Value properties we then call the Add method to add the new item to the list box:

AvailableOUs.Add(objOption)

Believe it or not, that’s all we have to do. We call the MoveNext method to move to the next item in the recordset and then repeat the process, adding the second OU to the list box. This continues until we’ve reached the end of the recordset and – not coincidentally – added all the OUs to the list box.

And now we really should get to work on our next column. Of course, that’s not going to be easy, knowing that the U.S. likely needs to beat Ghana and Italy in order to advance. But, hey, the show must go on, right? Seeing as how it’s annual review time, we hope our manager will appreciate how we soldier on even though we’ve been emotionally devastated.

No check that: the emotional devastation usually comes after we’ve had our review.


How Can I Position the Cursor in a Specific Text Box Each Time I Start an HTA?

Hey, Scripting Guy! How can I position the cursor in a specific text box each time I start an HTA?

-- BM

Hey, BM. You know, if the Scripting Guy who writes this column has any regrets in life it’s that he didn’t become a Theoretical Physicist Guy. Why? Well, among other things, theoretical physicists have concocted theories that completely explain the nature and makeup of the universe. You have to admit, that’s way cooler than concocting a script that can position the cursor in a specific text box each time you start an HTA.

OK, sure, if you want to get picky about it these theories really only explain about 4% of the universe. But that’s the other cool thing about theoretical physics. Is 96% of the universe missing? No problem: obviously that 96% must be composed of dark matter and dark energy, substances that, by definition, are likely undetectable. In other words, 96% of the universe is made up of substances we’ll never be able to prove even exist. Case closed!

Note. Personally, the Scripting Guy who writes this column is skeptical of dark energy and dark matter. He believes that the missing 96% is, instead, made up entirely of remote controls; at the Scripting Guy household, for example, and in the living room alone, are remote controls for the TV, the cable box, the DVD player, the VCR, and the stereo. Best of all, it’s impossible to get rid of any of them: even the so-called “universal remotes” seem to handle only 4% of all possible situations. Multiply the number of remote controls in a room by the number of rooms in a house and the number of houses in the universe and, well you get the idea.

Of course, as a Scripting Guy you can’t get away with saying that positioning the cursor in a specific text box in an HTA can only be done by using dark VBScript, code that cannot be typed on a keyboard or actually added to a script. Instead, Scripting Guys have to provide solutions that can be tested and implemented. You know, like this one:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="HTATest"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT LANGUAGE="VBScript">
    Sub Window_OnLoad
        BasicTextbox.Focus
    End Sub
</SCRIPT>

<body>
    <input type="text" name="BasicTextbox" size="30">
</body>

As you can see, there’s really not much to this. In fact, all we have here is a very simple HTA, one sporting a single item: a text box named BasicTextbox. Here’s the HTML tagging that creates that text box:

<input type="text" name="BasicTextbox" size="30">

There’s nothing special here, either, except that we gave the text box a name. That’s the key: as long as the text box has a name you can refer to it in a script. And if you can refer to that particular text box then you can do all sorts of cool things, like automatically position the cursor in the box each time the HTA loads up.

If you’re even interested in doing that sort of thing, that is.

Oh, that’s right: you are interested in doing that sort of thing, aren’t you? Well, fortunately, our HTA also includes a subroutine named Window_Onload that positions the cursor for you:

Sub Window_OnLoad
    BasicTextbox.Focus
End Sub

Again, this is almost-embarrassingly simple. All you really have to do are two things. First, make sure you give the subroutine the name Window_Onload. In an HTA (or in a Web page for that matter) any subroutine named Window_Onload automatically runs each time the HTA is started or refreshed. Window_Onload: the autoexec.bat of the HTA world.

Second, the notion of “positioning the cursor” simply means giving the control in question (in this case, the text box) the focus. That can be done just by referencing the control by name and then calling the Focus method. Hence the one line of code we truly care about:

BasicTextbox.Focus

When you start or refresh this HTA the cursor will automatically be positioned in the text box. At that point, happy typing!

Hope that answers your question, BM. Incidentally, if you happen to be a theoretical physicist we have a tip for you: if you’re still missing 96% of the universe you might check the hall closet in the home of the Scripting Guy who writes this column. To be honest, this Scripting Guy has absolutely no idea what all has been stuffed into that closet (although there are at least three basketballs, a football, several baseballs, and a Wiffle ball bat). However, if dark energy and dark matter really do exist that’s as good a place as any to start looking.


How Can I Prevent Users From Typing Anything But Numbers Into an HTA Text Box?

Hey, Scripting Guy! We have an HTA we use for data input. In one of the text boxes our users are only supposed to type numbers. How can I prevent users from typing anything except numbers in that text box?

-- KV

Hey, KV. You know that’s a pretty good question: is there a way to prevent users from typing anything except numbers into a text box? And the answer to your question is this: yes, yes there is a way to prevent users from typing anything except numbers into a text box. Thanks for the question, KV; we’ll see everyone tomorrow!

Hold on a second here; the Scripting Editor seems to have a problem with today’s column. (God forbid we dangled a participle or something!) What’s that? No, no, if KV wanted to know how to prevent users from typing anything except numbers into a text box then she would have asked how to do that.

Oh, she did not; go back and read the question again and see for yourself.

Oh. Well, she must have added that later, because we’re pretty sure that wasn’t in her original question.

At any rate, we’re back. As we were about to say (before we were so rudely interrupted), KV wants to know how to prevent users from typing anything except numbers into a text box. (Obviously.) How can she do that? To tell you the truth, we have no idea; heck, if we knew how to do things like that we’d have real jobs.

Note. In case you’ve been thinking, “I bet Microsoft only allows the best and the brightest of their employees to write the Hey, Scripting Guy! column,” well, let’s just say that you might want to think again.

The truth is, KV, we don’t know how to prevent users from typing anything except numbers into a text box. But let’s try a few things and see what happens. Nope … Nope … Close, but not quite … Ouch! that hurt … No … Ah, there we go: we did it!

Oops. False alarm; apparently W isn’t a number. Back to the old drawing board ….

Hold the phone, KV; we just might have something for you after all:

<SCRIPT Language ="VBScript">
    Sub KeyCheck
        If Window.Event.KeyCode < 48 OR Window.Event.KeyCode > 57 Then
            Window.Event.returnValue = False
        End If
    End Sub
</SCRIPT>

<BODY>
    <INPUT TYPE=text onKeyPress="KeyCheck">
</BODY>

It took a little poking around through the HTML and DHTML Reference on MSDN, but this looks like this will do the trick. As you can see, we have a very simple little HTA here consisting of a single text box and a single subroutine. (What’s that? You don’t know what HTAs even are? Then you should take a look at our HTA Developers Zone.) Let’s start off by eyeballing the code that creates the text box:

<INPUT TYPE=text onKeyPress="KeyCheck">

Nothing too fancy there: we simply use the INPUT tag to create the text box, adding the TYPE=text parameter to ensure that we get a text box. (If you’re new to HTML, the INPUT tag can also be used to add controls like buttons, checkboxes, and radio buttons to an HTA.) We then tack this parameter on to the end of the INPUT tag:

onKeyPress="KeyCheck"

What does this do? Well, any time a user presses a key while working in an HTA (or on a Web page, for that matter), the onKeyPress event is fired. This parameter simply says, “OK, HTA; here’s the deal. If the onKeyPress event gets fired and if the focus happens to be in our text box, then run the subroutine named KeyCheck.” In other words, if you’re typing something in the text box, each time you press a key the HTA will call the KeyCheck subroutine.

So then what does the KeyCheck subroutine do? Well, as you probably know, each key on the keyboard has a value assigned to it. (You can also find a list of keys – and their corresponding values – on MSDN.) The first thing we do in this subroutine is check to see if the KeyCode (which happens to be a property of the onKeyPress event) is less than 48 or greater than 57:

If Window.Event.KeyCode < 48 OR Window.Event.KeyCode > 57 Then

Why would we do a crazy thing like that? To answer that, take a look at the values assigned to keyboard keys 0 through 9:

Key | Value
0 | 48
1 | 49
2 | 50
3 | 51
4 | 52
5 | 53
6 | 54
7 | 55
8 | 56
9 | 57

In other words, if a key has a value of 48 through 57, then it has to be one of the numbers 0 through 9. By extension, that means that any key with a value less than 48, or any key with a value greater than 57, can’t be one of the numbers 0 through 9. By checking the value of the KeyCode property we can determine which key the user typed.

Now, if the user types a number between 0 and 9 that’s fine; in that case we don’t do anything except exit the subroutine. However, let’s assume the user has pressed a key that isn’t one of the numbers 0 through 9. For example, suppose the user types x, which has a value of 120. What happens then?

This is what happens then:

Window.Event.returnValue = False

Interestingly enough, events have all sorts of properties associated with them, including returnValue. What does the returnValue property do? Well, when set to True (the default value) it returns the value of the event; in this case, that means it will return the fact that the user pressed a key on the keyboard. However, if returnValue is set to False then the event value is not returned; in a sense, it’s as though the event never occurred. If a user presses any key other than one of the numbers 0 through 9, we set returnValue to False and the event value is discarded; that means that no character gets typed into the text box.

Confused? Give it a try and see for yourself. If you type any of the numbers between 0 and 9 (inclusive) those values will appear in the text box. But if you type anything other than those numbers ….

Anyway, we hope that helps, KV. And we apologize for the confusion at the beginning of today’s column; we had no idea that we were expected to explain how to prevent users from typing anything except numbers into a text box. Fortunately, the Scripting Editor was on the ball, and thanks to her diligence we ended up doing a bunch of research and hard work that we hadn’t planned on doing, and on what could end up being the only nice day we ever get here in Redmond. (As we sit here typing, it’s 70 degrees and sunny.) But duty is far more important than a sunny day in May, isn’t it Scripting Editor?

Note. Can we tell that it’s 70 degrees and sunny just by looking out our window? You bet we can. Or at least we could, if we actually had a window. In case you’re wondering, the Scripting Guy who writes this column (despite having single-handedly invented the Script Center) doesn’t have a window office. Interestingly, the Scripting Editor, whose claim to fame is that she … did … something … at some point in her life … does have a window office. But, then again, she deserves a window office.

Come to think of it, she deserves a lot of things. Wouldn’t you agree, Scripting Editor?

Editor’s Reply: Absolutely. Probably not anything the Scripting Guy who writes this column is thinking of, but yes, the Scripting Editor deserves a lot of things – especially her window office.


How Can I Print Just the Text Area of an HTA?

Hey, Scripting Guy! I have an HTA where information gets written to a text area. How can I print just the information found in that text area?

-- DS

Hey, DS. We want you to level with us here: are the Scripting Guys dead? We ask that because we saw the movie The Sixth Sense a few years ago and we understand how this life/death thing works. If you’re familiar with The Sixth Sense, you’ll recall that there were all these eerie happenings and coincidences which could only be explained by one thing: the protagonist (Bruce Willis) was dead. That made for a nifty little movie, only now we Scripting Guys seem to be involved with a bunch of eerie happenings ourselves; after all, in the span of three days we received two emails from people asking these very same question: how can I print just the text area of an HTML Application (HTA)? Coincidence? Let’s hope so; otherwise there can be only one explanation: the Scripting Guys are dead.

Note. Hey, don’t worry about us. After all, one of the nice things about being a Scripting Guy is that it doesn’t matter much whether we’re dead or alive: our days go pretty much the same either way.

Of course, the premise of The Sixth Sense is that even though Bruce Willis was dead he refused to acknowledge it. Taking a page from Bruce’s book, we’ve decided to go ahead and answer your question as if we were are still alive, too. And we were able to come up with an answer for you, even if it was somewhat less-than-elegant. For some reason we had the notion that there was a very straightforward way to print just the information found in a text area, but when we went looking for that answer we couldn’t find it. After a brief but fruitless search we gave up and came up with a workaround that accomplishes the same thing. (And, yes, had we been alive we might have had the energy to search a little harder. But we suppose you have to expect some drawbacks to being dead.)

At any rate, here’s a very simple HTA that contains just two items: a text area named DataArea and a button labeled Print. Any time you click the Print button the HTA saves the information in the text area to a temporary file and then uses the Shell.Application object to print that file. Here’s what the HTA code looks like:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="HTA Test"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<SCRIPT LANGUAGE="VBScript">

    Sub PrintText
        strText = DataArea.Value

        Set objFSO = CreateObject("Scripting.FileSystemObject")
        strFileName = "temporary_print_file.txt"
        Set objFile = objFSO.CreateTextFile(strFileName)
        objFile.Write strText
        objFile.Close

        Set objFile = objFSO.GetFile(strFileName)
        strPath = objFSO.GetParentFolderName(objFile)

        Set objShell = CreateObject("Shell.Application")
        Set objFolder = objShell.Namespace(strPath)
        Set objFolderItem = objFolder.ParseName(strFileName)
        objFolderItem.InvokeVerbEx("Print")

    End Sub

</SCRIPT>

<body>
    <textarea name="DataArea" rows=5 cols=70></textarea><p>
    <input id=runbutton  type="button" value="Print" name="run_button" onClick="PrintText">
</body>
</html>

Note. Before we begin explaining how this all works we should note that we won’t discuss the code involved in creating the HTA itself; our focus will be solely on the script that prints the information found in the text area. If you need some background information on creating HTAs then you might check out our HTA Developer’s Center.

So how do we print the information found in the text area? Well, we start by grabbing the information found in the text area and storing it in a variable named strText:

strText = DataArea.Value

After we’ve done that we encounter a block of code that creates an instance of the FileSystemObject and provides a file name (temporary_print_file.txt) for our temporary print file. We then call the CreateTextFile method to create this file (by default it will be created in the same folder as the HTA itself). As soon as the file has been created we call the Write method to write the value of strText to the file, then close the file. All that takes place here:

Set objFSO = CreateObject("Scripting.FileSystemObject")
strFileName = "temporary_print_file.txt"
Set objFile = objFSO.CreateTextFile(strFileName)
objFile.Write strText
objFile.Close

What did that gain us? Well, we’ve now taken all the information found in the text area and saved it to a separate text file. That means that, in order to print just the information in the text area, we don’t have to do anything any more complicated than simply printing the text file. And that’s what we do here:

Set objFile = objFSO.GetFile(strFileName)
strPath = objFSO.GetParentFolderName(objFile)

Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(strPath)
Set objFolderItem = objFolder.ParseName(strFileName)
objFolderItem.InvokeVerbEx("Print")

Yes, that looks a tiny bit complicated, but that’s primarily because of the eccentricities of the Shell object: in particular, the Shell object requires us to take a slightly roundabout path before we can bind to – and then print – the file. But don’t let the looks deceive you: if you’ve seen The Sixth Sense then you know that things aren’t always what they seem.

In other words, we first use the GetFile method to bind the FileSystemObject to our temporary file. After that we call the GetParentFolderName method to determine the path to the folder where the temporary file lives (for example, if our temporary file is named C:\Scripts\temporary_print_file.txt then the parent folder will be C:\Scripts). That’s what the first two lines of code do:

Set objFile = objFSO.GetFile(strFileName)
strPath = objFSO.GetParentFolderName(objFile)

Once the folder path is stashed away in the variable strPath we can then create the Shell.Application object and use the Namespace method to bind to the folder. In turn, we can call the ParseName method on that folder, passing as the sole parameter the variable strFileName (which is storing the name of the temporary file for us). That’s somewhat-goofy syntax, but the net result is that by binding to a folder and then passing ParseName the name of a file within that folder we get a FolderItem object pointing directly to our text file. Is that good? You bet it is: once the Shell object is bound to the file we can then use the InvokeVerbEx method to call any of the commands that appear on the context menu of that file.

Is that good? Of course. If you right-click a .txt file, one of the commands available to you is Print; if you then click Print the file will automatically be printed to the default printer. As we noted, the InvokeVerbEx method enables us to run any of the commands found on the context menu of a file. To call the Print command, and thus automatically print the file to the default printer, all we need to do is execute this line of code:

objFolderItem.InvokeVerbEx("Print")

Note. What if you don’t like the way the text file prints out? In that case, try saving the file as temporary_print_file.htm and see if you like that printout any better.

That’s all we need to do. Once the file has been sent to the printer we’re home free.

Pretty cool, huh? We have to tell you, it’s scripts like this one that make you glad to be alive.

Well, for those of you who are alive, that is.


How Can I Read the Individual Lines in a Multi-Line Text Box?

Hey, Scripting Guy! In an HTA, how can I read the individual lines in a multi-line text box?

-- KM

Hey, KM. You know, to be perfectly honest the Scripting Guy who writes this column has always had a grudging admiration for people who do totally ridiculous things and yet somehow manage to get away with them. For example, in the past couple weeks a large amount of the spam mail he has received has been exactly the same: it purports to be a “secret and confidential” message from the CEO of some company. These emails were obviously not intended for the Scripting Guy who writes this column; instead, the CEOs must have inadvertently addressed them to the Scripting Guy rather than their fellow inside traders. (We hate when that happens: you sit down to type kenmyer@fabrikam.com and you end up typing pilar.ackerman@contoso.org instead.)

As it turns out, these secret and confidential messages were all supposed to clue the CEOs’ cronies in on some incredible deals: thanks to a leveraged buyout/dramatic breakthrough/government contract the stock of some company no one has ever heard of is about to go through the roof. And now that you have this inside tip, all you need to do is buy the stock, sit back, and wait for the money to roll in. Thank goodness CEOs don’t know how to use email, eh?

Note. Interestingly enough, these CEOs all seem to have email names like way_2_cool_dude. But maybe if you’re the CEO of some major company you are a way too-cool dude.

The best part is that the company mentioned in the email is a real company and one whose stock is priced very low. We assume that the spammer must own a bunch of shares of this company’s stock, and is trying to entice as many people as he or she can to buy additional shares. In turn, that will drive the price up, at which point the spammer likely sells their shares and starts over again with another company. We don’t know for sure if that’s what’s going on; after all, maybe way_2_cool_dude really is the CEO of some company. But that sounds like a pretty good way to scam people. (Hmmm … wonder if anyone has patented that idea ….) Admittedly, it’s hard to believe that literally scores of CEOs would accidentally send you secret and confidential emails on the very same day. But, then again, if you’ve ever seen the people who run some of our major companies, well ….

At any rate, it all sounds ridiculous, but apparently it works, and works well.

Speaking of which, we Scripting Guys are well aware that there’s no way to read the individual lines in a multi-line text box. However – and please keep this under your hat, as it’s both secret and confidential – we happen to know of a way that can help you achieve the same effect. Just between us, you might want to consider stocking up on HTML Applications (HTAs); when word of this leaks out the number of people writing HTAs is going to go through the roof (which means you might want to buy stock in roofing companies while you’re at it):

<SCRIPT LANGUAGE="VBScript">

Sub TestSub
    strText = MyTextArea.Value
    arrLines = Split(strText, vbCrLf)
    For Each strLine in arrLines
        Msgbox strLine
    Next
End Sub

</SCRIPT>

<body>
    <textarea name="MyTextArea" rows=5 cols=70></textarea><p>
    <input type="button" value="Run Script" onClick="TestSub">
</body>

As you can see (and only you; remember, this is secret and confidential) this is an extremely simple HTA, consisting primarily of a multi-line text box named MyTextArea (technically, a multi-line text box is known as a text area) and a button labeled Run Script. The idea is that you type some information into the text area and then click the button; when the button is clicked it then runs a subroutine named TestSub. In turn, that subroutine reports back the value of each individual line in the text area.

By the way, here’s the HTML tagging that puts a text area (5 rows high by 70 columns wide) and a button in our HTA:

<textarea name="MyTextArea" rows=5 cols=70></textarea><p>
<input type="button" value="Run Script" onClick="TestSub">

What’s that? Well, you’re right: we did say that there was no way to read individual lines in a multi-line text box, didn’t we? And that’s true; there isn’t. Therefore, we’re going to cheat a little here. We’re assuming that after you type information into the text box you then press ENTER to go to the next line. In other words, you’re really typing in a list of information as opposed to a paragraph of information For example, maybe each line in your text box consists of a user name:

Ken Myer
Pilar Ackerman
Jonathan Haas

If that’s true, then we can separate the individual lines (that is, the individual user names) by using the following subroutine:

Sub TestSub
    strText = MyTextArea.Value
    arrLines = Split(strText, vbCrLf)
    For Each strLine in arrLines
        Msgbox strLine
    Next
End Sub

What we’re doing here is first grabbing all the text in the text box and stashing it in a variable named strText; that’s what this line of code is for:

strText = MyTextArea.Value

After we’ve done that we use the VBScript Split function to convert the data in strText to an array named arrLines:

arrLines = Split(strText, vbCrLf)

As you can see, we pass two parameters to the Split function: the value we want to convert to an array and the character we want to “split” on. (In other words, which character do we use to mark the end of one item in the array and then beginning of the next item?) Because we pressed the ENTER key after each line, we want to split on the carriage return-linefeed character, something we specify using the VBScript constant vbCrLf.

What will that do for us? That will give us an array named arrLines, consisting of the following items:

Ken Myer
Pilar Ackerman
Jonathan Haas

And you’re absolutely right: that’s an array in which each item represents an individual line in the text box.

At this point, of course, you can do anything you want with this information. Just for the sake of doing something we simply set up a For Each loop and echo back the value of each line in the text box:

For Each strLine in arrLines
    Msgbox strLine
Next

We’re assuming you’ll come up with a task that’s a little more interesting, and a little more useful.

Incidentally, you need the For Each loop only if you want to read every line in the text box. What if you wanted to read only, say, line 2? No problem. After all, each line is just an element in an array; to get the value of a specific line all we have to do is reference the appropriate index number. Because the first item in an array has the index number 0, that means the second item (the second line) has an index number of 1. This single line of code echoes back the value for the second line in the text box:

Msgbox arrLines(1)

Again, keep in mind that all this works only if you’ve pressed the ENTER key at the end of each line. Suppose you don’t do that, suppose you type in a paragraph that looks like this:

This is a paragraph typed into a text area. Instead of typing ENTER
at the end of each line we simply let the text wrap. That means there’s
really only one line in this text box.

If you run click the Run Script button the subroutine will simply echo back the entire paragraph; that’s because, as far as the subroutine is concerned, there’s only one line in the text box. Is it possible to somehow parse each line in the text box without using the ENTER key? Well, we can’t say with 100% certainty that it can’t be done. But for all practical purposes, no, it can’t be done.

Why not? Well, for one thing, suppose you had a text box that resized itself any time the HTA was resized. In that case the value of each “line” would change depending on the width of the text box. Of course, even in this case the vbCrLf trick would still work because we’re splitting on the carriage return-linefeed, regardless of how many lines appear to be in the text box.

Not a perfect solution by any means, but we hope it helps, KM. Meanwhile, now that people know how to read individual lines in a text area, well, the Scripting Guy who writes this column is going to just sit back and wait for the money to start rolling in.

Note. Well, no, he’s not exactly sure how he’s going to make money off this. Why, do you think he should have thought of that before writing the column and trying to pull off a scam like this one?


How Can I Remove an Item from a List Box in an HTA?

Hey, Scripting Guy! In an HTA, how can I remove an item from a list box?

-- LW

Hey, LW. You know, we Scripting Guys are forward-thinking guys and, as such, we never look back. Did we put an item in a list box? If so, there must have been a darn good reason for that and we’d never even consider removing it. What’s done is done, you know; we can’t spend all our time reliving past mistakes. (Trust us: it would take a long time for the Scripting Guys to relive all their past mistakes.)

Most people aren’t like the Scripting Guys, however. (Hey, who said “thank goodness”?) On top of that, there are plenty of valid reasons for wanting to remove options from a list box. For example, suppose you have an HTA (an HTML Application) that lists computer names; that HTA might allow you to select a computer and then perform some operation against it. After finishing the task, you might want to remove that computer from the list box; that way there won’t be any confusion about which computers you’ve used and which ones you haven’t. (This isn’t necessarily the best way to handle such a task, but it’ll do as a simple example.)

So how do you remove an option from a list box? Well, here’s an HTA that features a list box with 8 items. When you click the Clear List Box button, option 7 will be removed from the list box:

<html>
<head>
<title>Clear a List Box</title>
</head>

<SCRIPT Language="VBScript">

Sub ClearListbox
    For Each objOption in AvailableOptions.Options
        If objOption.Value = "7" Then
            objOption.RemoveNode
            Exit Sub
        End If
    Next
End Sub

</SCRIPT>

<body bgcolor="buttonface">

<select size="8" name="AvailableOptions" style="width:400" >
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
<option value="4">Option 4</option>
<option value="5">Option 5</option>
<option value="6">Option 6</option>
<option value="7">Option 7</option>
<option value="8">Option 8</option>
</select>

<p>
<input id=runbutton  class="button" type="button" value="Clear List Box"
name="run_button"  onClick="ClearListbox"><p>

</body>
</html>

We won’t spend much time talking about the code for creating an HTA; if you aren’t familiar with HTAs and what they can do, you might take a look at Part 1 in our HTA tutorial series. For today’s column, we’re going to concentrate on the subroutine named ClearListbox, the code that actually removes option 7 from the list box:

Sub ClearListbox
    For Each objOption in AvailableOptions.Options
        If objOption.Value = "7" Then
            objOption.RemoveNode
            Exit Sub
        End If
    Next
End Sub

Admittedly, we’re using a kind of brute-force method in order to grab – and remove – option 7, but that should be fine; unless you have tens of thousands of options in your list box your code will run plenty fast. When we say we’re using a brute-force method we simply mean this: we’re grabbing a list of all the items in the list box and then examining each one to see if the Value is equal to 7. This line of code retrieves a collection of all the items in the list box named AvailableOptions:

For Each objOption in AvailableOptions.Options

And this line of code checks each item to see if the Value is equal to 7:

If objOption.Value = "7" Then

And what if the value is equal to 7? In that case, we call the RemoveNode method to remove that option from the list box. And then, having done what we set out to do, we use this line of code to exit the subroutine:

Exit Sub

After all, having already found and removed the option in question, there’s no sense in looping through the remainder of the collection.

Keep in mind that this only works for removing items from a list box. We’re experimenting with a modified version that will loop through your life and remove items (and mistakes) from there, but that might take awhile. We’ll keep you posted.


How Can I Set Scriptomatic So That It Automatically Outputs to a Text File?

Hey, Scripting Guy! How can I set Scriptomatic so that it automatically outputs to a text file?

-- GD

Hey, GD. Well, all you have to do is – wait a second: you want to make changes to the Scriptomatic? Oh, GD: say it isn’t so. Would you paint a hat on the Mona Lisa? Would you give the Venus de Milo a tattoo? Would you completely redo Frank Gehry’s Experience Music Project building in Seattle?

OK, we’d redo the last one, too. But, GD, the Scriptomatic is a classic, and you simply don’t mess with the classics.

Oh, what the heck: it’s not that special. Here’s what you need to do:

First of all, it’s a good idea to download a copy of the Scriptomatic; thanks to our years of experience we’ve learned that it’s difficult to modify a script unless you actually have a copy of that script. Once you have a Scriptomatic to call your own it’s probably a good idea to make a copy of it before you start messing around with the code. That makes it easier to start all over again just in case something goes horribly wrong.

Not that the Scripting Guys would know anything about things going horribly wrong, mind you.

Next, you need to open the Scriptomatic in Notepad or some other text editor. (As if there was any text other than Notepad!) When you’ve done that, search for this line of code:

g_strOutputFormat        = "CmdLine"

The variable g_strOutputFormat keeps track of Scriptomatic’s current output format. As you can see, the initial output format is set for the command prompt. To change that to text file output simply set the value of g_strOutputFormat to PlainText:

g_strOutputFormat        = "PlainText"

Technically that’s all you have to do: if you save the file and start Scriptomatic it will default to outputting information to a text file.

The only problem is that, at first glance, it won’t look like it’s using Plain Text as its default output format. That’s because we haven’t modified the default settings for the Output Format radio buttons:

Everything will work, but it’s definitely a bit weird to have the output format seemingly set for Command Prompt yet actually going to a text file.

So can we fix that as well? Of course we can. All we have to do is search for these two lines of code; you’ll find them near the bottom of the Scriptomatic:

<tr><td><span
style="font-size:8pt;">Command
Prompt</span></td><td>
<input type="radio" name="output"
onClick="SetOutputFormat('CmdLine')" CHECKED></td><tr>
<tr><td><span style="font-size:8pt;">Plain
Text    </span></td><td>
<input type="radio" name="output"
onClick="SetOutputFormat('PlainText')"></td></tr>

Do you see the CHECKED parameter added to the Command Prompt <input> tag? That parameter means that we want the Command Prompt radio button to be selected by default. Of course, now we no longer want Command Prompt to be selected by default; instead, we want Plain Text selected by default. So how do we fix that? You’re absolutely right: we remove the CHECKED parameter from the Command Prompt <input> tag and add it to the Plain Text tag instead:

<tr><td><span
style="font-size:8pt;">Command
Prompt</span></td><td>
<input type="radio" name="output"
onClick="SetOutputFormat('CmdLine')" ></td><tr>
<tr><td><span style="font-size:8pt;">Plain
Text    </span></td><td>
<input type="radio" name="output"
onClick="SetOutputFormat('PlainText')"
CHECKED></td></tr>

Save the file, start Scriptomatic, and you’re in business:

Incidentally, while we don’t encourage you to paint a baseball hat on the Mona Lisa (at least not the actual Mona Lisa) we definitely encourage you to modify and customize the Scriptomatic all you want. We’d love to hear what you’ve done with your copy of the Scriptomatic. If you’ve enhanced the coding capabilities, added a new language, painted flames on the side, or otherwise souped-up the old Scriptomatic, drop us a line at scripter@microsoft.com and let us know what you did.


How Can I Specify a Window Size for My HTA?

Hey, Scripting Guy! How can I specify a particular window size for my HTA?

-- RN

Hey, RN. We can’t speak for Dear Abby, Ann Landers or any other daily columnist, but the Scripting Guys are always on the lookout for a question that can be answered using a single line of code. That’s the Holy Grail for daily columnists: one and done. And, to the best of our knowledge, neither Dear Abby nor Ann Landers have ever answered a question using just a single line of code. It’s always been our dream to beat them to the punch.

And now we have. How can you specify a particular window size for an HTA? Here’s how:

window.resizeTo 400,250

There you go: add that line of code to your HTA and the next time you start the thing the window will automatically be sized to 400 pixels wide by 250 pixels high.

What’s that? Well, OK, sure, if you want to be picky about it. Admittedly, you can’t just add that line of code anywhere within your HTA; instead, you’ll need to create a Window_onLoad subroutine and place the code inside there:

Sub Window_onLoad
    window.resizeTo 400,250
End Sub

As you probably know, Window_onLoad is a special subroutine designed to run any time the HTA starts up or is refreshed. If you place the resizeTo method inside Window_onLoad then that line of code will execute each time the HTA starts. In turn, that means that the window will be appropriately sized each time the HTA starts.

Here’s a more “complete” HTA, one that does nothing other than show up in a window 400 pixels by 250 pixels:

<SCRIPT LANGUAGE="VBScript">

    Sub Window_onLoad
        window.resizeTo 400,250
    End Sub

</SCRIPT>

<BODY>
</BODY>

Simply copy the code, paste it into Notepad, and then save the file with a .HTA file extension. If you aren’t sure what happens then, well, you might want to take a look at Part 1 in our series on creating HTAs. (Which, in case you’re wondering, is short for HTML Applications.)

So, yes, technically, we really needed to add three lines of code in order to get this to work:

Sub Window_onLoad
    window.resizeTo 400,250
End Sub

But do us a favor, OK? The next time Ann Landers asks, tell her we answered this question with just a single line of code. We can’t wait to see the look on her face when she hears that.


How Can I Specify the Tab Order in an HTA?

Hey, Scripting Guy! How can I specify the tab order in an HTA?

-- MJ

Hey, MJ. This is the time of year when the greatest and most-anticipated sporting event on the planet takes place, and we know that most of you are dying to hear what the Scripting Guy who writes this column has to say about that. Well, here’s what he has to say about that: he thinks you’d be crazy not to take part in the 2007 Winter Scripting Games.

What’s that? The Super Bowl? Sure, that’s OK … we guess. But unlike the 2007 Winter Scripting Games you’re not allowed to participate in the Super Bowl. And, as far as we know, the Super Bowl isn’t awarding out Dr. Scripto bobblehead dolls to 250 lucky winners, either.

And they call the Super Bowl a major sporting event. Ha!

At any rate, we hope you’ll enter at least one event in the Scripting Games; after all, that’s the only way to maybe get your hands on a Dr. Scripto bobblehead. (Dr. Scripto bobbleheads are not available in stores … probably for good reason.) With Advanced and Beginners divisions in both VBScript and Windows PowerShell this year’s Games have a little something for everyone. In fact, if you score at least 60 points in any one division you’ll receive a Certificate of Excellence signed by the Scripting Guys. Do you have any idea how much something like that is worth?

Well, true. But if you flip the thing over you can use the back as a piece of scratch paper.

As a warm-up for the Games let’s see if we can figure out how to modify the tab order in an HTA. To do so, we need to first take a look at a simple little HTA, one that features three text boxes and a button:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="HTATest"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<body>
    Box 1 <input type="text" name="BasicTextbox1" size="30"><P>
    Box 3 <input type="text" name="BasicTextbox3" size="30"><P>
    Box 2 <input type="text" name="BasicTextbox2" size="30" ><P>

    <input id=runbutton type="button" value="Run Script">
</body>

Onscreen this simple little HTA looks like this:

By default, tab order in an HTA is based on the location of the elements on a page; in this case, and assuming the cursor is in Box 1, the tab order is as follows:

· Box 1

· Box 3

· Box 2

· The Run Script button

Most of the time that’s going to be just fine. However, if you start using fancy, multi-column layouts you might not like the default tab order; for example, hitting the TAB key might cause the focus to move horizontally when you’d prefer that the focus move vertically. In our sample HTA, we’ve admittedly set up an unusual scenario: for demonstration purposes, we’ve placed Box 2 under Box 3. Ideally, if we have the cursor in Box 1 and we hit the TAB key we want the focus to move to Box 2 not Box 3. But, short of changing our layout, how can we do that?

Here’s how:

<html>
<head>
<title>HTA Test</title>
<HTA:APPLICATION
     ID="objTest"
     APPLICATIONNAME="HTATest"
     SCROLL="yes"
     SINGLEINSTANCE="yes"
>
</head>

<body>
    Box 1 <input type="text" name="BasicTextbox1" size="30" tabIndex=1><P>
    Box 3 <input type="text" name="BasicTextbox3" size="30" tabIndex=3><P>
    Box 2 <input type="text" name="BasicTextbox2" size="30" tabIndex=2><P>

    <input id=runbutton type="button" value="Run Script">
</body>

Sure, at first glance it doesn’t appear as though we’ve done anything. (Which, when it comes to the Scripting Guys, is usually a pretty safe assumption.) Ah, but take a closer look at our text box tags:

Box 1 <input type="text" name="BasicTextbox1" size="30" tabIndex=1><P>
Box 3 <input type="text" name="BasicTextbox3" size="30" tabIndex=3><P>
Box 2 <input type="text" name="BasicTextbox2" size="30" tabIndex=2><P>

There’s your answer right there, MJ. If we want to specify a particular tab order all we have to do is add the tabIndex property to our page elements and then number each element accordingly. With this revised HTA, and with the cursor in Box 1, pressing the TAB key will cause the focus to jump to Box 2; pressing TAB again will then cause the focus to jump back up to Box 3. Cool, huh?

You know, that’s a good question: what will happen if the cursor is in Box 3 and we press TAB? Well, in that case, we’ve cycled through all the items that have a specified tabIndex. Therefore, the focus moves to the first item that doesn’t have a tabIndex; in this example, that’s going to move the focus to the Run Script button.

Now, that might be good or it might not be good; after all, perhaps you don’t want the user to ever be able to tab to the button. That’s fine; all you have to do is add the tabIndex property to the button tag and set the value of tabIndex to -1:

<input id=runbutton type="button" value="Run Script" tabIndex=-1>

What’s the point of that? This is the point of that: items that have a tabIndex of -1 will be excluded from the tab order.

And yes, as a matter of fact you can programmatically assign and reassign the tab order. Why would you want to do that? Well, maybe you have a checkbox and, if the user checks the box, you’d like to tab them to a particular section of your HTA. If they don’t check the checkbox then you might want to tab them to a different section of the HTA. That’s fine; here’s a sample subroutine that – by setting the tabIndex to -1 – removes an element from the tab order:

Sub TestSub
    BasicTextbox2.tabIndex = -1
End Sub

To return the element to the tab order just assign a new value to the tabIndex. The nice thing here is that you can change the tabIndex property for one element without having to re-index all the other elements. For example, suppose you have items with tabIndexes of 1, 2, 3, 4, and 5. You run a script that sets the tabIndex of item 3 to -1; in effect, that removes this item from the tab order, and leaves you with tabIndexes of 1, 2, 4, and 5. Is that going to be a problem? Nope; if the cursor is in item 2 and you press TAB it will simply – and correctly – move the focus to item 4. The fact that there’s no item 3 won’t matter.

Note. Because of that, instead of numbering items 1, 2, and 3 you might number them 10, 20, and 30. That way you can easily insert a new item – say, 25 – without having to re-index everything.

And, of course, you can start your HTA with the focus on a particular element by using a Window_OnLoad subroutine and calling the Focus method:

Sub Window_OnLoad
   BasicTextbox1.Focus
End Sub

Nice.

In case you’re wondering, the Scripting Guy who writes this column did watch the Super Bowl; in fact, he always watches the Super Bowl. However, he doesn’t believe that the Super Bowl is the greatest and most exciting sporting event in the planet. Instead, that title is reserved for March Madness, the NCAA men’s basketball tournament. The fact of the matter is that nothing can come even close to matching the NCAA basketball tournament for sporting thrills and chills.

What’s that? World Cup soccer? No, we said greatest and most exciting sporting event. What does soccer have to do with sports?

Note. It’s up to you, but there’s probably no point in sending irate emails to the Scripting Guy who writes this column. After all, if he doesn’t like soccer by now he’s never going to like soccer. But don’t feel bad soccer fans: he doesn’t like hockey, boxing, car racing, the NBA, or rhythmic gymnastics, either.

And yes, we could go on and on. But, as a courtesy to our readers, we won’t.


How Can I Start an Application From an HTA?

Hey, Scripting Guy! Is there an alternative to the Wscript.Shell command for HTAs? I need to run an application and specify the file to open.

-- DL

Hey, DL. Yes, we do know of an alternative to the Wscript.Shell command that will work in HTAs, and we’ll show you that in a minute. Before we do that, however, we should note that you actually can use the Wscript.Shell object within an HTA. This is a point that often creates confusion: because you can’t use certain commands – such as Wscript.Echo and Wscript.Sleep – within an HTA people assume you can’t use any WSH commands in an HTA.

First things first: why can’t you use Wscript.Echo and Wscript.Sleep in an HTA? Well, those methods are properties of the Wscript object, and you can’t create an instance of the Wscript object. Instead, the Wscript object is automatically created – and only created – when you run Windows Script Host (that is, Wscript.exe or Cscript.exe). That’s why this is a perfectly valid script:

Wscript.Echo "Hey."

Notice that we didn’t create the Wscript object; instead, it was created for us when we invoked Windows Script Host.

But that’s just for the Wscript object. There are other WSH objects that you can create, including the Shell object. For example, here’s a simple little HTA that creates the Wscript.Shell object and then runs Notepad.exe (opening the file C:\Scripts\Test.txt along the way):

<html>
<head>
<script language="VBScript">

    Sub RunProgram
        Set objShell = CreateObject("Wscript.Shell")
        objShell.Run "notepad.exe c:\scripts\test.txt"
    End Sub

</script>
</head>

<body>
<button onclick="RunProgram">Run Program</button> <p>
</body>
</html>

As you can see, this is about as simple an HTA as you can get: it consists entirely of a button that, when clicked, runs a subroutine named RunProgram. And take a look at the code for RunProgram:

Sub RunProgram
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run "notepad.exe c:\scripts\test.txt"
End Sub

There it is: we create an instance of the Wscript.Shell object and then call the Run method. And in doing so we pass Run a single parameter: the executable file name (notepad.exe) followed by the path to the file we want to open. That’s all we have to do.

Incidentally, as long as you run this in an HTA everything will work just fine. If you try to run it in an HTML file (that is, a file with a .htm file extension) you’ll be presented with a message box warning you that an ActiveX control is trying to run on the page. At that point you’ll have to click Yes to allow the subroutine to create the Shell object and then run. This is due to the fact that WSH objects are considered “unsafe for scripting.”

Note. Yes, it sounds a bit weird that scripting objects aren’t considered safe for scripting. But that’s because Internet Explorer uses a different script host and a different security model than WSH. Fortunately, HTAs use a different security model than Internet Explorer, which means you won’t encounter this problem when creating the Shell object within an HTA.

Now, what about that alternative? Well, if for some reason you don’t want to use the Wscript.Shell object then you can use the Windows Shell object instead. This HTA will also start Notepad and open the file C:\Scripts\Test.txt:

<html>
<head>
<script language="VBScript">

Sub RunProgram
    Const NORMAL_WINDOW = 1
    Set objShell = CreateObject("Shell.Application")
    objShell.ShellExecute "notepad.exe", "c:\scripts\test.txt", , , NORMAL_WINDOW
End Sub

</script>
</head>

<body>
<button onclick="RunProgram">Run Program</button> <p>
</body>
</html>

To be honest, we don’t see any real advantage to using the Windows Shell object as opposed to the Wscript.Shell object: both objects do pretty much the same thing. However, if you’d like to play around a bit with the Windows Shell, check out the documentation on the ShellExecute method. The important thing is this: if you want to start an application from your HTA, either approach will work.


How Can I Temporarily Pause a Script in an HTA?

Hey, Scripting Guy! How can I temporarily pause a script in an HTA?

-- TJ

Hey, TJ. You know, throughout history people have spent an enormous amount of time and energy searching for the meaning of life. The Scripting Guys have never taken part in this quest. Why not? Well, laziness aside, we know that it doesn’t matter: even if you did find the meaning of life, no one would care. No one wants to know why we exist; instead, they – like you – just want to know how the heck they can pause a script embedded in an HTML Application (HTA). That’s the cause the Scripting Guys have dedicated their lives to.

Now, if you’re not familiar with HTAs you’re first thought will likely be, “Just use Wscript.Sleep.” (Incidentally, if you’re not familiar with HTAs, you might want to wander over to the HTA Developers Center.) That’s a good thought, but it won’t work. Why not? That’s because the Wscript object is a somewhat-unique object; for one thing, you can’t actually create an instance of this object. Instead, the Wscript object is automatically provided for you any time you’re running under Windows Script Host. And that’s the problem right there: when you’re running code within an HTA you are not running under Windows Script Host. Instead, you’re running under the script host provided by Internet Explorer. Because you’re not running under Windows Script Host you aren’t automatically given access to the Wscript object, and because you can’t create the Wscript object on your own there’s no way to access Wscript.Sleep. You are – in highly-technical terms – hosed.

This wouldn‘t be that big of a deal if Internet Explorer provided a method comparable to Wscript.Sleep; unfortunately, it doesn’t. So is there some kind of crazy workaround we can use instead of a Sleep method? Did you even have to ask?

<script language = "VBScript">

    Dim dtmStartTime

    Sub Test
        dtmStartTime = Now
        idTimer = window.setTimeout("PausedSection", 5000, "VBScript")
    End Sub

    Sub PausedSection
        Msgbox dtmStartTime & vbCrLf & Now
        window.clearTimeout(idTimer)
    End Sub

</script>

<body>
    <input id=runbutton  type="button" value="Run Button" onClick="Test">
</body>

What we have is a very simple HTA: it consists entirely of a single button that, when clicked, runs a subroutine named Test. Big deal, you say? Well, let’s take a look at the <SCRIPT> section of our HTA and decide how big a deal it really is.

The first thing we do within the <SCRIPT> section is declare a global variable named dtmStartTime:

Dim dtmStartTime

This actually has nothing to do with pausing the script; instead it’s just a variable we use to help us see that the script really did pause as expected. If that doesn’t make any sense to you at the moment, just be patient: it should all become clear very soon.

Next we have the Test subroutine, which gets called when we click the button:

Sub Test
    dtmStartTime = Now
    idTimer = window.setTimeout("PausedSection", 5000, "VBScript")
End Sub

Notice that we have just two lines of code in this subroutine. In the first line we simply assign the current date and time (using the Now function) to the variable dtmStartTime. That brings us to this line of code:

idTimer = window.setTimeout("PausedSection", 5000, "VBScript")

Believe it or not, this is how we mimic the function of the Sleep method. What we’re doing here is using the setTimeout method to create a timer, which we give the ID idTimer. As you can see, we pass setTimeout three parameters:

· PausedSection. This is the name of the subroutine we want to run when the timer expires.

· 5000. This is the amount of time (in milliseconds; 5000 milliseconds equals 5 seconds) we want the timer to wait before calling PausedSection. In other words, that’s our pause. If we want to pause the script for 30 seconds then we’d set this parameter to 30000.

· VBScript. A required parameter that simply lets the script know that PausedSection is written in VBScript.

In other words, when we click the button the Test subroutine will run. When the Test subroutine runs, it creates a timer named idTimer. The sole function of idTimer is to wait 5 seconds and then call the subroutine PausedSection. Does that make sense? Good.

So what do we do inside the subroutine PausedSection? Well, obviously we could run any code we wanted; for simplicity’s sake we do only two things here. First, we display a message box showing the time we clicked the button and the time that the message box itself actually appeared on screen:

Msgbox dtmStartTime & vbCrLf & Now

Allowing for rounding errors, the difference between the time we clicked the button and the time the message box appeared should be 5 seconds. Is that important? You bet it is. Remember, displaying this message box is the first thing that happens inside the PausedSection subroutine. If it shows up 5 second after we click the button then that means we paused our script for 5 seconds. Which, as you might recall, was the whole idea in the first place.

The other thing we do inside the PausedSection subroutine is call the clearTimeout method and effectively get rid of our timer:

window.clearTimeout(idTimer)

Why do we do that? That’s easy: timers are designed to run forever. If we don’t clear the timer then every 5 seconds idTimer will call the PausedSection subroutine, and every 5 seconds our little message box will pop up onscreen. We don’t really want to see that message box every 5 seconds, so we use clearTimeout to remove the timer.

Admittedly, this isn’t quite as easy as simply saying Wscript.Sleep 5000; that means you’ll probably have to think a little bit before you start enabling and disabling timers. In turn, that can be a bit of a challenge, but, hey, isn’t that what life’s all about?

And no, that wasn’t a rhetorical question. Like we said, we’ve been busy trying to pause HTAs; beyond that we have no idea whatsoever what life is all about.


How Can I Use Graphics in an HTA’s Bulleted List?

Hey, Scripting Guy! Often-times while browsing the Internet I run across Web pages where they used a bulleted list, except that they use little pictures as the bullets. Can I add that type of a bulleted list to my HTAs?

-- JA

Hey, JA. You know, as a long-time baseball coach, the Scripting Guy who writes this column knows exactly how to answer that question: That’s up to you, JA; do you believe that you can add that type of bulleted list to your HTAs? The truth is, the only thing that can stop you from adding that type of bulleted lists to your HTAs is yourself. As the immortal Magic Charlie so aptly put it, “Anything you can conceive and believe, you can achieve.”

Of course, we should note that, as a long-time baseball coach, the Scripting Guy who writes this column knows that none of that is really true. It’s great that you want to be a shortstop and it’s great that you truly believe that you can be a shortstop. However, it’d be even better if you could field ground balls and make the throw to first base. When it comes to playing shortstop, it’s not enough to believe that you can turn the double play; you have to actually be able to turn the double play.

Unless, of course, you play for the Seattle Mariners. In that case, actual ability seems to be optional.

So, in other words, no, JA, you can’t add that type of a bulleted list to your HTAs; it’s just not going to happen.

Well, not unless you use code like this, that is:

<html>

<head>
    <style>
        ul
        {list-style-image: url("bullet.gif")}
    </style>
</head>

<body>
    <ul>
        <li>This is the first item in the list.</li>
        <li>This is the second item in the list.</li>
        <li>This is the third item in the list.</li>
    </ul>
</body>

</html>

As it turns out, there are two things you need to do to create a bulleted list that uses pictures as the bullet. To begin with, you need to have a picture that you can use as a bullet; in our case that happens to be a graphics file named Bullet.gif, stored in the same folder as our HTA. Second, you need to create a style for the <UL> tag, the HTML element that creates a bulleted list. That style should look something like this:

<style>
    ul
    {list-style-image: url("bullet.gif")}
</style>

As you can see, what we’ve done here is define a global style that uses the file Bullet.gif for all the bulleted lists found in our HTA. To make that happen we define the style by specifying the desired HTML tag (<UL>) followed by the property values to be assigned to that tag. In this case we’re assigning a value to the list-style-image attribute, with that value being the url parameter followed by the location of the graphics file.

Believe it or not, that’s all we need to do. When we start this HTA any and all of our bulleted lists will use the file Bullet.gif as the bullet character:

You know, we never thought of that: what if we did want to use different bullet styles for different lists? Well, one way to do that is to use a so-called “inline style” rather than a globally-defined style. For example, take a look at the following HTA code; in this case we have one bulleted list that uses a graphic as the bullet and one list that doesn’t use a graphic as a bullet:

<html>

<body>
    <ul style="list-style-image: url('bullet.gif')" >
        <li>This is the first item in the list.</li>
        <li>This is the second item in the list.</li>
        <li>This is the third item in the list.</li>
    </ul>
    <p>
    <ul>
        <li>This is the first item in the list.</li>
        <li>This is the second item in the list.</li>
        <li>This is the third item in the list.</li>
    </ul>
</body>

</html>

Notice that, this time around, we didn’t define a global style. Instead, we specified the value for the list-style-image attribute when we actually added the bulleted list to the HTA:

<ul style="list-style-image: url('bullet.gif')" >

Compare that syntax to the syntax for the second bulleted list in the HTA:

<ul>

Because there is no style parameter here, and because there is no global style defined for bulleted lists, we get the default bullet format. In turn, that means our HTA looks like this:

By using inline styles you can have as many different types of bulleted lists as you, well, have bulleted lists.

If you don’t like dealing with graphics you can also use the list-style-type attribute to modify the look of your bulleted lists. The list-style-type attribute accepts the following values:

Value | Description
disc | Default. Solid circles.
circle | Outlined circles.
square | Solid squares.
decimal | 1, 2, 3, 4, and so on.
lower-roman | i, ii, iii, iv, and so on.
upper-roman | I, II, III, IV, and so on.
lower-alpha | a, b, c, d, and so on.
upper-alpha | A, B, C, D, and so on.
none | No marker is shown.

For example, suppose you define a new bulleted list using this syntax:

<ul style="list-style-type: 'lower-roman'">

That’s going to give you a bulleted list that looks like this:

Not too bad, not too bad at all.

Incidentally, list-image-style and list-style-type are just two of the many attributes available to you when using Cascading Style Sheets (CSS). For more information, take a look at the CSS Reference information found on MSDN.

So apparently Magic Charlie was right after all: whatever you can conceive and believe you really can achieve. Unless you want to get technical about it, in which case you’d have to amend that to read “Whatever you can conceive and believe and the Scripting Guys can achieve you can then copy and paste into your HTA and use it as if you’d created it yourself.” But not only is that a bit of a mouthful, but it doesn’t rhyme, either. Maybe that’s why Magic Charlie decided to use Whatever you can conceive and believe, you can achieve instead. Once again we fail to get the credit we so richly deserve simply because it’s hard to find anything that rhymes with Scripting Guys.


How Can Users Type Data Into an HTA Without Using Text Boxes?

Hey, Scripting Guy! I use HTAs as data entry forms, but I don’t really like the way the forms look when they have a bunch of text boxes on them. How can I allow my users to type data into a form without using text boxes?

-- JJ

Hey, JJ. Before we begin we should, in all fairness, give you a chance to withdraw your question and submit your inquiry to another daily scripting column. Keep in mind that we’re not making this offer because we’re too lazy to answer your question (we are, but that’s another story). Instead, we’re making this offer because you’re expecting to have your question answered in a mature and responsible manner. Unfortunately, we can’t guarantee that’s what will happen.

If you don’t believe us just ask the Scripting Son. Yesterday the Scripting Son did not have to go to school until 10:00 AM. “Hey, Scripting Dad!” he said to his father early that morning. “Could you call me from work around 9:00 and wake me up?” The Scripting Dad, after grumbling some fatherly advice about the Scripting Son learning responsibility and getting himself up, said yes, he would call at 9:00 AM.

About 10:15 the phone rang in the office of the Scripting Guy who writes this column. “Uh, hey, Dad,” said a disgruntled voice on the other end. “Thanks for waking me up this morning.”

Oops. Shortly after lecturing the Scripting Son on the importance of responsibility the Scripting Dad totally neglected his responsibility and forgot all about making the promised phone call.

Fortunately the Scripting Son only missed history class. Imagine if he’d missed something important, like baseball practice!

In other words, JJ, if you’d prefer to take your scripting questions elsewhere, well, we can’t say that we blame you. However, if you’re willing to place your trust in the Scripting Guy who writes this column, then here’s a simple little HTA that doesn’t include any text boxes yet still allows users to type in data:

<body>
    <b>Please enter some data in the space below:</b>
    <div id=divWriter1 CONTENTEDITABLE></div><p>
    <b>Please enter more data in the space below:</b>
    <div id=divWriter2 CONTENTEDITABLE></div><p>
</body>

Can we take a moment to explain how this HTA works? You bet we can.

So who do you think will win the World Series this year? The Phillies seem to be the “in” pick this season, but we’re not convinced; after all, they have a history of underachieving, and – what’s that? Explain how the HTA works? What are you talking a-- oh, right. Sorry; we forgot all about that.

Sure, we can explain how this HTA works; after all, we Scripting Guys take our responsibilities very seriously. As you can see, there’s not much to our HTA; to begin with, we simply have a label (in boldface) that asks the user to enter data in the space below. And then we have this HTML construction:

<div id=divWriter1 CONTENTEDITABLE></div><p>

In the typical HTA this is where we would insert a text box. However, JJ doesn’t want to use a text box here. Therefore, we’ve inserted a <DIV> tag instead. A DIV is nothing more than a named portion of an HTA or HTML page; in this case, we’ve named our DIV divWriter1. DIVs are often used as a container for dynamic updates; for example, a script that periodically goes out and retrieves information about the processes running on a computer might write that information to a DIV. That enables you to periodically display up-to-the-minute process information without having to overwrite (and then recreate) the rest of your HTA.

But we’re doing something a little different here. Notice that we added a parameter to our DIV: CONTENTEDITABLE. What does this parameter do? You got it: it makes the DIV editable, meaning that you can type text into that space, and thus into your HTA, despite the fact that there’s no text box anywhere in sight. Pretty cool, huh?

Well, we thought it was pretty cool.

Let’s take a look at the finished product, which features a pair of DIVs, each with the CONTENTEDITABLE attribute enabled:

OK, admittedly it doesn’t look like much. But notice what happens when we click in the blank spaces below each label and start typing:

See? There’s no text box, but we can still type in data. If we wanted to, we could even select the data and make it boldfaced, italic, or underlined (for example, select a word and then press Ctrl+B). In fact, if you wanted to you could create a nifty little editor, with controls that allowed you to change the font or the paragraph alignment or pretty much anything you wanted. But that goes a bit beyond what we’re able to cover in this column. For now we’re just happy that we can type text into the HTA without using a series of text boxes.

Of course, you can easily “style” your DIV any way you want. You say you want red text? This tag should do the trick:

<div id=divwriter1 CONTENTEDITABLE style="color:red"></div><p>

And here’s a tag that sets the background color of the DIV to something other than white:

<div id=divwriter1 CONTENTEDITABLE style="background-color:navajowhite"></div><p>

Etc. etc.

You know, that’s a very good question: how do you retrieve the information typed into a DIV? As it turns out that’s pretty easy: all you have to do is examine the value of the DIV’s innerText property. Here’s a sample subroutine that displays the information typed into divWriter1:

<Script Language="VBScript">
    Sub TextRetriever
        Msgbox divwriter1.innerText
    End Sub
</Script>

Like we said, etc., etc.

That should get you started, JJ. As for the Scripting Son, he eventually made it to school, albeit a bit late. But, then again, that’s really his fault. After all, if he’s silly enough to rely on the Scripting Dad to be the mature, responsible one in the family, well, then he deserves everything that happens to him. After 17 years, you’d think he’d know better.


How Can I Automatically Dismiss a Message Box After a Specified Length of Time?

Hey, Scripting Guy! I’d like to display a Yes/No message box for a few seconds and then, if no one clicks a button, have the message box disappear and the script perform the default operation. How do I do that?

-- JR

Hey, JR. Sounds like you need the WSH Shell object and the Popup method. Popup works a lot like the VBScript Msgbox function: like Msgbox it displays various types of graphical message boxes (for example, one with Yes and No buttons; one with OK and Cancel buttons; etc.). There’s one big difference, however. When you display a message box using the Msgbox function, that message box stays on screen until someone clicks a button and dismisses it; if no one clicks a button, the message box will stay there forever. (And, by extension, the script will remain frozen in place.) With the Popup method, however, you can specify an optional timeout value; when the specified amount of time has elapsed, the message box automatically dismisses itself, and the script is free to resume operation.

Let’s take a look at a simple script that displays a message box (one with Yes and No buttons) and then reports whether the user clicked one of these buttons or whether the message box dismissed itself after the 10-second time limit was reached:

Const wshYes = 6
Const wshNo = 7
Const wshYesNoDialog = 4
Const wshQuestionMark = 32

Set objShell = CreateObject("Wscript.Shell")

intReturn = objShell.Popup("Do you want to delete this file?", _
    10, "Delete File", wshYesNoDialog + wshQuestionMark)

If intReturn = wshYes Then
    Wscript.Echo "You clicked the Yes button."
ElseIf intReturn = wshNo Then
    Wscript.Echo "You clicked the No button."
Else
    Wscript.Echo "The popup timed out."
End If

The script begins by defining a few constants. The first two – wshYes and wshNo – are used to determine which button the user clicked. Clicking the Yes button returns 6; clicking the No button returns 7. If the script times out and the popup dismisses itself, then -1 is returned. Later on in the script we’ll check for these values in order to determine which action to take.

The other two constants – wshYesNoDialog and wshQuestionMark – are used to construct the message box; wshYesNoDialog specifies a message box with Yes and No buttons, and wshQuestionMark indicates that the message box should also include a question mark icon. In fact, our finished message box will look an awful lot like this:

After defining the constants we create an instance of the WSH Shell object and then call the Popup method (with the return value to be assigned to the variable intReturn). For this script we’ve provided the Popup method with four parameters:

Parameter | Description
Do you want to delete this file? | Message box message.
10 | Timeout value, in seconds. If no one clicks a button within 10 seconds, the message box will automatically dismiss itself.
Delete File | Message box title.
wshYesNoDialog + wshQuestionMark | Message box type: a message box with Yes and No buttons and a question mark icon.

When we call the Popup method the message box appears on screen. When the message box is dismissed – either because someone clicked a button or because the message box timed out – the return value is assigned to the variable intReturn. The script then checks the value of intReturn and reports whether the Yes button was clicked, whether the No button was clicked, or whether the message box timed out.

Hopefully this will do the trick for you. For a slightly more useful script, here’s one that asks whether or not you want to delete a file (C:\Scripts\Test.vbs). If you click No, the script terminates and the file is left untouched. However, if you click Yes or if the message box times out, the file is deleted.

Here’s the script:

Const
wshYes = 6
Const wshNo = 7
Const wshYesNoDialog = 4
Const wshQuestionMark = 32

Set objShell = CreateObject("Wscript.Shell")
Set objFSO = CreateObject("Scripting.FileSystemObject")

intReturn = objShell.Popup("Do you want to delete this file?", _
10, "Delete File", wshYesNoDialog +
wshQuestionMark)

If intReturn = wshNo Then
Wscript.Quit
End If

objFSO.DeleteFile("c:\scripts\test.vbs")


How Can I Give a User a Yes/No Prompt?

Hey, Scripting Guy! I have a script which performs some file management tasks, and then should ask the user whether or not they want to delete a set of files. How do I ask a user a Yes or No question like this?

-- SE, Casper, WY

Hey, SE. There are a couple different ways that you can do this, but because you’re dealing with users the best way is probably to just pop up a message box and let them click Yes or No. How hard is that? Hey, it’s no harder than these few lines of code:

intAnswer = _
    Msgbox("Do you want to delete these files?", _
        vbYesNo, "Delete Files")

If intAnswer = vbYes Then
    Msgbox "You answered yes."
Else
    Msgbox "You answered no."
End If

All we’re doing here is creating a message box that features the following:

Item | Description
The message to be displayed. | In this example, the message is Do you want to delete these files?
A Yes button and a No button. | This is specified by the VBScript constant vbYesNo. There are other values that can give you different message box configurations; for example, you could have a message box with buttons labeled Abort, Retry, and Ignore. Who could forget good old Abort, Retry, and Ignore?
The message box caption. | In this example, the caption is simply Delete Files.

The message box will sit there until the user clicks a button. And how do we know which button was clicked? Well, the results of the button click are stored in the variable intAnswer. All we have to do is check the value of intAnswer. If intAnswer is equal to another VBScript constant (vbYes) then we know the user must have clicked yes; if it’s equal to anything else, then they must have clicked No. (After all, we only have two buttons, so if they didn’t click Yes they had to have clicked No.)

For more information about the MsgBox function, see the VBScript documentation on MSDN.


How Can I Make Selections from a Form When Using a Script?

Hey, Scripting Guy! I’d like to have a form with four radio buttons on it, each button representing a different computer. You choose a radio button, click another button, and then a script runs against the computer you selected. How do I do that?

-- CW

Hey, CW. If we’re talking strictly VBScript and Windows Script Host, the answer is simple: you don’t. Other than displaying a message box, neither VBScript nor WSH have the ability to create graphical user interfaces; there’s no way to use radio buttons, list boxes, dropdown lists, and other graphical elements from within a script.

But – oh, you must have seen this episode before. That’s right: first we tell you that something can’t be done, and then we tell you a way to do it anyway. (Hey, everyone needs a gimmick, right?) And you’re right: while you’re not going to do this using only VBScript, you definitely can do this using an HTA (HTML Application).

We won’t spend much time talking about HTAs today; if you’re interested, you might check out a Webcast we did on this topic about a year ago. Suffice to say that HTAs allow us to combine Internet Explorer and scripting code and, in turn, give our scripts a graphical user interface. And while there are other ways to incorporate a graphical user interface into your scripts, this is probably the easiest for those of you just starting out with GUI development.

Let’s start by giving you the code for the HTA, and then we’ll explain how it all works. This sample HTA displays four radio buttons, each representing a different computer. You select a computer and click a Run Script button; when you do, a subroutine will run that goes out, connects to the chosen computer, and then reports back the name of the operating system installed on that computer. Cool, huh? To see how this works, copy the code, paste it into Notepad, and then save the file with a .hta file extension (for example, os_name.hta). Don’t use a .vbs file extension; that won’t work. It must be .hta.

<SCRIPT
LANGUAGE="VBScript">

Sub RunScript

If ComputerOption(0).Checked Then
strComputer =
ComputerOption(0).Value
End If
If ComputerOption(1).Checked Then
strComputer =
ComputerOption(1).Value
End If
If ComputerOption(2).Checked Then
strComputer =
ComputerOption(2).Value
End If
If ComputerOption(3).Checked Then
strComputer =
ComputerOption(3).Value
End If

If strComputer = "" Then
Exit Sub
End If

Set objWMIService = GetObject _
("winmgmts:\\" &
strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
("Select * From
Win32_OperatingSystem")
For Each objItem in ColItems
Msgbox objItem.Caption
Next

End Sub

Sub CancelScript
Self.Close()
End Sub

</SCRIPT>

<BODY>
<input type="radio" name="ComputerOption"
value="atl-ws-01">atl-ws-01<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-02">atl-ws-02<BR>
<input type="radio" name="ComputerOption" value="atl-ws-03">atl-ws-03<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-04">atl-ws-04<P>

<input id=runbutton class="button" type="button"
value="Run Script" name="ok_button"
onClick="RunScript">
&nbsp;&nbsp;&nbsp;
<input id=runbutton class="button" type="button"
value="Cancel" name="cancel_button"
onClick="CancelScript">

</BODY>

So what do we have here? We can break this code down into four sections: two that use HTML tags to display the radio buttons and Run Script and Cancel buttons, and two others to run subroutines depending on whether you click Run Script or Cancel. Let’s take a closer look at each of these sections.

Here, for example, is the HTML code which displays the four radio buttons. (If you know HTML, there’s nothing special here; this is standard HTML coding.) Note that all the buttons have the same name (ComputerOption); that’s required to ensure that you can only select one button at a time. Note, too that the Value for each button is set to the name of the computer:

<BODY>
<input type="radio" name="ComputerOption"
value="atl-ws-01">atl-ws-01<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-02">atl-ws-02<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-03">atl-ws-03<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-04">atl-ws-04<P>

And here is the code that displays the Run Script and Cancel buttons. The key here is the onClick parameter, which indicates which subroutine will run when a button is clicked. As you can see when you click the first button, the subroutine RunScript runs; click the second button and the subroutine CancelScript runs:

<input id=runbutton
class="button" type="button" value="Run Script"
name="ok_button"
onClick="RunScript">
&nbsp;&nbsp;&nbsp;
<input id=runbutton class="button" type="button"
value="Cancel" name="cancel_button"
onClick="CancelScript">

</BODY>

The CancelScript subroutine, by the way, simply closes the HTA. As you can see, not a terribly-complicated procedure by any means:

Sub CancelScript
   Self.Close()
End Sub

Now – at last! – we get to the good stuff. You select a radio button, and then click Run Script. How does our HTA know which button you selected, and how does it know which computer to run the script against? For that matter, where the heck is the script we want to run? Relax; everything is here in the RunScript routine:

Sub RunScript

    If ComputerOption(0).Checked Then
        strComputer = ComputerOption(0).Value
    End If
    If ComputerOption(1).Checked Then
        strComputer = ComputerOption(1).Value
    End If
    If ComputerOption(2).Checked Then
        strComputer = ComputerOption(2).Value
    End If
    If ComputerOption(3).Checked Then
        strComputer = ComputerOption(3).Value
    End If

    If strComputer = "" Then
        Exit Sub
    End If

    Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_OperatingSystem")
    For Each objItem in ColItems
        Msgbox objItem.Caption
    Next

End Sub

The first half of this subroutine determines which button you selected. Radio buttons (at least those with the same name) are stored as an array; the first button in the array is item 0, the second button in the array is item 1, etc. What we do here is determine which button was selected; that’s done by looking at the Checked property for each button. For example, this line of code looks to see if the Checked property of button 0 (the first button in the array) is true; if it is, that means this was the button that was selected:

If ComputerOption(0).Checked Then

And what happens if Checked is true? Well, then we assign the value of the radio button (and remember, the value of the button happens to be the name of a computer) to the variable strComputer:

strComputer = ComputerOption(0).Value

And what happens if Checked is False? No big deal; after all, we check the value of each radio button. Sooner or later we’ll find out which button was selected (and there can only be one). If it turns out that none of the buttons were selected, then we simply exit the subroutine. That’s what this code does:

If strComputer = "" Then
    Exit Sub
End If

If a button was selected, then strComputer will be equal to the name of the computer we want to connect to. And that’s what the second half of this subroutine does: it’s a standard WMI script that connects to the specified computer and returns the name of the operating system that’s installed.

Whew! We’re bet you’re glad that’s done, aren’t you? Or at least you would be if we were actually done. But there’s one other point we need to make. The sample HTA we showed you here retrieves the name of the operating system installed on a computer, and then displays that name in a message box. That’s fine, but what if you wanted to display, say, a list of all the services installed on a computer? In that case, you’d find yourself responding to scores of message boxes, which is probably not the user experience you’re we’re hoping for.

So can we fix that? Of course. We don’t want to spend much more time on this, but we need to do a few things. First, we have add a SPAN area to our HTA; this is simply an identifiable area on the screen where we can write information. Use code like this to put a SPAN (with the ID of DataArea) beneath all your buttons:

<P>
<span id=DataArea></span>

Second, instead of displaying all the data in a message box, we need to gather up that data and store it all in a variable. This code sets the value of the variable strText to anything that’s currently in strText plus the value of the caption property plus the <BR> tag (which is the HTML equivalent of hitting the ENTER key on the keyboard):

strText = strText & objItem.Caption & "<BR>"

Finally, we need to set the InnerHTML property of our SPAN to the value of the variable strText:

DataArea.InnerHTML = strText

Got all that? Again, without bothering to go into much explanation, here’s a revised HTA which gathers the names of all the services installed on a computer, and then writes those names in the HTA itself:

<SCRIPT
LANGUAGE="VBScript">

Sub RunScript

If ComputerOption(0).Checked Then
strComputer =
ComputerOption(0).Value
End If
If ComputerOption(1).Checked Then
strComputer =
ComputerOption(1).Value
End If
If ComputerOption(2).Checked Then
strComputer =
ComputerOption(2).Value
End If
If ComputerOption(3).Checked Then
strComputer =
ComputerOption(3).Value
End If

If strComputer = "" Then
Exit Sub
End If

Set objWMIService = GetObject _
("winmgmts:\\" &
strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
("Select * From
Win32_Service")
For Each objItem in ColItems
strText = strText & objItem.Name
& "<BR>"
Next

DataArea.InnerHTML = strText

End Sub

Sub CancelScript
Self.Close()
End Sub

</SCRIPT>

<BODY>
<input type="radio" name="ComputerOption"
value="atl-ws-01">atl-ws-01<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-02">atl-ws-02<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-03">atl-ws-03<BR>
<input type="radio" name="ComputerOption"
value="atl-ws-04">atl-ws-04<P>

<input id=runbutton class="button" type="button"
value="Run Script" name="ok_button"
onClick="RunScript">
&nbsp;&nbsp;&nbsp;
<input id=runbutton class="button" type="button"
value="Cancel" name="cancel_button"
onClick="CancelScript">
<P>
<span id=DataArea></span>
</BODY>


How Can I Mask Passwords Using an InputBox?

Hey, Scripting Guy! How can I mask passwords using an InputBox?

-- PG

Hey, PG. If you’re hoping to mask passwords using a function or method built into WSH or VBScript we’re afraid you’ll be disappointed; neither technology supports password masking. That doesn’t mean you can’t do it, it just means we’ll have to look beyond the core scripting technologies.

If you’re running Windows XP or Windows Server 2003, you can use ScriptPW (a COM object found only in those two versions of Windows) to mask passwords from the command line. Here’s a sample script that creates an instance of the ScriptPW.Password object and then uses the StdOut Write method to request that the user enter a password:

Set objPassword = CreateObject("ScriptPW.Password")
WScript.StdOut.Write "Please enter your password:"

strPassword = objPassword.GetPassword()
Wscript.Echo
Wscript.Echo "Your password is: " & strPassword

When you run this script, the message Please enter your password: appears on the screen. At that point, the script will pause and wait for you to type in a password; you simply type the password and press ENTER. This line of code will then grab the password and store it in the variable strPassword:

strPassword = objPassword.GetPassword()

For this simple script, we just echo back the password typed in to prove that the keystrokes really were captured. We do this because with ScriptPW and the GetPassword() method, the keystrokes you type are not displayed onscreen. In other words, the password gets masked.

Of course, that’s great if you’re running Windows XP or Windows Server 2003. But what if you’re running Windows 2000? How can you mask passwords?

Well, some might consider it a bit of a hack, but you can call up a Web page and use the HTML password box to mask passwords. This column isn’t really the place to discuss HTML tagging, so we won’t give you a detailed explanation of how this all works. Instead, we’ll just tell you that you need to do two things.

First, save the following as C:\Scripts\Password.htm (and yes, we want this to be an HTML page):

<SCRIPT LANGUAGE="VBScript">

Sub RunScript
OKClicked.Value = "OK"
End Sub

Sub CancelScript
OKClicked.Value = "Cancelled"
End Sub

</SCRIPT>

<BODY>
<font size="2" face="Arial">
Password:&nbsp;&nbsp;&nbsp; </font><font
face="Arial">
<input type="password" name="UserPassword"
size="40"></font></p>

<input type="hidden" name="OKClicked" size =
"20">

<input id=runbutton class="button" type="button"
value=" OK "
name="ok_button" onClick="RunScript">
&nbsp;&nbsp;&nbsp;
<input id=runbutton class="button" type="button"
value="Cancel"
name="cancel_button" onClick="CancelScript">

</BODY>

Next, save this code as a .vbs file (for example, Password.vbs):

On Error Resume Next

Set objExplorer = WScript.CreateObject _
    ("InternetExplorer.Application", "IE_")

objExplorer.Navigate "file:///C:\Scripts\password.htm"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 350
objExplorer.Left = 300
objExplorer.Top = 200
objExplorer.Visible = 1

Do While (objExplorer.Document.Body.All.OKClicked.Value = "")
    Wscript.Sleep 250
Loop

strPassword = objExplorer.Document.Body.All.UserPassword.Value
strButton = objExplorer.Document.Body.All.OKClicked.Value
objExplorer.Quit
Wscript.Sleep 250

If strButton = "Cancelled" Then
    Wscript.Quit
Else
    Wscript.Echo strPassword
End If

So now what do you do? Well, start Password.vbs. When you do this a Web page with a password box will pop up on screen. If you type a password and click OK, the password you typed will be echoed to the screen (again, just to demonstrate that we’re actually grabbing the password you typed.) If you click Cancel, the script will simply end.

If you’re looking for some more practical examples of both ScriptPW and HTML password boxes, you might want to check out the Remote/Multiple computer templates found in the Script Center. Here you’ll find sample scripts that don’t simply echo back passwords you enter; instead, they take those passwords and then use WMI’s ConnectServer method or ADSI’s OpenDSObject method to securely connect to remote computers.


How Can I Prompt a User for Input?

Hey, Scripting Guy! I need my script to prompt users to enter some information, like the name of the file they want to create. How do I do that?

-- RW, Williamsport, PA

Hey, BN. The simplest – and most foolproof – way to do this is to use an InputBox; when you do that users will be presented with a graphical dialog box similar to this:

Displaying the dialog box is easy; you simply call the InputBox function, passing that function two parameters:

Parameter | Description
The message to be displayed. | In this example, the message is Please enter a name for your new file:
The caption for the dialog box. | In this example, the caption is Create File.

The code itself looks something like this:

strAnswer = InputBox("Please enter a name for your new file:", _
    "Create File")

Note that the variable strAnswer will be used to store whatever the user types into the dialog box.

So far so good. Now, how do you figure out what file name the user typed in? That’s easy; like we said, the value the user enters is automatically stored in the variable strAnswer. Thus:

strAnswer = InputBox("Please enter a name for your new file:", _
    "Create File")
Wscript.Echo strAnswer

There’s only one complication here. As you can see, the dialog box has both an OK button and a Cancel button. The idea is that the user types in a file name and then clicks OK. But what happens if the user clicks Cancel? Theoretically, that means that the user doesn’t want to create a file after all; because of that, the script should either terminate or move on to some other task.

Fortunately, this a case where theory can easily be put into practice. Any time a user clicks the Cancel button, strAnswer is set to an empty string (e.g., strAnswer = “”). To determine whether the user clicked Cancel (or, alternatively, clicked OK without entering a file name), all we have to do is check to see if strAnswer equals an empty string. This modified script checks the value of strAnswer; if strAnswer is empty the script terminates (using Wscript.Quit). Otherwise, the script echoes the value the user typed into the dialog box:

strAnswer = InputBox("Please enter a name for your new file:", _
    "Create File")
If strAnswer = "" Then
    Wscript.Quit
Else
    Wscript.Echo strAnswer
End If

Here’s a variation which places the InputBox in a loop. If the user clicks Cancel, the script reminds the user to enter a file name, and then displays the InputBox again. As soon as the user enters a file name and clicks OK, the script displays the value entered and then exits the loop (Exit Do):

Do While X = 0
    strAnswer = InputBox _
        ("Please enter a name for your new file:","Create File")
    If strAnswer = "" Then
        Wscript.Echo "You must enter a file name."
    Else
        Wscript.Echo strAnswer
        Exit Do
    End If
Loop

For more information about the Input function, see the VBScript documentation on MSDN.


How Can I Prompt a User to Delete (or Not Delete) a Computer Account?

Hey, Scripting Guy! How can I delete a computer account from Active Directory, but provide a Yes/No prompt to the user before the account is actually deleted?

-- NM

Hey, NM. Let’s start by showing you how to delete a computer account from Active Directory. But don’t worry; that won’t take long. In fact, it only takes two lines of code:

Set objComputer = GetObject _
    ("LDAP://CN=atl-ws-01, CN=Computers, DC=fabrikam, DC=com")
objComputer.DeleteObject (0)

As you can see, we bind directly to the account we want to delete (this one happens to be a workstation named atl-ws-01, which can be found in the Computers container in fabrikam.com). After that we call the DeleteObject method, which immediately deletes the account from Active Directory. (Incidentally, the (0) parameter is required, even though the only allowed value is 0. Leave it out and the script will fail.)

As we noted, the moment you call the DeleteObject method that object is deleted from Active Directory. So how can we prompt the user with an “Are you sure you want to delete this account?” prompt? Well, what we need to do is bind to the account, issue the prompt, and only then delete the account (assuming, of course, that the user answers Yes). Here’s a sample script that does just that:

Set objComputer = GetObject _
("LDAP://CN=atl-ws-01, CN=Computers, DC=fabrikam,
DC=com")
strComputer = objComputer.CN

intReturn = Msgbox("Are you sure you want to delete this computer
account?", _
vbYesNo, "Delete " & strComputer)

If intReturn = vbYes Then
objComputer.DeleteObject (0)
End If

Here’s how the script works. We bind to the computer account, and then retrieve the CN (Common Name) for that account. Note that we don’t really have to grab the CN; we just do that so we can include the computer name in our prompt. We then use VBScript’s Msgbox method to display a message box that includes: 1) a Yes button and a No button (that’s what the vbYesNo constant does for us); 2) displays the message Are you sure you want to delete this computer account?; and, 3) just for the heck of it, sets the title of the message box to Delete atl-ws-01 (the word Delete followed by the CN of the computer).

The message box will stay on screen until the user clicks one of the two buttons (Yes or No); as soon as a button is clicked, a value corresponding to that button is assigned to the variable intReturn. We then check the value of intReturn. If intReturn is equal to the VBScript constant vbYes, then the Yes button was clicked; in that case, we go ahead and delete the computer account. If intReturn is equal to anything else (the only other possibility would be vbNo, meaning the No button was clicked) we don’t do anything at all, the script ends, and the account is not deleted.


How Can I Read in IP Addresses and Use Them in a Script?

Hey, Scripting Guy! I have a text file that contains a bunch of IP addresses. I’d like to be able to read these addresses from a script, and then use them to connect to those computers. Is that possible?

-- RL

Hey, RL. You bet you can do this, and here’s how. First, make sure your text file looks something like this, with each IP address on a single line:

192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.4

Next, you need a script that can read the text file line-by-line, storing the IP address in a variable as it goes. Here’s a sample script that reads a file named C:\Addresses.txt, and simply echoes back each value read:

Const ForReading = 1
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\Addresses.txt", ForReading)
Do Until objTextFile.AtEndOfStream
    strComputer = objTextFile.Readline
    Wscript.Echo strComputer
Loop

As you can see, we open the file, then use a Do Until loop to read each line of code. The Do Until loop will read a line of code (using the ReadLine method), store that value in the variable strComputer, and then echo back the value (and because each line in the text file consists of a single IP address, each value echoed back will be a single IP address). The script then loops around and reads the next line of code, automatically repeating this process until there are no more lines left to read (in other words, it keeps going until the AtEndOfStream property is True).

All we have to do now is replace the line of code that echoes back the IP address with some code that does something more useful. In our example, we’re going to retrieve the name of the operating system installed on each computer whose IP address is found in the text file. This is easy to do, because WMI allows you to connect to computer using IP addresses, like so:

strComputer = GetObject("192.168.1.1")

That’s what we’re going to do here; the only difference is that instead of hard-coding in the IP address we’ll use the variable strComputer to represent the IP address. Because the value of strComputer changes each time the loop runs (because each time we read in a new IP address from the text file), over the course of the script we’ll end up connecting to – and retrieving information from – each computer in the text file:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile _
    ("c:\Addresses.txt", ForReading)

Do Until objTextFile.AtEndOfStream
    strComputer = objTextFile.Readline

    Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery _
        ("SELECT * FROM WIN32_OperatingSystem")
    For Each objItem in colItems
        Wscript.Echo objItem.Caption
    Next

Loop

Much easier than you thought it would be, right?


How Can I Show Users a Dialog Box for Selecting Files?

Hey, Scripting Guy! Is there any way I can use a script to present a user with a dialog box and let him or her select a file?

-- BF

Hey, BF. If you’re using Windows 2000, we don’t know of a way to do this, at least not a way that’s built into the operating system. That’s not the case with Windows XP, however. On Windows XP, you can use the UserAccounts.CommonDialog object to present users with a standard File Open dialog box. Here’s what the script looks like:

Set objDialog =
CreateObject("UserAccounts.CommonDialog")

objDialog.Filter = "All Files|*.*"
objDialog.InitialDir = "C:\"
intResult = objDialog.ShowOpen

If intResult = 0 Then
Wscript.Quit
Else
Wscript.Echo objDialog.FileName
End If

It’s a small script, so let’s walk through it line-by-line. We begin by creating an object reference (named objDialog) to the UserAccounts.CommonDialog object. Next we set the Filter property for the dialog box. We want to show all files, so we set the filter like so:

objDialog.Filter = "All Files|*.*"

Suppose we wanted to show only text files? Well, in that case we’d use this filter:

objDialog.Filter = "Text Files|*.txt"

You can probably see how this works: we provide a description of the file types (Text Files), toss in a “pipe separator” (|) and then use a standard wildcard to indicate all .txt files (*.txt). Want to get fancy and show.txt files by default, but then give people the option of looking at all files? Then use this code:

objDialog.Filter = "Text Files|*.txt|All Files|*.*"

Try it, and you’ll see what we mean.

We then specify the default folder. By default, we want the dialog box to show the file in the root folder of drive C, so we set the InitialDir property like this:

objDialog.InitialDir = "C:\"

Want to show files in the C:\Windows folder instead? Then use this code:

objDialog.InitialDir = "C:\Windows"

And don’t worry: this is a real File Open dialog box, so you can click around and end up anywhere you want. Just because you start in C:\Windows doesn’t mean that’s the only folder where you can open files.

Finally, we display the dialog box using this line of code:

intResult = objDialog.ShowOpen

Now we just sit and wait for the user to select a file and click OK (or – alternatively – for the user to click Cancel). If the user clicks Cancel, the variable intResult will be set to 0. In our script, we check the value of intResult and, if it’s 0, we simply use Wscript.Quit to terminate the script.

But what if the user actually selects a file and then clicks OK? In that case, intResult will be set to -1, and the FileDialog property will be set to the path name of the selected file. Our script simply echoes back the path name, meaning we’ll get output similar to this:

C:\WINDOWS\Prairie Wind.bmp

Needless to say, however, you’re not restricted to merely echoing back the file path. Instead, you could use WMI or the FileSystemObject or some other technology to bind to that file and delete it, copy it, compress it, retrieve the file properties, or do pretty much anything you can do to a file.

Well, with a script anyway.

Incidentally, you can only select a single file at a time with this approach; no holding down the Ctrl key and selecting multiple files. There is a way to select multiple files, at least on an XP machine, but we’ll have to address that in a future column.


How Can I Use a Multi-Select Dialog Box in a Script?

Hey, Scripting Guy! Awhile back you showed us how to use a File Open dialog box in our scripts. At the same time you said you’d show us how to use a multi-select dialog box. Did you just forget about the multi-select dialog box?

-- AA

Hey, AA. Yes, we … forgot … about the multi-select dialog box. (Saying we forgot sounds better than saying, “No, we just irresponsibly said we’d do something and then failed to follow up on it.”) For those of you who missed our original column on adding a File Open dialog box to a script, you’ll find it here. For the rest of you, here’s a script that uses a multi-select File Open dialog box. (Note: By “multi-select” we simply mean a dialog box that allows you to select multiple files. The dialog box we showed in a previous column limited you to selecting a single file.)

Set objDialog =
CreateObject("UserAccounts.CommonDialog")
objDialog.Filter = "VBScript Scripts|*.vbs|All Files|*.*"
objDialog.Flags = &H0200
objDialog.FilterIndex = 1
objDialog.InitialDir = "C:\Scripts"
intResult = objDialog.ShowOpen

If intResult = 0 Then
Wscript.Quit
Else
Wscript.Echo objDialog.FileName
End If

There are a few things we need to explain here, beginning with the fact that this dialog box is available only on Windows XP; the preceding script won’t work on any other version of Windows, including Windows Server 2003. Second, there’s only one difference between this and the script we showed you a couple months ago. If you want to create a multi-select dialog box, you simply need to add this line of code:

objDialog.Flags = &H0200

That’s it: set the Flags property to &H0200, and you’ll have a multi-select File Open dialog box. To select multiple files in the dialog box, just do what you usually do: click the first file, then hold down the Ctrl key and click any additional files you want to select.

Having said that, we need to point out that the File Open dialog box you get, while fully functional, isn’t as nice as the File Open dialog box you’re used to. For one thing, you don’t get the nice shortcut bar (with the icons for things like Desktop and My Documents). For another, the dialog box displays everything using short file names; in other words, the dialog box is going to look something like this:

Fully functional, but not as aesthetically pleasing as the other File Open dialog box, the one that allows you to select only a single file.

In addition, the return value from the multi-select dialog box is a string that consists of the folder name and the short file names of all the files you selected. In this string, individual items are separated by a blank space. Thus you get back something like this:

C:\Scripts\ file_dialog.vbs file_save.vbs test.vbs

That means we need to do some additional coding here: we need to take the folder path (C:\Scripts\) and add on the individual file names in order to create valid file paths. Let’s take a look at a revised script that echoes back the file paths for each file selected:

Set objDialog =
CreateObject("UserAccounts.CommonDialog")
objDialog.Filter = "VBScript Scripts|*.vbs|All Files|*.*"
objDialog.Flags = &H0200
objDialog.FilterIndex = 1
objDialog.InitialDir = "C:\Scripts"
intResult = objDialog.ShowOpen
If intResult = 0 Then
Wscript.Quit
Else
arrFiles = Split(objDialog.FileName, " ")
For i = 1 to Ubound(arrFiles)
strFile = arrFiles(0) &
arrFiles(i)
Wscript.Echo strFile
Next
End If

Here we’re using the VBScript Split command to take the string we get back from the dialog box and make it into an array (by splitting on the blank space). The first element in the array will be the folder path; the remaining elements will be the individual file names.

We then use a For Next loop to walk through the items in the array. Note that we start with the second item in the array, the item with the index number 1. (The first item in a VBScript array has an index number of 0.) We do that simply because the first item in the array isn’t actually a file name; it’s the folder path. Inside the For Next loop we combine the folder path (item 0) with the file name; that gives us a complete file path, which we store in a variable named strFile. We then echo this file path.

When we run the script, we get output similar to this:

C:\Scripts\file_dialog.vbs
C:\Scripts\file_save.vbs
C:\Scripts\test.vbs

As soon as we have file paths we can actually do something interesting with those files, like open them, delete them, compress them, or whatever else you like to do with your files.


How Can I Display a Hyperlink in a Message Box?

Hey, Scripting Guy! Is it possible to have a hyperlink to a Web page in a message box?

-- CB

Hey, CB. Sounds like you want to display a message box to users and, ideally, you’d like to include a hyperlink they could click on for more information. For example, suppose a user tried to access some resource and was turned away. In a case like that, you could pop up a message box that tells them that access is denied, then provide them with a link to a Web page that tells them what they need to do to gain access. All in all a much nicer solution than simply telling them “Access Denied.”

So can you do this using a scripting language? Well, no, not as far as we know. Hyperlinks in message boxes would be very cool, but you’re pretty much limited to rudimentary message boxes when using a scripting language.

But wait, don’t leave! You didn’t ask us, “Is there a workaround of some kind, something that does the same thing but doesn’t embed a hyperlink in a message box?” Had you asked us that, we would have said, “Hey, there’s always a workaround of some kind.” What you’re looking for is one-click access to a Web page from a message box. One way to do that would be to embed a hyperlink in the message box; unfortunately, as we just noted, you’re not going to do that with VBScript. So, let’s try another way: why not pop up a message box that asks, “Do you want to apply for access to this resource?” If the user clicks No, the script terminates. If the user clicks Yes, then the script automatically takes them to the appropriate Web page. One message box, one-click access to a Web site. It’s not quite the same as using a hyperlink, but the net result is identical.

As you can see, this only takes a few lines of code:

Set objShell = CreateObject("Wscript.Shell")

intMessage = Msgbox("Would you like to apply for access to this resource?", _
    vbYesNo, "Access Denied")

If intMessage = vbYes Then
    objShell.Run("http://www.microsoft.com")
Else
    Wscript.Quit
End If

We begin by creating an instance of the WSH Shell object; we’ll need that in order to pop up a Web browser and connect to the Web site. We then display a message box (with the title Access Denied) that asks “Would you like to apply for access to this resource?” This message box has a Yes button and a No button, that’s what the VBScript constant vbYesNo is there for.

Some of you might be thinking, “Wait, I thought you had to explicitly define constants in VBScript.” That’s true in most cases; if we were working with the FileSystemObject, for example, we’d need to define constants using code like this:

Const ForReading = 1

However, VBScript has some intrinsic constants of its own. vbYesNo is one; it’s used to display a Yes button and a No button in a message box. vbCrLf is another; it’s used to append a carriage return-linefeed to the end of a string. These intrinsic constants – which are part of the VBScript language – don’t have to be defined; VBScript knows what you mean when you type vbYesNo.

Now, where were we? Ah, yes, we pop up a message box and give the user Yes and No buttons. If the user clicks Yes, we’re going to take them to the specified Web site. How do we know if they clicked Yes? That’s easy: when we create the message box, we store the user’s response in the variable intMessage. To determine which button the user clicked, we just check the value of intMessage. If the value is equal to vbYesNo (another intrinsic constant, equal to 6) then the user clicked Yes, and we use the Shell object’s Run method to open the Web site http://www.microsoft.com. (Notice that all we have to do is specify the URL; the operating system then use the default browser to navigate to that site.)

And what if the user clicks No? In that case, we just quit (Wscript.Quit).

Like we said, not exactly what you had in mind, but it’ll do the trick.

Incidentally, if you’d like to know more about VBScript’s Msgbox function and the different ways you can configure it, check out this portion of the VBScript Language Reference on MSDN.

And what if you have a need to create really elaborate message boxes? Well, in that case, you could actually make yourself an HTML page and use that to mimic a true Windows message box. But that’s a story we’ll have to tell some other day.


How Can I Display Multiple Lines of Output in a Web Page?

Hey, Scripting Guy! How do I get multiple lines of output in a Web page using the Document.Body.InnerHTML property? I can only get one line of output on my page.

-- CM

Hey, CM. Sounds like you’re creating an instance of Internet Explorer and trying to display script output in it. And we’re willing to bet you’re using a script similar to this:

strComputer = "."

Set objExplorer = WScript.CreateObject("InternetExplorer.Application")
objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Visible = 1

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
  ("SELECT * FROM Win32_Service")

For Each objItem in colItems
    strHTML = objItem.DisplayName  & " = " & objItem.State & "<BR>"
    objExplorer.Document.Body.InnerHTML = strHTML
Next

In theory, this script is supposed to display the name and status of all the services installed on your computer. If you have three services installed on your computer, you expect to get output that looks like this:

Service A = Running
Service B = Stopped
Service C = Running

Instead, you’re getting output that looks like this, with only the very last service in the collection displayed:

Service C = Running

What’s wrong? Well, you have a For Each loop that loops through the collection of services. And you have a line of code that assigns the service name and state to a variable. That’s this line:

strHTML = objItem.DisplayName  & " = " & objItem.State & "<BR>"

You then have this line of code, which sets the InnerHTML property of your Web page to the value of strHTML:

objExplorer.Document.Body.InnerHTML = strHTML

This is where the problem lies. The InnerHTML property, for our purposes, consists of all the information displayed on the Web page. The first time your code loops through the collection strHTML is equal to this:

Service A = Running

And that’s exactly what gets displayed in the Web page. But what happens the second time your code loops through the collection? When that happens strHTML gets set to this:

Service B = Stopped

That’s fine, except now you’re setting the InnerHTML – remember, that’s all the information that is displayed on the Web page – to the value of strHTML. That means that the entire contents of your Web page is erased and replaced by strHTML. That’s why only one line of information is displayed: strHTML only contains one line of information. You keep setting the InnerHTML property to one line of information, then a second later you replace it with a new line. You’re replacing information rather than appending it.

So is there a way to fix this? Of course there is; otherwise we wouldn’t have chosen to answer this question! This script solves the problem for you:

strComputer = "."

Set objExplorer = WScript.CreateObject("InternetExplorer.Application")
objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Visible = 1

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
  ("SELECT * FROM Win32_Service")

For Each objItem in colItems
    strHTML = strHTML & objItem.DisplayName  & " = " & objItem.State & "<BR>"
Next

objExplorer.Document.Body.InnerHTML = strHTML

What’s different here? First notice the new line of code for assigning a value to strHTML:

strHTML = strHTML & objItem.DisplayName  & " = " &  objItem.State & "<BR>"

What we’re doing here is saying that strHTML gets assigned whatever is already in strHTML plus some new information. The first time your script runs through the loop, strHTML gets assigned this value:

Service A = Running

The second time the script runs through the loop, it gets assigned the value it already has plus the new information. Thus strHTML equals this:

Service A = Running
Service B = Stopped

By the third time through, strHTML will equal this:

Service A = Running
Service B = Stopped
Service C = Running

This is the data you wanted displayed. And notice that we don’t display the data inside the For Each loop. Instead, all we do in the loop is build up the value of strHTML. After we have looped through all the data, then we finally set the InnerHTML property to the value of strHTML. And because strHTML contains all our data, all our data gets displayed on the Web page.


How Can I Write Output to the Screen that Overwrites Whatever is Currently on the Screen?

Hey, Scripting Guy! How can I write output to the screen that overwrites whatever is currently on the screen?

-- KM

Hey, KM. If you specifically need to write your output to the command window, well, then we don’t have an answer for you: although we played around with a few things, we never found an easy, straightforward way to overwrite information in the command window.

However, if you’re willing to output your information to an Internet Explorer window, well, then we do have an answer for you. And, coincidentally, here’s that answer right now:

Set objExplorer = CreateObject("InternetExplorer.Application")
objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 200
objExplorer.Left = 0
objExplorer.Top = 0

Do While (objExplorer.Busy)
    Wscript.Sleep 200
Loop

objExplorer.Document.Title = "Process Information"
objExplorer.Visible = 1

objExplorer.Document.Body.InnerHTML = "Retrieving process information."

Wscript.Sleep 2000

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_Process")
For Each objItem in colItems
    objExplorer.Document.Body.InnerHTML = objItem.Name
    Wscript.Sleep 500
Next

objExplorer.Document.Body.InnerHTML = "Process information retrieved."
Wscript.Sleep 3000
objExplorer.Quit

Granted this looks a little long, but it’s actually pretty easy, as you’re about to find out. For example, at first glance this block of code might look a bit intimidating:

Set objExplorer =
CreateObject("InternetExplorer.Application")
objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 200
objExplorer.Left = 0
objExplorer.Top = 0

As it turns out, though, all we’re doing here is creating a blank instance of Internet Explorer; in particular, that’s what the first two lines of code do. The remaining lines of code simply configure various properties of the Internet Explorer window: we set the width to 400 pixels, we hide the tool bar, and we position the Internet Explorer window in the upper left-hand corner of the screen. If you’re happy with the default configuration of the Internet Explorer window then you could skip lines 3 through 8.

After we’re done setting up our Internet Explorer window we use this Do While loop to pause the script until Internet Explorer is fully loaded:

Do While (objExplorer.Busy)
    Wscript.Sleep 200
Loop

See? That’s no so bad. With Internet Explorer up and running we next give our Internet Explorer window a title and set the Visible property to True (1). We need to do that because, up till now, Internet Explorer has been running in a hidden window and has not been visible on screen. But this code will take care of that little problem:

objExplorer.Document.Title = "Process Information"
objExplorer.Visible = 1

Now we’re ready to write something to our window. For this sample script that’s as easy as this:

objExplorer.Document.Body.InnerHTML = "Retrieving process information."

As you can see, we’re simply assigning a value to the Document.Body object’s InnerHTML property. In this case we’re merely assigning InnerHTML some text: Retrieving process information. However, we could easily add some HTML tags and create fancier output. For example, this line of code will boldface the text written to the window:

objExplorer.Document.Body.InnerHTML = "<B>Retrieving process information.</B>"

OK, maybe that doesn’t qualify as fancy. But you get the idea.

After displaying our text string in the Internet Explorer window we then pause the script for 2 seconds (2000 milliseconds). There’s no reason why we have to do that; that’s done simply so you’ll have a chance to see the text before we overwrite it.

That brings us to this block of code:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_Process")
For Each objItem in colItems
    objExplorer.Document.Body.InnerHTML = objItem.Name
    Wscript.Sleep 500
Next

All we’re doing here is retrieving a collection of the processes running on the local computer. That’s not important; we just need the script to do something interesting. What we care about today is what happens inside the For Each loop we set up to walk through all the items in that collection:

For Each objItem in colItems
    objExplorer.Document.Body.InnerHTML = objItem.Name
    Wscript.Sleep 500
Next

In most WMI scripts we’d echo back property values (such as Name) inside our For Each loop. Here, however, we don’t use Wscript.Echo. Instead, we assign the name of the first process in the collection to the body’s InnerHTML property:

objExplorer.Document.Body.InnerHTML = objItem.Name

What’s that going to do? That’s going to overwrite the existing contents of our Internet Explorer window with the name of the first process in the collection. For example, at the time we begin the loop the Internet Explorer window will contain this text:

Retrieving process information.

As we start the loop, that text will be replaced with the name of the first process in the collection. For example:

Winword.exe.

After overwriting the contents of the window we pause the script for half a second (500 milliseconds). Again, that’s not required; we do that simply to slow the script down a little and give you a chance to see the first process name displayed. Half a second later we loop around and retrieve information about the second process in the collection. The script then replaces the existing contents of the Internet Explorer window (technically, the value assigned to the InnerHTML property) with the name of the second process. Etc. etc.

After we’ve looped through the entire collection we indicate that the script has finished, pause for 3 seconds, and then dismiss the Internet Explorer window:

objExplorer.Document.Body.InnerHTML = "Process information retrieved."
Wscript.Sleep 3000
objExplorer.Quit

This works pretty good, and is pretty easy as well. And because it uses HTML that means you can make your output as fancy as you want; in fact, you could even make your Internet Explorer window look like a command window. If that’s not cool, well, we don’t know what is. (Actually, as one of the Scripting Sons constantly reminds his Scripting Father, we don’t know what cool is.)


Can I Change the Current Directory When Running a Script?

Hey, Scripting Guy! Can I change the current directory while my script is running?

-- KO, Kalispell, MT

Hey, KO. Yes, you can change the current directory while a script is running, provided you are running Windows Script Host 5.6, that is. To change the current directory, all you have to do is create an instance of the Wscript.Shell object, and then set the value of the CurrentDirectory property to the desired directory. For example, this little two-line script changes the current directory to C:\Temp:

Set objShell = CreateObject("WScript.Shell")
objShell.CurrentDirectory = "C:\Temp"

To verify that this works, copy this script, save it to some folder (doesn’t matter which one, as long as it isn’t C:\Temp) and then run it. You should see two items get echoed to the screen: the name of the folder where you started the script from, and then the new current directory, C:\Temp:

Set objShell = CreateObject("WScript.Shell")
WScript.Echo objShell.CurrentDirectory
objShell.CurrentDirectory = "C:\Temp"
WScript.Echo objShell.CurrentDirectory

If the folder C:\Temp doesn’t exist, you’ll get a file not found error.

Incidentally, as implied above the current directory for a script isn’t necessarily the folder where the script lives, it’s the folder you were in when you started the script. Suppose the script is in C:\Scripts. You open a command prompt window, change to the Windows directory, and then type this:

cscript c:\scripts\my_script.vbs

In that case, the current directory will be C:\Windows, not C:\Scripts.

For more information about the CurrentDirectory property, see the Windows Script Host documentation on MSDN.


How Can I Change the Working Folder of a Script?

Hey, Scripting Guy! My script needs to have the same working folder as the application that the script starts. How can I change the working folder of a script?

-- JM

Hey, JM. You can change the current (or working) folder of a script simply by setting the value of the Wscript Shell object’s CurrentDirectory property. (Note. The CurrentDirectory property was introduced in Windows Script Host 5.6, so the scripts we’ll show you today require you to be running that version of WSH.) For example, here’s a demonstration script that echoes the current directory for a script, changes the current directory to C:\Windows, and then echoes the current directory again:

Set objShell = CreateObject("Wscript.Shell")
Wscript.Echo objShell.CurrentDirectory

objShell.CurrentDirectory = "C:\Windows"
Wscript.Echo objShell.CurrentDirectory

As you might expect, in the last line of the script C:\Windows is echoed as the name of the current directory.

Now that we know how to change the current folder all we have to do is write a simple little script that starts an application and then changes the current directory to match the working folder of that application. For example, this script starts Notepad (C:\Windows\System32\Notepad.exe), determines the desired working folder by parsing the path to Notepad.exe, and then changes the current directory to C:\Windows\System32:

Set objShell =
CreateObject("Wscript.Shell")

strApp = "C:\Windows\System32\Notepad.exe"
arrPath = Split(strApp, "\")

For i = 0 to Ubound(arrPath) - 1
strAppPath = strAppPath & arrPath(i) & "\"
Next

objShell.CurrentDirectory = strAppPath
Wscript.Echo objShell.CurrentDirectory

objShell.Run(strApp)

All we’re doing is storing the application path (C:\Windows\System32\Notepad.exe) in a variable named strApp. We then use the VBScript Split function to split this path into an array (using the \ as our delimiter); that gives us an array consisting of these elements:

C:
Windows
System32
Notepad.exe

Why do we want an array consisting of these elements? Well, as you can see, the first three elements – plus a couple \’s – give us the path we want: C:\Windows\System32. By splitting the path like this, we can determine the folder path by taking the first three items and discarding the executable file name (Notepad.exe), That’s what we do with this line of code, which reconstructs the path using all the items in the array except the very last one:

For i = 0 to Ubound(arrPath) - 1
    strAppPath = strAppPath & arrPath(i) & "\"
Next

In case you’re wondering, arrays always start with item 0, which is why our For-Next loop starts at 0. In turn, the Ubound property represents the last item in the array. We don’t want to use the last item, we only want to go as far as the next-to-last item. Hence Ubound – 1, which simply means the last item in the array minus 1, or the next-to-last item. In the sample script shown here, that gives us this equation:

C: + \ + Windows + \ + System32 + \

Or, C:\Windows\System32\. (The extra \ on the end doesn’t make any difference, so we don’t bother trimming it off.)

From there we change the current directory to C:\Windows\System32; echo the value of the current directory (just so you know that the change has been made); and then go ahead and start Notepad. Problem solved!


How Can I Determine How Long It Takes a Script to Run?

Hey, Scripting Guy! Is there a good way to time how long it takes for a script to run?

-- BN, Montreal, Canada

Hey, BN. Probably the easiest way to do this is to let the script time itself. To accomplish that feat, set the first line of the script to this:

dtmStart = Now

This code simply sets the value of the variable dtmStart to the current date and time. Now, make the last two lines of the script look like this:

dtmEnd = Now
Wscript.Echo DateDiff("s", dtmStart, dtmEnd)

What does that do for us? Well, now we’ve saved the time the script began and the time the script ended. To determine how long it took for the script to run, we just need to subtract the beginning time from the ending time. For example, if the script started at 8:40 and finished at 8:41, we’d calculate the running time like this:

8:41 – 8:40 = 60 seconds

Fortunately that kind of date arithmetic is very easy in VBScript. All we have to do is call the DateDiff function, and pass along three values:

Value | Description
The time interval we want our answer to be in. | For this script, we want to measure running time in seconds, so we pass the value “s”. For longer scripts, we might pass the value “m” in order to get the time in minutes.
The time the script began. | This is stored in the variable dtmStart.
The time the script ended. | This is tored in the variable dtmEnd.

The entire script might look like this. Note that for demonstration purposes we’ve included code that simply loops from 1 to 10,000,000 without doing anything else; that’s there simply to waste some time and make the script run a bit longer.

dtmStart = Now()

For i = 1 to 10000000
Next

dtmEnd = Now()
Wscript.Echo DateDiff("s", dtmStart, dtmEnd)

In case you’re wondering, you can only measure running times to the nearest second; you can’t calculate times in microseconds or milliseconds or anything crazy like that. That might sound like a limitation, but look at it this way: if your script can complete in less than a second then you really don’t have much to worry about, do you?

For more information about the DateDiff function, see this portion of the Microsoft Windows 2000 Scripting Guide.


How Can I Determine the Default Script Host on a Computer Before I Run a Script?

Hey, Scripting Guy! I know that when I run a script I can use code to determine whether the script is running under WScript or CScript. What I can’t figure out is this: how can I determine the default script host on a computer before I run a script?

-- AT

Hey, AT. You know, it’s not very often that people completely stump the Scripting Guys; however, we have to admit this question posed a bit of a challenge. (OK, OK: so maybe it’s not that hard to stump the Scripting Guys; to be honest, it’s actually pretty easy. But you guys don’t need to know that.) We had no idea how to answer this question, and we knew it was going to take a lot of hard work and dedication in order to help you out with this.

But, in the face of adversity, did the Scripting Guys give up? Well, as a matter of fact we did. However, in the process of doing something totally unrelated, we accidentally stumbled upon the answer. Here, using a somewhat-roundabout methodology, is a script that will tell you the default script host on a computer:

Const HKEY_CLASSES_ROOT = &H80000000

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "VBSFile\Shell\Open\Command"
objRegistry.GetExpandedStringValue HKEY_CLASSES_ROOT,strKeyPath,vbNullString,strValue

strValue = LCase(strValue)

If InStr(strValue, "wscript.exe") then
    Wscript.Echo "WScript"
Else
    Wscript.Echo "CScript"
End If

As it turns out, information about the default script host is stored in the registry, although not in a particularly obvious location. What you need to do is open HKEY_CLASSES_ROOT, find the registry key VBSFile\Shell\Open\Command and then look at the default value. (The default value is an otherwise-unnamed registry value, one that shows up as (Default) in Regedit.) The value of (Default) will be the path to the script host executable (e.g., %SystemRoot%\System32\WScript.exe). In turn, the executable file tells you whether the default script host is CScript or WScript.

So how do we actually get to that value? Well, we begin by defining a constant named HKEY_CLASSES_ROOT and setting the value to &H80000000; that tells the script which registry hive we want to work with. We then connect to the WMI service on the local computer (although we could just as easily run this script against a remote machine), taking care to bind to the root\default namespace. (Most WMI scripts use the root\cimv2 namespace, but, for some reason, the System Registry Provider lives in root\default instead. Probably cheaper housing or better schools.)

After making the connection we create a variable named strKeyPath, assigning it the value VBSFile\Shell\Open\Command. At that point we’re ready to read the value from the registry.

What’s that? Didn’t we forget something? No, we didn’t forget anything. (Although now that you mention it we’re beginning to wonder if anyone turned off the stove when we left the house this morning.) You’re right that, in most WMI registry scripts, we would need to assign values to two variables, one to represent the registry key, the other to represent the desired value within that registry key. In this case, however, we don’t need to create a variable in which to store the name of the registry value. Why not? Because, technically, (Default) values don’t actually have a name; they’re just, well, the default values. What we’ll end up doing is telling the script to read a Null value from this particular registry key. That doesn’t sound like a very good plan but, fortunately, the Registry Provider will know that this means to read the (Default) value.

In fact, as long as we’re on the subject, here’s the line of code that retrieves the value for us:

objRegistry.GetExpandedStringValue HKEY_CLASSES_ROOT,strKeyPath,vbNullString,strValue

Because (Default) happens to have the REG_EXPAND_SZ data type we call the GetExpandedStringValue method, passing four parameters:

· HKEY_CLASSES_ROOT, the constant that tells the script which registry hive to work with.

· strKeyPath, the variable that tells the script which registry key to work with.

· vbNullString, a VBScript constant representing a Null value. This is where we would usually insert a variable representing the value name but, as we noted, this time around our value doesn’t have a name.

· strValue, an “out” parameter. GetExpandedStringValue needs a place to store the value it reads from the registry; an out parameter is simply a placeholder variable where that value can be stashed.

As soon as GetExpandedStringValue does its thing we use this line of code to change all the characters in strValue to lowercase; we do that to make it easier to determine whether the value contains a particular string or not:

strValue = LCase(strValue)

Finally, we call the InStr function, looking to see if the string value wscript.exe can be found anywhere within strValue. If it can, then we echo back the fact that the default script host is WScript; if it can’t, then that must mean the default script host is CScript:

If InStr(strValue, "wscript.exe") then
    Wscript.Echo "WScript"
Else
    Wscript.Echo "CScript"
End If

So there you go, AT: now you can determine the default script host on a computer without running a script. (Yes, we know, you have to run the script that determines the default script host…but you get the point.) We hope you find this useful, and we hope everyone has learned an important lesson today: try as you might, you can’t stump the Scripting Guys.

Well, not unless you ask us a question we can’t answer. But you’ll never stump us by asking a question that we can answer.

OK: almost never.


How Can I Determine the Drive Letter for the Drive My Script is Currently Running On?

Hey, Scripting Guy! I have a script that runs off a CD and copies a bunch of files to a computer. However, in order to copy the files I need to know the complete file path, including the drive letter. How can I determine the drive letter for the drive my script is currently running on?

-- RKG

Hey, RKG. You know, about a year or so ago one of the Scripting Guys installed – or, more accurately, tried to install – a relatively well-known application.

Note. If this had been a Microsoft program we’d tell you the name, but this wasn’t a Microsoft program. As if Microsoft would ever release software that didn’t work flawlessly right from the start!

As you probably already guessed, this application wouldn’t install. Why not? Because the setup program assumed that the CD-ROM drive would always be drive D. When faced with a computer where the CD drive was on drive E, the installation program did the only thing it could do: it gave up. In order to install the application you had to download a patch that could deal with alternate drive letters.

Of course, even then you were prompted to enter the drive letter to your CD-ROM; the patch couldn’t figure out the drive letter for the CD drive, either. But at least you could finally install the software.

Pretty sad story, isn’t it? And what makes it sadder is that this multi-million dollar software company could have solved their problem using a script no more complicated than this:

Set objFSO =
CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("Wscript.Shell")

strPath = objShell.CurrentDirectory
strDrive = objFSO.GetDriveName(strPath)

Wscript.Echo strDrive

Good point: we should have contacted this company and offered to act as high-priced installation consultants. Now you tell us!

As you can see, this is a pretty simple little script. We begin by creating instances of two objects: Scripting.FileSystemObject and Wscript.Shell. We then encounter this line of code:

strPath = objShell.CurrentDirectory

It goes without saying (although we’ll say it anyway) that the CurrentDirectory property reports back the current directory for the script. For example, suppose our CD-ROM is drive E, and the script is running in the Setup folder. Try to guess the value of the CurrentDirectory property:

E:\Setup

You’re right; that was a bit too easy, wasn’t it?

Once we have a file path we can then use the FileSystemObject’s GetDriveName method to extract the drive letter from that path. That’s what we do here:

strDrive = objFSO.GetDriveName(strPath)

We simply pass GetDriveName the file path in question, then store the returned value in the variable strDrive. And what do you suppose we get when we echo back the value of strDrive? You got it:

E:

All of which means that now you’re ready to act as a high-priced installation consultant. When you get your first big job, remember: it was the Scripting Guys who helped you get started. And, yes, we will take a check. Thanks for asking. (Editor’s Note: Time for the editor to make the lawyers happy: As much as we’d like to accept your check [money order, credit card number, cash, etc.], this column and all advice in it are provided free of charge. The Scripting Guys will have to continue to survive on their regular paychecks.)


How Can I Determine the Name of a Script While That Script is Running?

Hey, Scripting Guy! From within the script itself, how can I get the name of a running script? I need that in order to create a generic error handler for all my scripts.

-- TB

Hey, TB. You know, the Scripting Guy who writes this column is in the office today, trying to wrap things up so he can start a well-deserved vacation. And no, he’s not going anywhere: after carting assorted mothers, aunts, sisters, and even a friend of the Scripting Son to Europe last year, this year he fully intends to spend his time doing absolutely nothing.

True, but that’s only at work. In addition to that, he also plans on doing absolutely nothing at home.

Of course, this Scripting Guy is only taking one week off; he’s not taking one week and one day off. Therefore, you can rest assured that he will put in a full day today, working tirelessly to help answer your scripting questions.

Editor’s Note: Before the mass hysteria begins: don’t worry. The Scripting Guy left plenty of Hey, Scripting Guy! articles to cover his absence, the daily column will go on. You can relax knowing your faithful column will still be here each day, the only difference being the Scripting Editor might have a little more fun next week than usual.

It’s just a coincidence that today’s question can be answered with only two lines of code:

Wscript.Echo "Script name: " & Wscript.ScriptName
Wscript.Echo "Script path: " & Wscript.ScriptFullName

Hey, it could have been worse: we could have answered this question using just one line of code. But that really looked like we were just trying to get the column done so we could sneak out early. Therefore, we tossed in a bonus line of code that retrieves the complete file path; that’s in addition to the line of code that returns the script name.

As it turns out, the Wscript object (the parent object for Windows Script Host) has a pair of properties that can return meta-information about a script while that script runs. As the name implies, the ScriptName property returns the file name of the script; as the name doesn’t necessarily imply, the ScriptFullName property returns the file path for the script.

For example, suppose you’re running the script C:\Scripts\Test.vbs. What do you suppose the ScriptName and ScriptFullName properties will return?

You got it:

Script name: test.vbs
Script path: C:\scripts\test.vbs

You’re right: even for a Scripting Guy it’s a bit early to sneak out, isn’t it? Well, we could show you some slides from last year’s vacation; in fact, we could even – oh. OK. How about this? Here’s a nice little chunk of code that can return the build number for VBScript; that can be useful in ensuring that your computers are all fully up-to-date with the latest patches and all:

Wscript.Echo "Build number: " & _
    ScriptEngineMajorVersion & "." & ScriptEngineMinorVersion & "." & ScriptEngineBuildVersion

As you can see, in this little script we simply combine three VBScript functions: ScriptEngineMajorVersion, ScriptEngineMinorVersion, and ScriptEngineBuildVersion. Put them all together and they spell something similar to this:

Build number: 5.6.8820

If all you care about is whether your computers are running WSH 5.6 (as opposed to WSH 2.0) then this line of code is all you need:

Wscript.Echo "Version: " & Wscript.Version

OK, that should just about – oh. Seems the Scripting Editor still doesn’t believe we’ve done enough work for this column. What’s that? Is the Scripting Editor one of the reasons we’re so anxious to get away? Well, as a matter of fact – uh, no, no. Of course not ….

Editor’s Note: It’s probably pretty obvious who’s really getting the vacation here.

Tell you what: how ‘bout we show you some code that lets a script know whether or not it’s running under WScript or CScript? (An important consideration: not only do you potentially face the prospect of clicking OK in hundreds of message boxes if certain scripts are run under WScript, but there are a few things – such as StdIn and StdOut – that are available only if you are running under CScript.) Scripting veterans have seen this before, but newcomers might find it handy:

strScriptHost = LCase(Wscript.FullName)

If Right(strScriptHost, 11) = "wscript.exe" Then
    Wscript.Echo "This script is running under WScript."
Else
    Wscript.Echo "This script is running under CScript."
End If

How does this code help us determine which script host we’re running under? Well, the FullName property returns the path to the executable file for the current script host; depending on where Windows has been installed that will usually be either C:\Windows\System32\Cscript.exe or C:\Windows\System32\Wscript.exe. If the last 11 characters in FullName are wscript.exe then we’re running under WScript; otherwise we’re running under CScript.

With that in mind, we use the LCase function to convert the FullName to lowercase letters, and then store that value in the variable strScriptHost:

strScriptHost = LCase(Wscript.FullName)

From there we use the Right function to determine whether or not the last 11 characters in strScriptHost are equal to wscript.exe. If they are we echo back the fact that the script is running under WScript; otherwise we echo back the fact that we’re running the script under CScript.

Note. You say you’re not familiar with the Right function? Boy, is this your lucky month: try to guess what the latest edition of Sesame Script is all about?

Um, no, that’s not even close: it’s about the Right, Left, and Mid functions. But that was a good guess … sort of.

And now we are going to go. (It’s either that or we start showing last year’s vacation slides; take your pick.)


How Can I Determine the Path to the Folder Where a Script is Running?

Hey, Scripting Guy! How can I determine the path to the folder where a script is running? What I’d like to do is have the script automatically open the folder where the script is running.

-- PD

Hey, PD. You know, for the Scripting Guys this question – as do so many of the questions we receive – falls under the category Why Didn’t We Think of That? The Scripting Guys spend a lot of time testing scripts, including scripts submitted by members of the Windows scripting community. (Well, one of the Scripting Guys spends a lot of time testing scripts. Not that he’s complaining, mind you, even though he has to do pretty much all the work around here. [Editor’s Note: Including most of the whining.]) Many of these scripts add files to or delete files from a folder or write information to a log file in the same folder where the script itself can be found. That means we often need to have Windows Explorer opened to the script’s home folder. How do we open that folder? Well, we double-click My Computer, we double-click C:, we double-click Scripts, etc., etc. It never occurred to us to have the script go ahead and open up the folder for us.

You know, someone really should write a daily column explaining how to do useful little scripting tasks like that. That would be a handy thing to have, wouldn’t it?

So maybe the Scripting Guys aren’t particularly good at thinking up new ideas. However, once someone else has come up with an idea we can usually help them turn that idea into a script:

Set objShell =
CreateObject("Wscript.Shell")

strPath = Wscript.ScriptFullName

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.GetFile(strPath)
strFolder = objFSO.GetParentFolderName(objFile)

strPath = "explorer.exe /e," & strFolder
objShell.Run strPath

As you can see, this isn’t a particularly complicated script. We begin by creating an instance of the Wscript.Shell object; we’ll use this object later to launch Windows Explorer. We then use this line of code to get the full path to the running script (for example, C:\Scripts\My_script.vbs):

strPath = Wscript.ScriptFullName

Before we go any further, a quick clarification. Some of you might be wondering why we didn’t use the CurrentDirectory property to determine the current directory of the script. Well, we could have, except for one thing: the current directory of the script isn’t necessarily the same folder in which the script is running. After all, you can easily start a script in C:\Scripts and then change the current directory to C:\Test. For this column we wanted the name of the folder where the script resides. If you’d prefer to get the current directory then use this script instead:

Set objShell =
CreateObject("Wscript.Shell")
strPath = objShell.CurrentDirectory

strPath = "explorer.exe /e," & strPath
objShell.Run strPath

Now, back to our original script. After storing the full path to the script in the variable strPath we next create an instance of the Scripting.FileSystemObject. That brings us to these two lines of code:

Set objFile = objFSO.GetFile(strPath)
strFolder = objFSO.GetParentFolderName(objFile)

At the moment we have the path to the script file itself: C:\Scripts\My_script.vbs. That’s fine, but what we really want to do is get rid of the file name and leave us with just the folder path: C:\Scripts. Although there are several different ways we could do this, we felt this approach was the simplest. What we do here is use the GetFile method to bind to the file C:\Scripts\My_script.vbs (notice how we pass the variable strPath as the sole parameter to GetFile). After making the connection to the file we can then use the GetParentFolderName method to get the full path to the folder where the file resides; needless to say, that happens to be C:\Scripts. And that also happens to be the very folder we want to open.

The rest is easy. With the path to the script folder safely tucked away in a variable named strPath, we can then use this line of code to construct a command line string that will open a Windows Explorer window, a window that has the focus set to C:\Scripts:

strPath = "explorer.exe /e," & strPath

All that’s left to do now is call the Run method, passing along the command line string we just constructed:

objShell.Run strPath

Cool, huh? Now, anyone out there have an idea for tomorrow’s column?


How Can I Determine Which Account a Script is Running Under?

Hey, Scripting Guy! How can I determine which account a script is running under?

-- KW

Hey, KW. You know, it’s been awhile since we started off a column with a bunch of excuses, and that’s been hard on us: after all, making excuses is what we Scripting Guys do best. With that in mind, then, let’s start off with one of our favorite excuses: the script we’re about to show you works only on Windows XP and Windows Server 2003. We’ll show you a way to kind of, sort of do this same thing on Windows 2000, but it’s definitely not as good.

Ah, yes: now that felt good.

OK, no more excuses (for now, at least). Instead, let’s talk scripting. Here’s what the script looks like:

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

Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process Where " & _
    "Name = 'cscript.exe' or Name = 'wscript.exe'")

For Each objProcess in colProcessList
    If InStr(objProcess.CommandLine, "test.vbs") Then
        colProperties =   objProcess.GetOwner(strNameOfUser,strUserDomain)
        Wscript.Echo "This script is running under the account belonging to " _
            & strUserDomain & "\" & strNameOfUser & "."
    End If
Next

As you can see, we begin by connecting to the WMI service on the local computer, although this same script could just as easily be targeted against a remote machine. (Yes, we do say that a lot. But that isn’t an excuse, it’s just a statement of fact: almost all WMI scripts work equally well against a remote computer as they do the local computer. Every now and then we do say something that isn’t an excuse!)

We then encounter this line of code:

Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process Where " & _
    "Name = 'cscript.exe' or Name = 'wscript.exe'")

You might have already guessed that we need to use the Win32_Process class to carry out our task; that’s because Win32_Process is the WMI class that keeps track of all the processes currently running on a computer. Of course, we aren’t interested in all the processes running on the computer; we’re only interested in the scripts. Because of that we include a Where clause that restricts the returned information only to instances of the two Windows script hosts: Cscript.exe and Wscript.exe.

Note. Yes, we could have written this script in a slightly different fashion, and maybe saved a line or two of code in the process. We opted for this approach because it more closely resembles the way we have to do this chore on Windows 2000.

After issuing the query we set up a For Each loop to walk through the returned collection. In this example we’re trying to determine the owner for a script named Test.vbs. Therefore, we need to check each script to see if it happens to be named Test.vbs. How do we do that? By using this line of code:

If InStr(objProcess.CommandLine, "test.vbs") Then

All we’re doing here is using the InStr function to see if the string test.vbs can be found anywhere in the CommandLine property. And what is the CommandLine property? In somewhat simplistic fashion, this is the command string required to start the script from a command prompt. For example, the CommandLine might be something like this:

C:\Scripts\Test Scripts\Test.vbs

We’re checking for test.vbs because we’re assuming we don’t have a script named, say, MyTest.vbs. If you’re concerned with this kind of name collision, then we could simply use InStr and test for a string like \test.vbs. That’s something you’ll have to decide for yourself.

If it turns out that our target string can be found in the CommandLine value we then call the GetOwner method to find out the “owner” of the process (that is, the name of the account that the script is running under):

objProcess.GetOwner(strNameOfUser,strUserDomain)

With GetOwner, we need to pass along a pair of “out parameters.” An out parameter is nothing more than a variable (which we name ourselves) that the method will fill with a value. Here we’re passing variables named strNameOfUser and strUserDomain; in return, GetOwner will assign those variables the name of the user and the domain of the user who owns the process.

At that point all we have to do is echo back the returned information:

Wscript.Echo "This script is running under the account belonging to " _
    & strUserDomain & "\" & strNameOfUser & "."

So then why can’t we run this same script on Windows 2000? Actually there’s a good reason for that: the CommandLine property is found only on Windows XP and Windows Server 2003. On other versions of Windows we can’t identify individual scripts; the best we can do is return owner information for any instances of Cscript.exe and Wscript.exe that happen to be running. If there’s only one script running, no problem: that single instance of CScript.exe or Wscript.exe has to represent that one script. In turn, that means the owner of the script host process is also the owner of the script process. If you have multiple scripts running, though, that’s a different story. If that’s the case, the best you can do is say, “Well, Ken Myer owns one of these scripts, although we don’t know which one. And whichever one he doesn’t own Pilar Ackerman does own.”

No, not quite as good. But it is what it is. (And yes, that is an excuse. A somewhat lame one, but an excuse nonetheless.)

Here’s what the Windows 2000 solution (OK: semi-solution) looks like:

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

Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process Where " & _
    "Name = 'cscript.exe' or Name = 'wscript.exe'")

For Each objProcess in colProcessList
    objProcess.GetOwner strNameOfUser,strUserDomain
    Wscript.Echo "A script is running under the account belonging to " _
        & strUserDomain & "\" & strNameOfUser & "."
Next


How Can I Display A Progress Bar (or Something Similar) While My Script Runs?

Hey, Scripting Guy! How can I get my script to do something interesting while the code executes; you know, how can I display a progress bar or something?

-- HD

Hey, HD. To begin with, we don’t recommend that you try to use a true progress bar; that’s because it’s very difficult to calculate – let alone display – progress. We’re all familiar with those so-called progress bars – some, alas, included in Microsoft products – that tell you that the estimated time to complete an operation is 3 minutes, then 296 minutes, then 1 minute, then 14 minutes. We don’t want to mess around with something like that.

Instead, we suggest you try a simple little dialog box (or at least something that looks like a dialog box) that merely informs the user that something is going on and asks them to be patient. When the operation is complete, our sample dialog box displays a message to that effect, and then disappears. It’s not fancy, but it works.

Here’s the code:

On Error Resume Next

Set objExplorer = CreateObject _
    ("InternetExplorer.Application")

objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Width = 400
objExplorer.Height = 200
objExplorer.Visible = 1

objExplorer.Document.Title = "Logon script in progress"
objExplorer.Document.Body.InnerHTML = "Your logon script is being processed. " _
    & "This might take several minutes to complete."

Wscript.Sleep 10000

objExplorer.Document.Body.InnerHTML = "Your logon script is now complete."

Wscript.Sleep 5000
objExplorer.Quit

All we’re doing here is creating an instance of Internet Explorer, then using this line of code to open up a blank page in the browser window:

objExplorer.Navigate "about:blank"

We get rid of the toolbar and the status bar (by setting those values to 0), and then set the size of our window to 400 pixels and 200 pixels. We then set the Visible property to 1, which actually displays our little Internet Explorer window on the screen. And just for the heck of it we configure the Title property for the window using this line of code:

objExplorer.Document.Title = "Logon script in progress"

The net result? Something that looks like this:

That’s almost OK in and of itself, but we can probably do a little better: for one thing, we can display a custom message in our Internet Explorer document. To do that we set the InnerHTML property of the document body:

objExplorer.Document.Body.InnerHTML = "Your logon script is being processed. " _
    & "This might take several minutes to complete."

What’s cool about this is that we can use all our favorite HTML tags when assigning a value to the InnerHTML property. For example, suppose we want this message displayed in bold. In that case, we just need to include the <B> and </B> tags:

objExplorer.Document.Body.InnerHTML = "<B>Your logon script is being processed. " _
    & "This might take several minutes to complete.</B>"

After setting the InnerHTML property, we have an instance of Internet Explorer that looks like this:

Not too bad, huh? In our sample script, we pause for 10 seconds and then replace our old message with a new message, one that informs the user that their logon script has completed. We pause for 5 more seconds, and then dismiss our instance of Internet Explorer.

If you want to get a little fancier, you can do a couple things. In the revised script we’ll show you momentarily, we use the WMI class Win32_DesktopMonitor to determine the current video resolution (e.g., 1024x768). We then use some simple math to position the IE window in the middle of the screen. For example, if our screen is 1024 pixels wide, we subtract 400 (the width of our Internet Explorer window) from 1024. We divide that number by two, leaving us with the pixel position for the left side of our window. Repeating this trick with the display height (768) gives us the pixel position for the top of our window, and thus centers our dialog box on screen. Here’s the code that gets the screen width and height:

strComputer = "."
Set objWMIService = GetObject("Winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_DesktopMonitor")
For Each objItem in colItems
    intHorizontal = objItem.ScreenWidth
    intVertical = objItem.ScreenHeight
Next

And here are the two lines of code that position the window on screen:

objExplorer.Left = (intHorizontal - 400) / 2
objExplorer.Top = (intVertical - 200) / 2

Note. The preceding code is really designed for computers that have only a single monitor; things get a bit more complicated on a multi-monitor system, especially if one of those monitors is turned off. For now, we’ll assume a single monitor; we’ll deal with the multi-monitor issue somewhere down the road.

In addition to centering the Internet Explorer window, we reinforce the fact that the user needs to wait for a moment by setting the cursor to an hourglass. That’s done with this line of code:

objExplorer.Document.Body.Style.Cursor = "wait"

Later in the script we set the cursor to default, which dismisses the hourglass and brings back the standard arrow cursor.

Here’s what our new and improved script looks like:

On Error Resume Next

strComputer = "."
Set objWMIService = GetObject("Winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_DesktopMonitor")
For Each objItem in colItems
    intHorizontal = objItem.ScreenWidth
    intVertical = objItem.ScreenHeight
Next

Set objExplorer = CreateObject _
    ("InternetExplorer.Application")

objExplorer.Navigate "about:blank"
objExplorer.ToolBar = 0
objExplorer.StatusBar = 0
objExplorer.Left = (intHorizontal - 400) / 2
objExplorer.Top = (intVertical - 200) / 2
objExplorer.Width = 400
objExplorer.Height = 200
objExplorer.Visible = 1

objExplorer.Document.Body.Style.Cursor = "wait"

objExplorer.Document.Title = "Logon script in progress"
objExplorer.Document.Body.InnerHTML = "Your logon script is being processed. " _
    & "This might take several minutes to complete."

Wscript.Sleep 10000

objExplorer.Document.Body.InnerHTML = "Your logon script is now complete."

objExplorer.Document.Body.Style.Cursor = "default"

Wscript.Sleep 5000

objExplorer.Quit

Still not fancy enough for you? Well, another way to spice things up is to include an animated .GIF in your InnerHTML. For example, this line of code displays an animated .GIF as well as a message:

objExplorer.Document.Title = "Logon script in progress"
objExplorer.Document.Body.InnerHTML = "<img src='file:///C:\Scripts\watch.gif'> " & _
    "Your logon script is being processed. This might take several minutes to complete."

The end result:

It might not qualify as art, but we think it does qualify as doing “…something interesting while the code executes.” And, remember, you can change the picture alignment, you can change the font size and color -- you can pretty much do anything HTML allows you to do.

Note. If you want to do something really interesting while your code executes, visit Dr. Scripto’s Fun Zone and learn how you can incorporate Microsoft Agent technology into your scripts.


How Can I Ensure That a Script Runs Only Once on a Computer?

Hey, Scripting Guy! How can I ensure that a script runs only once on a computer?

-- ML

Hey, ML. You know, we were going to start off today’s column by saying, “Show us your bobbleheads!” However, we didn’t want to have to explain to Microsoft people that there was nothing risqué or offensive in that statement; instead, all we wanted is to see what people have done with their Dr. Scripto Bobblehead dolls. (Hard as this is to believe, not everyone who works at Microsoft is a fun-loving, carefree type who enjoys a good laugh every now and then.) So while we do want people to show us their bobbleheads, we’re not going to ask people to show us their bobbleheads. After all, the last thing the Scripting Guys want to do is get themselves into trouble.

In other words, whatever you do, don’t show us your bobbleheads. Thank you.

Instead, if you were one of the lucky 250 people who won a bobblehead during the 2007 Winter Scripting Games we’d love to know what you’ve done with it. Just snap a digital photo and send it to scripter@microsoft.com. We’ll publish your photo in the Script Center, right alongside the cool picture we received from Switzerland’s very own Ben Simkins:

Note. Yes, that is Dr. Scripto. How are you supposed to tell the difference between Dr. Scripto and Scripting Guy Jean Ross? Here’s a hint: Dr. Scripto doesn’t have any staples in his head.

OK, let’s get down to business. To tell you the truth, we don’t know of a foolproof way to ensure that a script runs only once on a computer; there might very well be such a way, but it’s probably too complicated to configure and even more complicated to enforce. Therefore, we’ve come up with a simpler solution, one that might not be 100% foolproof, but at least is easy to implement.

What we’re going to do is this: each time the script in question runs, the first thing it’s going to do is check a value in the registry. If that value is No that means the script has never run on this computer. As a result, the script will go ahead and run, and then, upon completion, change the registry value to Yes. Logically enough, if the registry value is Yes that means that script has run on the computer. So what happens if you try to run the script a second time? That’s easy: the script checks the registry, sees that it has already run, and then automatically terminates itself. Not the fanciest approach in the world, but it should work.

Of course, before we can actually get the script to work we need to create a registry value. To accomplish that task we need to do two things. First, we need to create a new registry key (in the HKEY_LOCAL_MACHINE hive) named AdminScripts. Once that’s done we’ll then need to create a new registry value named Script 1 inside the AdminScripts key. Here’s what the script that accomplishes both of those tasks looks like:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\AdminScripts"
objRegistry.CreateKey HKEY_LOCAL_MACHINE, strKeyPath

strValueName = "Script 1"
strValue = "No"
objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Note: If you run this script on Windows Vista, you need to open the command prompt window as an administrator. Right-click Command Prompt in the Start menu and select Run As Administrator. We remind you of this because if you don’t Run As Administrator, the script will run just fine with no errors. This seems fine, until you discover that you don’t have a new registry key.

As you can see, we start out by defining a constant named HKEY_LOCAL_MACHINE and setting the value to &H80000002; this constant tells the script which registry hive we want to work with. We then connect to the WMI service on the local computer, binding directly to the StdRegProv class, which just happens to live in the root\default namespace:

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

And sure, you can run this script against a remote computer; just assign the name of that remote computer to the variable strComputer. For example, this line of code causes the script to run against the remote machine atl-fs-01:

strComputer = "atl-fs-01"

Once we’ve gotten this far, creating the new registry key is a piece of cake: all we have to do is assign the registry path to a variable named strKeyPath and then call the CreateKey method. That’s what we do with these two lines of code:

strKeyPath = "SOFTWARE\AdminScripts"
objRegistry.CreateKey HKEY_LOCAL_MACHINE, strKeyPath

As soon as the registry key exists we can then add a new value to that key. To do that we first assign the value name to a variable named strValueName; after that we assign the default value (No) to a variable named strValue. That’s what these two lines of code are for:

strValueName = "Script 1"
strValue = "No"

And then we simply call the SetStringValue method, which not only creates the Script 1 registry value but – as an added bonus – assigns it the value No to boot:

objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

That was easy, wasn’t it? We might add that we opted to approach today’s problem by splitting the task into two parts: first we created the registry key and value (that’s the part we just showed you), then we created (or, to be more accurate, are about to create) a second script, the script that we want to run only once per computer. Could we have combined these two scripts into a single script, one that checks for the presence of the registry value and, if it doesn’t exist, goes ahead and creates it? Sure. For information on verifying whether or not a registry value exists see this column from our oldies-but-goodies section.

Note. Why didn’t we just combine these two scripts in the first place? Mainly because we wanted to keep things simple: in our experience, it’s typically easier for people to read and understand two 10-line scripts than it is to read and understand one 20-line script. And trust us, when it comes to being simple no one is simpler than the Scripting Guys.

So what about that other script, the one we want to run only once on a computer? Well, here’s a quick-and-easy version that demonstrates the concept:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\AdminScripts"
strValueName = "Script 1"
objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

If strValue = "No" Then
    Wscript.Echo "This script will now run."
    strValue = "Yes"
    objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue
Else
    Wscript.Quit
End If

Note: Same thing here about Windows Vista: Run As Administrator if you actually want this script to work.

As you probably noticed, there are quite a few similarities between this script and the one we just showed you. As we did before, we define a constant named HKEY_LOCAL_MACHINE and then connect to the WMI service. Once that’s done we assign values to the variables strKeyPath (the path within the registry hive) and strValueName (the name of the registry value we want to read). That brings us to this line of code:

objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Here we’re using the GetStringValue method to read the registry value Script 1. As you can see, we pass this method four parameters:

· HKEY_LOCAL_MACHINE, representing the registry hive.

· strKeyPath, representing the path with that hive.

· strValueName, representing the registry value we want to read.

· strValue, an “out” parameter for storing the value read from strValueName.

And you’re right: we didn’t assign a value to strValue, did we? That’s the beauty of out parameters: we don’t have to do anything. Instead, we merely provide a variable name to the GetStringValue method, and GetStringValue then goes ahead and assigns the appropriate value to the variable for us.

Just WMI’s way of saying thanks.

Needless to say, our next step is to determine what value did get assigned to strValue:

If strValue = "No" Then

If strValue is No that means that the script hasn’t been run on this machine yet. Therefore, we go ahead and run this script. In our sample script, that simply means echoing a value back to the screen:

Wscript.Echo "This script will now run."

All you have to do is replace the preceding line of code with the line or lines that you want your script to execute.

Now that the script has run we need to change the value in the registry from No to Yes. That’s what this block of code is for:

strValue = "Yes"
objRegistry.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

Nothing too fancy here: we assign the value Yes to the variable strValue, then call the SetStringValue method to write the new value to the registry.

Now, suppose strValue didn’t equal No. In that case, we’re assuming two things: that strValue equals Yes, and that the script has already run on this computer. What do we do in that case? We do what the Scripting Guys do best: nothing. Instead, we just call the Quit method and terminate the script:

Wscript.Quit

And that should do it. Again, this isn’t foolproof: obviously someone could go into the registry and manually change the value of Script 1 from Yes to No. If that happens then the script can be fooled into thinking it’s never run on this particular machine. But it’s generally safe to assume that won’t happen.

Note. If that is a concern you could set up auditing on the key in question or monitor that key for changes. That won’t prevent people from making the change, but at least you will be notified if and when they do.

As for sending us pictures, well, we realize that most people didn’t win a Dr. Scripto bobblehead doll. What can we do for those of you who didn’t win? Nothing. Next time try a little harder, huh?

No, hey, just kidding. As it turns out, the Scripting Guys have a few bobbleheads leftover, bobbleheads which are currently being held under lock-and-key at Fort Knox. We’ll be giving away a bunch of those at TechEd 2007; if you’re planning on going to TechEd be sure and swing by the TechNet Magazine booth, say hi to the Scripting Guys, and sign up for the drawing. And if you’re not going to the conference, well, don’t despair; in conjunction with TechEd we’ll be holding a little contest which will give everyone a chance to win a bobblehead. We’ll provide more details on that in a few weeks.

Just remember, if you do win, don’t show us your bobblehead. Just send us a picture of your prize instead.


How Can I Force Users to Type Command-line Arguments in a Specific Order?

Hey, Scripting Guy! How can I force users to type command-line arguments in a specific order?

-- JA

Hey, JA. You know, the Scripting Guy who writes this column has a 16-year-old son. He coaches Colt League baseball. He works every day with all the other Scripting Guys. With that background, it probably come as no surprise to hear that he doesn’t believe it’s possible to get anyone to do anything, let alone get them to type command-line arguments in a specific order. So, sorry, but as far as we know there’s no way to force users to type command-line arguments in a specific order (e.g., myscript.vbs atl-fs-01 alerter, where atl-fs-01 is the name of a computer and alerter is the name of a service). It just can’t be done.

But wait, don’t go: the Scripting Guys never let anyone leave empty-handed. Let’s think about what it is you really want to do here. We’re guessing you don’t really care whether the computer name is entered before the service name; you’re just looking for a way to know which command-line argument is which. And that makes sense; after all, these two commands are going to produce very different results:

· myscript.vbs atl-ws-01 alerter

· myscript.vbs alerter atl-ws-01

You’d like to force users to type the computer name first, because then you can be assured that the first argument will always be the computer name and the second argument will always be the service name. We’re assuming that the order doesn’t really matter to you: you just want to be able to identify which argument represents the computer name and which argument represents the service name.

If it’s true that you don’t really care about order you just care about being able to identify which argument is which, well, then here’s your answer:

If Wscript.Arguments.Named("s") = "" Or Wscript.Arguments.Named("c") = "" Then
    Wscript.Echo "You must specify both the service and the computer using syntax like this:"
    Wscript.Echo
    Wscript.Echo "myscript.vbs /s:alerter /c:atl-ws-01"
    Wscript.Quit
End If

Wscript.Echo "Service: " & Wscript.Arguments.Named("s")
Wscript.Echo "Computer: " & Wscript.Arguments.Named("c")

What we’re doing in this script is taking advantage of “named arguments” in Windows Script Host. We can’t force users to type command-line arguments in a particular order; however, we can require users to specify arguments by name. In this script, we require people to include an argument named s and an argument named c. In other words, you need to start the script using one of the following statements:

· myscript.vbs /s:alerter /c:atl-ws-01

· myscript.vbs /c:atl-ws-01 /s:alerter

If either (or both) of the two arguments are missing, the script will display some usage instructions and then quit. For example, suppose you try starting the script like this, with nary a /s or a /c in sight:

myscript.vbs alerter atl-ws-01

Here’s what you’ll get back:

You must specify both the service and the computer using syntax like this:

myscript.vbs /s:alerter /c:atl-ws-01

Is this foolproof? No; obviously a user could type /s (for service) and then type the computer name, like so:

/s:atl-ws-01

We can’t guard against that. But, at the very least, there will be no doubt as to which argument is supposed to represent the service name. And that’s all we were hoping to do.

So how does the script work? Well, the key is the very first line of code:

If Wscript.Arguments.Named("s") = "" Or Wscript.Arguments.Named("c") = "" Then

As you can see, we’re checking two things here: whether or not the named argument s is an empty string or whether or not the named argument c is an empty string. Suppose that argument s is equal to an empty string. That means one of two things: either you didn’t include that particular argument when starting the script, or you didn’t supply that argument with a value (for example, you started the script using the command myscript.vbs /s: /c:atl-fs-01). If no argument (or no value) can be found for either the named argument s or the named argument c then this block of code executes:

Wscript.Echo "You must specify both the service and the computer using syntax like this:"
Wscript.Echo
Wscript.Echo "myscript.vbs /s:alerter /c:atl-ws-01"
Wscript.Quit

This code simply echoes back usage instructions and then uses the Quit method to terminate the script. In other words, if you don’t supply the proper arguments the script won’t run. Period.

But suppose you do supply values for both arguments. In that case the script uses these two lines of code to echo back the values for the two arguments:

Wscript.Echo "Service: " & Wscript.Arguments.Named("s")
Wscript.Echo "Computer: " & Wscript.Arguments.Named("c")

Of course, you’d probably do something a bit more interesting with those values – like, say, connect to computer c and then stop service s. But that’s up to you. The important thing is that we can require people to enter specific arguments, and we can identify which argument is which. And all with a minimal amount of code.

A couple quick notes here. To begin with, argument names are purely arbitrary: we went with s and c because they’re easy to type and easy to remember. However, we could have given these arguments any name we wanted; we would just need to modify the code accordingly. For example, if we wanted to use service and computer then the first line of our script needs to look like this:

If Wscript.Arguments.Named("service") = "" Or Wscript.Arguments.Named("computer") = "" Then

Second, keep in mind that the name of the argument is a tad bit different from the syntax used when specifying that argument. Our arguments are named s and c. However, when we actually use those arguments we need to use this syntax: /argument_name:argument_value. Don’t type something like this; it won’t work:

myscript.vbs s alerter c atl-ws-01

Type this instead:

myscript.vbs /s:alerter /c:atl-ws-01

One more thing: if an argument value includes a blank space make sure you enclose that value in double quote marks. For example, suppose you want to stop the service Automatic Updates. In that case, you need to type the following command:

arguments.vbs /s:"Automatic Updates" /c:atl-fs-01

And, of course, order doesn’t matter: if you’d rather type the computer name and then the service name, well, hey, why not?

Incidentally, if you’re new to the whole idea of command-line arguments you might want to take a look at the Tales from the Script column Scripts Like a Good Argument. And, no, we can’t force you to read that column. After all, we still can’t get our rightfielder to throw the ball to the cutoff man, let alone make someone read an article about scripting and command-line arguments. But we’re working on it.

Note. Which one are we working on? Forcing people to read our articles. We’ve given up on our rightfielder.


How Can I Get a Script to Delete Itself?

Hey, Scripting Guy! I have a script for installing a printer that I give to my end users. When the script finishes, I’d like it to be able to delete itself. How can I get a script to delete itself?

-- EA

Hey, EA. You know, that’s an interesting idea: disposable scripts, just like disposable razors or disposable cameras. Actually, they’re better than disposable razors or disposable cameras. After all, when you’re done with a disposable camera you still have to throw it away yourself. But the disposable script throws itself away! You could be on to something here, EA.

Of course, for hundreds of years philosophers have been debating whether or not it’s possible to create a disposable script; what makes the Scripting Guys think we could create such a thing on our first try? To be honest, we weren’t sure that we could, but we assigned all the scientists and technicians at Scripting Guys Laboratories to work on the problem anyway, telling hundreds of researchers that they couldn’t go home until they invented the disposable script. About a minute later they came up with this:

Set objFSO = CreateObject("Scripting.FileSystemObject")

For i = 1 to 5
    Wscript.Echo i
    Wscript.Sleep 1000
Next

strScript = Wscript.ScriptFullName
objFSO.DeleteFile(strScript)

Actually it’s even easier than it looks; after all, over the half the lines of code are there just so the script does something before it deletes itself. We begin by creating an instance of the Scripting.FileSystemObject; this is the object we’ll use to delete the script. We then enter a For Next loop which – as we noted – doesn’t really have anything to do with the script itself; it’s just there so that something happens in between the time we start the script and the time when the script deletes itself:

For i = 1 to 5
    Wscript.Echo i
    Wscript.Sleep 1000
Next

The excitement begins after the For Next loop finishes. For starters, we get the value of Windows Script Host ScriptFullName property. That property returns the complete path to the script, which we then store in the variable strScript:

strScript = Wscript.ScriptFullName

As you might expect, strScript now contains a path similar to this: C:\Scripts\My_Script.vbs. Once we have the complete path we can then delete the script simply by passing the variable strScript to the DeleteFile method:

objFSO.DeleteFile(strScript)

What’s going to happen when the script runs? Well, it’s going to echo the numbers 1 through 5 to the screen (pausing one second between each echo). As soon as the script has finished with that chore it will then delete itself. Just like that.

We don’t blame you: that does sound like a major problem just waiting to happen, doesn’t it? After all, if the script is still running and it deletes itself shouldn’t that cause some kind of problem?

Well, it probably would if the script had to continually read from its .vbs file. However, when you run a script the entire script is loaded into – and then executes from – memory. At that point the .vbs file becomes irrelevant; the script no longer needs it or refers to it. For example, take a look at the following script:

Set objFSO = CreateObject("Scripting.FileSystemObject")
strScript = Wscript.ScriptFullName
objFSO.DeleteFile(strScript)

For i = 1 to 5
    Wscript.Echo i
    Wscript.Sleep 1000
Next

As you can see, here we start out by deleting the script and only then do we enter the For Next loop and echo numbers back to the screen. Can a script really run even though the script file has already been deleted? Give it a try and see for yourself. We can’t guarantee this approach will work with every script ever written, but it wouldn’t surprise us if it does.

Now, before we go here’s a historical fact most people aren’t aware of. A few years ago Microsoft actually tried developing a disposable Scripting Guy. The idea was that the disposable Scripting Guy would do his or her work and then automatically delete itself. So how did that turn out? To tell you the truth, we don’t know: everyone is still waiting for the disposable Scripting Guy to actually do some work. Apparently they made the thing a little too realistic, if you know what we mean.


How Can I Get the Command Window to Stay Open After Running a Command-Line Tool?

Hey, Scripting Guy! How can I get the command window to stay open after running a tool like Ping or Ipconfig?

-- DB

Hey, DB. Now this question takes us back. Back when one of the Scripting Guys first started here at Microsoft many people thought WMI and ADSI were way too hard for script writers to use. As a result, this Scripting Guy was discouraged from using WMI or ADSI; instead, he was urged to simply use VBScript as a way to call command-line tools. In fact, the first chapter this Scripting Guy ever drafted for what became the Microsoft Windows 2000 Scripting Guide was a chapter on managing event logs. It was also a chapter that had no scripting code whatsoever.

And you’re right: only the Scripting Guys could find themselves working on a scripting guide that included absolutely no scripting code.

Eventually, of course, the Scripting Guys – using a mixture of a little eloquent persuasion and a lot of whining and crying – were able to convince people that maybe it would be OK if something called the Microsoft Windows 2000 Scripting Guide actually had a script or two in it. Along the way, though, the Scripting Guys also learned a thing or two about calling command-line tools from within a script, which is why we’re able to answer your question.

We’re guessing that you have a script similar to this one, a script which runs the command-line tool Ipconfig.exe:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("ipconfig /all")

As you doubtless know, this script works just fine: a command window pops up and Ipconfig runs. The only problem is that the command window then closes before you can actually read the information IPconfig returned (unless you read really fast). That’s definitely a problem.

So how do we fix it? Here’s how:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("%comspec% /k ipconfig /all")

As you can see, this revised script retains the same basic structure as our original script: we create an instance of the Wscript.Shell object and then call the Run method to actually run the command-line tool. The difference lies in the way we call that command-line tool. In the original script we simply called the tool itself:

objShell.Run("ipconfig /all")

This time around, we use a very different syntax:

objShell.Run("%comspec% /k ipconfig /all")

The environment variable %comspec% represents the Windows command shell; this is equivalent to calling Cmd.exe (which, of course, would open up a command window). So why don’t we just call Cmd.exe? Well, suppose you have computers running Windows 98. On those machines the command shell is invoked by running Command.com; there is no Cmd.exe. Using %comspec% helps ensures that we’ll get a command window regardless of the version of Windows that the script is running on.

In other words, with this script we aren’t directly running Ipconfig; instead, we’re running an instance of the command shell, and passing that instance several parameters. The first such parameter is /k; this parameter tells the command shell to do whatever we ask it to and then remain open. (We’ve been told that the k is short for keep, as in “keep open” but we aren’t sure whether that’s true or not.) Alternatively, we could have used the parameter /c (c for close), which would have automatically closed the command window at the completion of its task.

What about the other parameters passed to the command shell? Those are simply the commands needed to run IPconfig: ipconfig /all. Want to use Ping.exe to ping IP address 192.168.1.1? This script will do that, and ensure that the command window stays open afterwards:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("%comspec% /k ping 192.168.1.1")

Want to run Net.exe in order to get – and then be able to view – a list of local user accounts? Okey-doke:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("%comspec% /k net user")

Want to – well, you get the idea.

If you tend to use command-line tools a lot in your scripts (and there’s nothing wrong with that; use whatever you find easiest/best) you might be interested in this column, which tells you how to change both the command window title and the command window colors. After all these years the Scripting Guys still have a soft spot in their hearts for scripts that call command-line tools. And still can’t figure out why people thought it was a good idea to write a scripting guide that didn’t have any scripts.


How Can I Get the Command Window to Stay Open When I Right-Click a Script and Choose Open With Command Prompt?

Hey, Scripting Guy! How can I get the command window to stay open when I right-click a .VBS file and choose Open With Command Prompt??

-- BB

Hey, BB. You know, according to BB’s email signature, he’s 9 years old. If that’s true, well, that’s absolutely amazing. A 9-year-old who knows VBScript? At age 9 the Scripting Guys could barely feed themselves or tie their own shoes.

Which, in their case, simply means that the more things change the more they stay the same.

Of course, we have to admit that, just because BB says he’s 9 years old doesn’t mean he really is 9 years old. After all, the Scripting Editor continues to insist that she’s 23 years old. But how, then, can she explain the photographs that clearly show her attending Franklin Delano Roosevelt’s Inauguration Ball in 1937? The truth is, she can’t.

Instead, she just throws things any time the subject is broached.

Anyway, we can’t be sure that BB really is 9 years old, but we’ll try to figure out a way to verify that, maybe by asking him a question that only a 9-year-old would know the answer to. You know, like this one: name all four Ninja Turtles.

Good point; after all, even the Scripting Guy who writes this column knows the answer to that: Leonardo, Donatello, Michelangelo, and Raphael. And that’s without using the Internet to look up the answer.

Note. Actually, the Scripting Guy who writes this column never uses the Internet, or anything else, to look up an answer. He just assumes he knows everything.

Of course, whether BB is really 9 years old or not doesn’t matter; either way he has a good question here. If you right-click a .VBS file one of the options available to you on the context menu is this: Open With Command Prompt. If you choose that option something seems to happen: typically a command window will briefly flash onscreen and then just as quickly disappear. And that’s it. Why doesn’t the script actually run, and why doesn’t the command window stay open?

Let’s take the first question first. (Kind of an unusual approach for Hey, Scripting Guy!, isn’t it?) As it turns out, the script actually does run; it’s just that many VBScripts execute so quickly that you don’t realize that anything happened. (About the same time the command window appears onscreen the script has completed its tasks and it’s now time for the command window to close.) Here’s one way to verify this. Create a simple script that does nothing more than display a message box:

Msgbox "Hey"

Now, right-click this .VBS file and choose Open With Command Prompt. You should see the message box, indicating that the script really did run after all.

So then why didn’t the command window stay open? That’s easy: by default the command window tends to do its thing and then disappear. For example, open the Run dialog box, type ipconfig, and then press ENTER. You’ll see a command window open and – if you’ve got good eyesight – you might even see that Ipconfig.exe has run. But the very instant that Ipconfig finishes, the command window automatically closes. That’s why you’re usually instructed to first open a command window and then run a script or command-line tool. If you don’t the script or tool will run, but you’ll probably never see the output.

Is that a problem, having to open a command window and then run the script from within that window? Of course it is; after all, that pretty much defeats the whole idea of having an Open With Command Prompt shortcut in the first place.

But we have a good news for you, BB: if you don’t like the way the Open With Command Prompt shortcut works then you can go ahead and change the behavior of this shortcut. How do you do that? Good question; might we suggest using a script like this one:

Const HKEY_CLASSES_ROOT = &H80000000

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "VBSFile\Shell\Open2\Command"
strValueName = ""
strValue = "cmd.exe /K cscript.exe " &
Chr(34) & "%1" & Chr(34) & " %*"

objRegistry.SetExpandedStringValue _
HKEY_CLASSES_ROOT,strKeyPath,strValueName,strValue

As it turns out, the behavior of the Open With Command Prompt shortcut is dictated by a setting in the registry (HKEY_CLASSES_ROOT\VBSFile\Shell\Open2\Command). By default, this setting simply calls the CScript or WScript script engine (depending on your preferred settings), passing along the file path as a command-line argument:

%SystemRoot%\System32\CScript.exe "%1" %*

If we want the command window to stay open any time we run a script using this shortcut all we have to do is modify the registry setting. How do we do that? We were just about to get to that.

If you take a look at our script you’ll see that we start things off by defining a constant named HKEY_CLASSES_ROOT and setting the value to &H80000000; we’ll use this constant to tell the script which registry hive we want to work with. We then connect to the WMI service on the local computer, binding to the StdRegProv class. (Which, we should point out, resides in the root\default namespace.)

Note. Can you run this script against remote computers? Of course you can; all you have to do is assign the name of the remote computer to the variable strComputer.

After connecting to the WMI service we then assign values to three variables:

· strKeyPath, which represents the registry path within the HKEY_CLASSES_ROOT hive. In this case we set the path to VBSFile\Shell\Open2\Command.

· strValueName, which represents the actual registry value we want to modify. As it turns out, we want to modify the default value, a value that shows up in Regedit like this: (Default). How do we indicate that we want to work with this default value? That’s easy: we just assign an empty string (“”) to strValueName.

· strValue, which represents the new value to be written to the registry.

Let’s talk a little bit about strValue. As we noted earlier, the command window tends to open up, do its thing, and then close. However, there is a way to keep the command window open after it completes its appointed tasks: you just have to use the /K (keep) switch when opening the window. And that’s exactly what we’re doing here. Notice that, this time, we aren’t directly starting Cscript.exe. Instead, we’re opening up a command window (Cmd.exe) and using the /K switch to ensure that the window stays open. That’s what this portion of our code is for:

cmd.exe /K

We then pass the name of the script host (CScript.exe) and the file path as additional command-line arguments for Cmd.exe. In other words, any time someone selects Open With Command Prompt we’re going to open a persistent version of the command window, then tell the command window that – when it opens – it needs to run CScript.exe and the selected .VBS file.

Confusing? Yes, it is, at least a little. And the syntax of our command – which includes the function Chr(34), something we use to insert double quote marks into the string value – doesn’t help much. The long and short of it, however, is that strValue ends up being assigned this value:

cmd.exe /K cscript.exe "%1" %*

And, believe it or not, that’s the command that will “fix” the Open With Command Prompt shortcut.

In fact, after assigning values to our variables all we have to do is call the SetExpandedStringValue method (because our registry value happens to have the REG_EXPAND_SZ data type) and we’re done:

objRegistry.SetExpandedStringValue _
    HKEY_CLASSES_ROOT,strKeyPath,strValueName,strValue

Now try right-clicking a .VBS file and selecting Open With Command Prompt. This time a command window should open, the script should run, and – upon completion – the command window should stay open. Pretty sweet, huh?

Again, BB, if you really are 9 years old the Scripting Guys are impressed. Or at least the Scripting Guy who writes this column is impressed. After all, was the Scripting Guy who writes this column fooling around with VBScript when he was 9? No he wasn’t, primarily because neither VBScript nor the command prompt had been invented way back then.

In fact, come to think, the number 9 hadn’t even been invented yet back when the Scripting Guy who writes this column was a kid. Wow ….


How Can I Hide the Command Window When Executing a Command Like net Localgroup Administrators?

Hey, Scripting Guy! How can I hide the command window when executing a command like net localgroup Administrators?

-- AA

Hey, AA. You know, before we begin we’d like to take a few minutes to recognize all those faithful readers who read the Hey, Scripting Guy! column each and every day. Hey, June; how’s it going?

No, that’s pretty much it. Maybe we should have said we wanted to recognize our faithful reader.

Hmmm, that’s an interesting idea: if we actually answered the questions instead of rambling on and on maybe we would have more than just one faithful reader. What the heck; we’re willing to try anything:

Set objShell = CreateObject("Wscript.Shell")
strCommand = "net localgroup Administrators"

Set objExec = objShell.Exec(strCommand)

Do Until objExec.Status
    Wscript.Sleep 250
Loop

Wscript.Echo objExec.StdOut.ReadAll()

To begin with, AA, we’re assuming that you’ve been using the Windows Script Host Run method in order to run net localgroup Administrators. And, yes, that will cause a command window to open up on screen. (Even worse, if you don’t include the proper parameters that window will open and then close before you ever have a chance to read the information that net localgroup Administrators returns.) What you want to do is run your command-line utility without a second command window opening up and then have the information returned from net localgroup Administrators displayed in the original command window. In other words, you’d like your entire session to look something like this:

C:\scripts>cscript hidden.vbs
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

Alias name     Administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
Ken Myer
kenmyer
FABRIKAM\InfoSec Secure Environment
FABRIKAM\Domain Admins
FABRIKAM\kenmyer

The command completed successfully.

Can we do that? You bet we can, as long as we use the Exec method rather than the Run method. When you run a command-line tool from Exec the script will not open up a second command window. Instead, the script runs in an invisible window, and the data returned by the tool gets stored in the StdOut property. All we have to do is read in the value of StdOut and we’ll have complete access to the returned data.

Which is exactly what we do in our script. We start out by creating an instance of the Wscript.Shell object, then assign the name of our command-line command (net localgroup Administrators) to a variable named strCommand. After doing that tiny bit of setup we can run the command using this single line of code:

Set objExec = objShell.Exec(strCommand)

As you can see, we call the Exec method, passing it the variable strCommand. At the same time we create an object reference named objExec. That’s required because the Exec method actually creates an instance of an object: the WshScriptExec object.

No, that’s not a bad thing; that’s a good thing. Because Exec creates an object we can then work with the properties of that object, properties like StdOut and Status. And that’s the key to creating this script. For example, take a look at the next block of code in our script:

Do Until objExec.Status
    Wscript.Sleep 250
Loop

What we’re doing here is waiting until the Status property of the WshScriptExec object is true. If Status is false (that is, equal to 0) that means that Exec is still running the command; in other words, net localgroup Administrators hasn’t finished doing whatever it is that net localgroup Administrators does. (We’re just kidding: it lists the members of the local Administrators group.) If Status is false we pause the script for 250 milliseconds (Wscript.Sleep 250), then loop around and check the value of the Status property again. This continues until Status is no longer equal to 0, which can mean only one thing: our command is finished.

As we noted earlier, when Exec runs a command-line utility that utility runs in a hidden window; in addition, any information returned by that utility is stored in Exec’s StdOut property. Logically enough, that means we can report back the results of running net localgroup Administrators simply by doing this:

Wscript.Echo objExec.StdOut.ReadAll()

So what are we doing here? We’re just using the ReadAll() method to read the value of StdOut, a value we then echo back to the screen. The net result? Although we don’t see any command windows or any other signs of activity, net localgroup Administrators runs in the background; when it’s done processing, the list of group members is displayed in the command window. Simple and elegant.

We hope that answers your question, AA. Oh: and see you tomorrow, June!


How Can I Launch Two Applications, Wait Until One Program Ends, and Then Close the Other?

Hey, Scripting Guy! Here’s what I’d like to do: I want a script that will launch two executable files. After the first application closes I’d like the script to close the second application and then quit. How can I do that?

-- MK

Hey, MK. You know, this is the kind of question we like. Why? Because it sounds really complicated and really hard, and if anyone wants anything from us we can just say, “You know, I am trying to write a script that will launch two applications, wait for the first one to close, and then automatically close the second one.” And inevitably they’ll say, “Oh, I’m sorry; obviously you’re very busy” and then leave us alone.

What they don’t know, of course, is that this only sounds hard. In truth, it’s no more difficult than this:

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

errResult = objWMIService.Create("calc.exe", null, null, intCalcID)
errResult = objWMIService.Create("notepad.exe", null, null, intNotepadID)

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

Set colProcesses = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceDeletionEvent " _
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")

Do Until i = 999
    Set objProcess = colProcesses.NextEvent
    If objProcess.TargetInstance.ProcessID = intCalcID Then
        Exit Do
    End If
Loop

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where ProcessID = " & intNotepadID)

For Each objProcess in colProcesses
    objProcess.Terminate()
Next

No, trust us: this is actually pretty simple once you know what the script is doing. We begin by connecting to the WMI service on a computer and, more specifically, binding to the Win32_Process class. That’s what we do here:

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

Next we use the Create method to create two new processes: Calc.exe and Notepad.exe. For each new process we use code similar to this:

errResult = objWMIService.Create("calc.exe", null, null, intCalcID)

All we’re doing is calling the Create method followed by:

· The name of the executable file (depending on how your computer is set up, you might have to specify the full path name for the application).

· A pair of Null parameters. These two parameters enable us to specify a different working folder for the application as well as configure some additional startup options. We aren’t going to worry about either of those things in this sample script, so we just set the parameter values to Null.

· A variable (named intCalcID) that functions as an “out parameter.” When our process gets created, the ProcessID number assigned to the process will also be assigned to our out parameter variable.

The net result is that we’ll start Calculator, and the variable intCalcID will contain the process ID assigned to that instance of Calculator. We’ll then start Notepad, and the variable intNotepadID will contain the ProcessID assigned to that instance of Notepad. This is how we start the applications, and how we’ll keep track of them.

What we need to do next is, well, basically nothing: we want the script to pause until Calculator has been dismissed. To do that, we reconnect to the WMI service and then use ExecNotificationQuery to monitor for any processes that are deleted. We need to reconnect to the WMI service because, at the beginning of the script, we connected specifically to the Win32_Process class; our object reference (objWMIService) thus refers only to that class. We need to connect to the “generic” WMI service, so we just reuse the object reference objWMIService and make a new connection:

Set colProcesses = objWMIService.ExecNotificationQuery _
    ("Select * From __InstanceDeletionEvent " _
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")

Why do we do that? Each time a process is deleted, an instance of the __InstanceDeletionEvent class is generated. We want to examine each of these instances and see whether it happens to be a process with our target ID, the ID assigned to intCalcID. If the deleted process has a different ID, then it’s not our instance of Calculator; in that case, the script will resume monitoring. If the deleted process does have the same ID as intCalcID, then it must be our instance of Calculator (because process IDs must be unique). In that case, we’ll want to stop monitoring, and then close Notepad.

Here’s the code that actually does the monitoring:

Do Until i = 999
    Set objProcess = colProcesses.NextEvent
    If objProcess.TargetInstance.ProcessID = intCalcID Then
        Exit Do
    End If
Loop

What we’ve done here is set up a loop that will run until the variable i is equal to 999. Now, the truth is, i will never equal 999; this is just a little trick to ensure that our loop runs until Calculator has been dismissed. (How do we know i will never equal 999? Well, we don’t assign a value to i; consequently, it takes on the default value of 0. And because we never do anything to change that value, i will always be 0, and thus never be equal to 999.)

Inside the loop we use this line of code to wait for the next deleted process:

Set objProcess = colProcesses.NextEvent

Each time a process is deleted we check to see if the ProcessID matches up with the process ID assigned to Calculator. If it does, we then use the Exit Do command to break out of our loop and continue with the script. If it doesn’t have the same ID, then we simply loop around and wait for the next deleted process. (Like we said, i will never equal 999, but that’s OK: the Exit Do command will always break us out of a loop.)

Note. We realize that we’ve kind of skimmed over the whole idea of event monitoring. If you’re a bit confused by things like __InstanceDeletionEvent and colProcesses.NextEvent you might want to take a look at the Scripting Guys webcast An Ounce of Prevention: An Introduction to WMI Events.

Now all we have to do is terminate the instance of Notepad we started. To do that we use this WMI query to retrieve a collection of all processes that have the process ID assigned to Notepad:

Set colProcesses = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where ProcessID = " & intNotepadID)

After we have that collection, we use this block of code to loop through the set of processes (there will be only one) and then use the Terminate method to close the application:

For Each objProcess in colProcesses
    objProcess.Terminate()
Next

Incidentally, this approach will work on remote computers as well as on local computers; just change the value of the variable strComputer to the name of the remote machine. Keep in mind, however, that on Windows XP and Windows Server 2003 the processes you start on the remote computer will run in an invisible window; they will not be visible on screen. That means that, when it comes to dealing with remote computers, this approach is very useful for applications that require no user interaction, but far less useful (in fact, downright useless) for applications that do require user intervention.


How Can I Pause a Script and Then Resume It When a User Presses a Key on the Keyboard?

Hey, Scripting Guy! I would like to be able to pause my script, then have it resume as soon as the user presses any key on the keyboard. How do I do that?

-- AL

Hey, AL. Boy, this question takes us back: “Press any key to continue,” along with “Abort, Retry, or Fail,” has to be one of the most famous phrases in computing history. Excuse us for a second while we sigh and think about the good old days. Sigh ….

Back in the old days “Press any key to continue” was a cornerstone of nearly every batch file ever written. All you had to do back then was insert a pause command in the middle of the batch file; at that point, the batch file would halt and – like a faithful old dog – just sit there and wait for you to press a key on the keyboard. As soon as you pressed a key – any key – the batch file would resume again. Anyone interested in taking a walk down memory lane can try this simple little batch file to see how it all worked:

echo off
pause
echo The batch file is complete.

Make sure you save this as a .bat file, not a .vbs file.

No doubt many of you are thinking, “Wow, scripting is so much better and so much more powerful than batch files. I bet there’s a really cool way to do Press any key in a script.” To tell you the truth, we would have thought that way, too. But guess what: there isn’t a way to replicate this function in a script.

OK, there might be a goofy workaround of some kind, but off the top of our heads we couldn’t find one. The problem is that Windows Script Host isn’t designed to be an event-driven environment; in other words, WSH doesn’t know how to sit around and wait until an event (such as a key being pressed) occurs. Combine that with a limited ability to interact with the command line, and you end up with a scripting environment that can’t replicate the functionality of the pause command.

We know: that wasn’t exactly the answer you were hoping to get. But we do have a consolation prize for you. We can’t replicate the pause command, in which the computer sits and waits until you press any key on the keyboard. However, we can give you some code that will sit and wait until you press the ENTER key on the keyboard (and, yes, it has to be the ENTER key). This isn’t exactly what you wanted, but it will work:

strMessage = "Press the ENTER key to continue. "
Wscript.StdOut.Write strMessage

Do While Not WScript.StdIn.AtEndOfLine
   Input = WScript.StdIn.Read(1)
Loop
WScript.Echo "The script is complete."

What this script does is display a message on screen, and then use StdIn (found only in WSH 5.6, which means this script runs only if you have WSH 5.6 installed) to wait for the user to input data from the command line. StdIn sits there until you press the ENTER key; as soon as you press ENTER, the script resumes. What if you press some key other than ENTER? No big deal: the value of the key you pressed will appear on screen, as will the values of any other keys you press. The script will simply display the value of any key you press until you finally hit ENTER. At that point, the script breaks out of the loop and continues.

In case you’re wondering, StdIn is really intended as a way for you to use the command line to enter data into a script. For example, your script might prompt you to enter a user name or a file name. That’s why it waits for you to press the ENTER key, as a way of indicating that data entry is complete. Otherwise, you might type the first letter of the user name only to see StdIn grab that and start running with it. The ENTER key is the official signal that data entry is done.


How Can I Prompt a User for Information and Then Pass That Information to a Command-Line Tool?

Hey, Scripting Guy! How can I prompt a user for information and then pass that information to a command-line tool?

-- SD

Hey, SD. efh;r9d/fjwf

Sorry; let’s try that again. j ewewrj 2gr CX # 2kkkkw

Sorry; the Scripting Guy who writes this column is having a little difficulty moving his arms today. Last night the Scripting Son thought it would be “fun” to go to the gym and swim laps for 40 minutes instead of going to the gym and riding the exercise bike for 40 minutes.

Did we ever mention how dumb the Scripting Son is?

Note. OK, good point: if the Scripting Son is dumb for suggesting that the two swim laps for 40 minutes, what does that make the Scripting Dad for agreeing to that suggestion? Don’t bother; we all know the answer to that question. Making things even worse is the fact that the Scripting Dad knows better. Back in his high school days, one of his PE teachers offered to give anyone an A for the quarter if they could complete a one-mile swim in a specified amount of time. The Scripting Guy who writes this column was able to complete the swim and earn his A. Several days later, when he finally managed to crawl out of the pool, he vowed never to do anything that dumb ever again.

On the bright side, though, suppose the Scripting Dad finds himself on a boat that’s a mile or so off shore and that boat suddenly sinks. After last night, the Scripting Dad knows that he’ll be able to swim all the way back to land, no problem.

Provided, of course, that the water has walls every 50 yards or so that he can rest on.

Of course, the question on everyone’s mind right now is this: will a little thing like not being able to move his arms keep the Scripting Guy who writes this column from actually writing this column? Given his druthers, yes it would. Needless to say, however, the Scripting Guy who writes this column never seems to get his druthers:

Set objShell = CreateObject("Wscript.Shell")

strName = InputBox("Please enter the user name:")

If strName = "" Then
    Wscript.Quit
End If

strCommand = "%comspec% /k dsquery user -name " & Chr(34) & strName & chr(34)
strCommand = strCommand & " | dsget user -tel"

objShell.Run strCommand

According to your email, SD, you noted that what you really need to do is prompt a user to enter a name, then use the command-line tools Dsquery and Dsget to search Active Directory, locate the user with that name, and then display his or her telephone number. Just to keep everyone else in the loop, here’s the sample command SD sent, with the variable x representing the user name:

dsquery user –name x | dsget user –tel

The script we just showed you? That script will run this very command, substituting the name typed in by the user for the variable x.

No, we don’t have water on the brain. Well, not much anyway. Listen, maybe it will help if we explain how the script works. As you can see, we begin by creating an instance of the Wscript.Shell object; we’ll use that object when it comes time to call our command-line tools. We then use this line of code to pop up an InputBox, solicit a user name, and then store that name in a variable named strName:

strName = InputBox("Please enter the user name:")

Our next step is optional, but seemed like a good idea. If you type a name into an InputBox and then click OK (or press ENTER) that name will get stored (in this example) in the variable strName. But what happens if you click the Cancel button instead? In that case, an empty string value (“”) gets stored in strName. On top of that, if you click Cancel that probably means you want to cancel the script. (Yes, we Scripting Guys are pretty darn smart, aren’t we?) Therefore, we inserted this block of code to determine whether or not strName contains an empty string. If it does, we call the Quit method and terminate the script:

If strName = "" Then
    Wscript.Quit
End If

Hey, that’s why they pay us the big bucks.

We’re now ready to compose our command-line string (and yes, we usually find it easiest to compose the command-line string, store it in a variable, and only then run the command). This isn’t particularly difficult, but there are a couple of things to watch out for:

· Just running the two command-line tools isn’t good enough: the tools will run, but the output will scroll past too quickly to be read. Instead, we need to open a new command window, run the tools, then make sure that the command window stays open when the tools are finished. In our example, that’s what %comspec% /k does (%comspec% opens the command window and the /k keeps the window open).

· We have to include the double quote marks in the command. Admittedly, embedding double quotes in a string variable, as we’re about to do, can be a little tricky. But we’ll show you a pretty straightforward way to take care of that.

In order to keep things simple we opted to use two steps in order put our command-line string together. Here’s step 1:

strCommand = "%comspec% /k dsquery user -name " & Chr(34) & strName & chr(34)

What are we doing here? First we’re opening a new window and ensuring that the window stays open after Dsquery and Dsget finish running; again, that’s what the %comspec% /k is for. We then issue the basic Dsquery command: dsquery user –name. That brings us to the tricky part: specifying the user name, making sure that name is enclosed in double quote marks.

You know, now that we look at the script, it turns out that this wasn’t that tricky after all, was it? Notice that when we included the Dsquery command we put a blank space after the –name parameter; that’s gives us a ready-made spot for entering the user name. That’s something we do using this reasonably-simple syntax: Chr(34) & strName & Chr(34).

What’s all that mean? Well, for starters, the Chr function takes an ASCII value and substitutes the actual character corresponding to that value. As it turns out, the double quote mark has an ASCII value of 34; thus Chr(34) is going to insert a double quote mark for us.

After that we insert the variable strName and a second double quote mark. Assuming you typed in Ken Myer as the user name, the variable strCommand will now be equal to this:

%comspec% /k dsquery user –name "Ken Myer"

Pretty good, huh?

We then use this line of code to add in the second half of our command:

strCommand = strCommand & " | dsget user -tel"

All we’re doing here is setting the value of strCommand to the current value of strCommand plus this: | dsget user –tel. In turn, that’s going to make strCommand equal to this:

%comspec% /k dsquery user –name "Ken Myer" | dsget user -tel

Which, coincidentally enough, is the very command we need in order to get Dsquery and Dsget to retrieve Ken Myer’s phone number.

In fact, all that’s left now is to call the Run method:

objShell.Run strCommand

We should point out that this script is simply going to call Dsquery and Dsget and then display Ken Myer’s phone number in the command window. Is it possible to grab that phone number and then use it elsewhere in your script? As a matter of fact it is, but that requires you to do a little bit more coding (and to use the Exec method rather than the Run method). For more information, see the Tales from the Script column “Running Programs From WSH Scripts,” which, for you trivia buffs, happens to be the very first column published in the Script Center.

And yes, it probably is worth a lot of money. Feel free to make us an offer.

As for the Scripting Guy who writes this column, at this point all he can say is dsfhg3e^t r4.

And this time, he means it.


How Can I Repeatedly Prompt a User to Enter a File Name?

Hey, Scripting Guy! I have approximately 100 Excel files (all with names like 9365.xls) in a folder. I’d like to prompt a user for a file name, open the file, retrieve information from a specified cell, then close the file (ideally Excel would never appear on screen). I’d then like to prompt the user to enter another file name. How can I do that?

-- TW

Hey, TW. You know, even though we’ve been writing this column for over two years now it never ceases to amaze us that people are brave enough to ask the Scripting Guys for help any time they need to do something practical. Why does that amaze us? Well, last night, for example, the Scripting Guy who writes this column decided it was time to bake something. The recipe he opted to use called for one egg white. Before going to the store for the rest of the ingredients he checked to see if the Scripting Family had any eggs; they had one. But that’s all he needed, right?

After returning from the store our hero began mixing the batter. Being as good a chef as he is a scripter, he broke his lone egg in half and, standing over the sink, carefully poured the egg from one eggshell to the other, meticulously separating the white from the yolk. When he was down, the egg had been perfectly separated: the yolk was left in the shell, and the egg white – the part he needed – had been washed down the drain. Gee; how … amusing ….

Note. Fortunately, the Scripting Guy who writes this column is used to doing things like that; in fact, he finds himself running back to the store so often that they’ve given him his very own parking place.

What that all boils down to is this: if you’d rather withdraw your question, TW, and ask someone a bit smarter, well, we understand; no hard feelings. If you’re willing to take a chance, however, here’s a script that (we hope) will do the trick:

Do Until i = 1
    strFile = InputBox("Please enter the file name: ")

    If strFile = "" Then
        Exit Do
    End If

    strPath = "C:\Scripts\" & strFile

    Set objExcel = CreateObject("Excel.Application")
    Set objWorkbook = objExcel.Workbooks.Open(strPath)
    Set objWorksheet = objWorkbook.Worksheets(1)

    Wscript.Echo objWorksheet.Range("Status")
    objExcel.Quit
Loop

Let’s see if we can figure out how this baby works. As you can see, we start out by creating a Do loop designed to loop until the variable i is equal to 1. This is how we’re able to re-prompt the user over again: because the variable i will never be equal to 1 this loop will cause the script to run either for forever or until the user clicks Cancel in the InputBox or clicks OK without entering a file name, whichever comes first.

Speaking of the InputBox, the very first thing we do inside the Do loop is use the InputBox method to prompt the user to enter a file name:

strFile = InputBox("Please enter the file name: ")

The InputBox itself stays onscreen until the user clicks OK or Cancel; when that happens the results are stored in a variable named strFile. If the user clicks OK strFile will contain the file name entered into the InputBox; if the user clicks Cancel strFile will be equal to an empty string. With that in mind our next task is to check the value of strFile. If strFile is equal to nothing (“””) we call the Exit Do statement and exit the loop:

If strFile = "" Then
    Exit Do
End If

But what if strFile isn’t equal to nothing? In that case we append the file name (that is, the value of strFile) to the string C:\Scripts\; this enables us to construct a full path to the file we want to open. (For example, if the user types 9365.xls into the InputBox the path we construct will end up being C:\Scripts\9365.xls.) We then use this block of code to create an instance of the Excel.Application object, open the file whose path has been stored in strPath, and then bind to the first worksheet in the workbook:

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open(strPath)
Set objWorksheet = objWorkbook.Worksheets(1)

With the worksheet open our next task is to echo back the desired value. In this script, we’re assuming you’ve created a one-cell range named Status which contains the status information of interest. If that’s the case you can echo back the status information using code like this:

Wscript.Echo objWorksheet.Range("Status")

Alternatively, you can simply specify the address of the cell containing the status information. For example, if status is kept in cell A10 (that is, row 10, column 1) you can use this code to echo back the value of cell A10:

Wscript.Echo objWorksheet.Cells(10,1).Value

We then close Excel, loop around, and repeat the process. And no, Excel never does appear onscreen; instead, it runs invisibly in the background. That’s because we never set the Visible property to True.

That should get you started, TW. Ideally, you’d include some error-checking in here; for example, the script is doomed to fail if the file entered by the user doesn’t actually exist. Even better, you might consider popping up a File Open dialog box and allowing users to select a file without having to type in a file name. We won’t discuss this modified script today; for more information on adding a File Open dialog box to a script see our previous column on this very subject. As long as you’re running on Windows XP, however, you can accomplish the same feat using this script:

Do Until i = 1
Set objDialog =
CreateObject("UserAccounts.CommonDialog")
objDialog.Filter = "Excel Spreadsheets|*.xls"
objDialog.FilterIndex = 1
objDialog.InitialDir = "C:\Scripts"
intResult = objDialog.ShowOpen

If intResult = 0 Then
Exit Do
Else
strPath = objDialog.FileName
End If

Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open(strPath)
Set objWorksheet = objWorkbook.Worksheets(1)
Wscript.Echo objWorksheet.Range("Status")
objExcel.Quit
Loop

Incidentally, if anyone out there needs an egg yolk just let us know; we – alas – happen to have one we aren’t using.


How Can I Run a Script Against a Range of IP Addresses?

Hey, Scripting Guy! I’d like to run a script against all the computers on a subnet. Is there a way to do that without having to hardcode all the IP addresses into the script?

-- RB

Hey, RB. Based on your email, it sounds like you have a setup similar to this: you have a subnet with IP addresses ranging from 192.168.1.1 to 192.168.1.254. You’d like to create a script that will start with the first IP address, run some code against that computer, move on to the second address, run that same code, and continue in this same vein until you’ve hit each and every computer. Furthermore, you’d like to do that in as few lines of code as possible, and without having to hardcode a couple hundred IP addresses.

So is there a way to do this? Of course there is, and it’s actually easier than you think.

To start with, let’s show you how to loop through a range of IP addresses. This is sample code, so all it does is echo the name of each IP address. After we explain how this script works, then we’ll show you a more practical example:

On Error Resume Next

intStartingAddress = 1
intEndingAddress = 254
strSubnet = "192.168.1."

For i = intStartingAddress to intEndingAddress
    strComputer = strSubnet & i
    Wscript.Echo strComputer
Next

No, really, that’s the entire script. We start by assigning some variables: intStartingAddress gets assigned the value 1; intEndingAddress gets assigned the value 254; and strSubnet gets assigned the value 192.168.1. (note the period after the 1). As you might have guessed, these values will serve as the building blocks for constructing our IP addresses.

After assigning the variables, we create a For-Next loop that runs from 1 (intStartingAddress) to 254 (intEndingAddress). Why do we loop from 1 to 254? That’s easy: that’s your IP range. What if your IP range is 192.168.1.7 to 109.168.1.54? No problem: use the exact same loop, but just change the value of intStartingAddress to 7 and intEndingAddress to 54.

Inside our loop, we concatenate the string value 192.168.1. with the current value of our loop variable (i). The first time we run through the loop – when i equals 1 – we combine 192.168.1. and 1. And guess what? That gives us value of 192.168.1.1, which just happens to be our first IP address. The last time we run through the loop, we’ll combine 192.168.1. and 254, giving us the value of our last IP address, 192.168.1.254. Run this script, and you’ll get output like this:

192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.4

Pretty easy, huh?

Of course, you’re probably not all that interested in echoing a bunch of IP addresses; you’d like to run some WMI code instead. That’s fine:

On Error Resume Next

intStartingAddress = 1
intEndingAddress = 254
strSubnet = "192.168.1."

For i = intStartingAddress to intEndingAddress
    strComputer = strSubnet & i

    Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_OperatingSystem")
    For Each objItem in ColItems
        Wscript.Echo strComputer & ": " & objItem.Caption
    Next

Next

As you can see, we again set the value of the variable strComputer to an IP address. We then simply connect to the WMI service on the computer represented by that address. That’s an easy thing to do, because WMI can connect to computers using IP addresses as well as using computer names.

Now we’d like to add one more little wrinkle. You mentioned in your email that there are a few IP addresses you’d like to exclude, IP addresses that probably represent routers or something. Okey-doke. Here’s a modified script that uses a Select Case statement to exclude certain computers:

intEndingAddress = 254
strSubnet = "192.168.1."

For i = intStartingAddress to intEndingAddress
    Select Case i
        Case 10
        Case 50
        Case 100

        Case Else
            strComputer = strSubnet & i
            Set objWMIService = GetObject _
                ("winmgmts:\\" & strComputer & "\root\cimv2")
            Set colItems = objWMIService.ExecQuery _
                ("Select * From Win32_OperatingSystem")
            For Each objItem in ColItems
                Wscript.Echo strComputer & ": " & objItem.Caption
            Next

    End Select
Next

Notice what happens when the value of i (the last octet in the IP address) is equal to 10, 50, or 100. That’s right: nothing happens. If a computer has an IP address of 192.168.1.10, 192.168.1.50, or 192.168.1.100, nothing happens; no WMI code will run, and the script will simply loop around. The WMI code will execute only for computers that have IP addresses other than those three. A simple but effective way to exclude specific IP addresses from the WMI portion of the script.


How Can I Run a Script Under Alternate Credentials?

Hey, Scripting Guy! Is there a way for me to log on to a computer using a regular user account, but then run a script as a domain administrator?

-- WW

Hey, WW. In answer to your question, yes, both WMI and ADSI provide a way for you to run a script under alternate security credentials; that is, both allow you to specify a user name and a password under which the script will run. Furthermore, both of these technologies provide a way to ensure that this user name and password are encrypted, and are not passed across the network using clear text. Thanks for the question, and hope that helped.

Hey, just kidding! Most likely what you really want to know is how to run a script under alternate security credentials. Let’s start by taking a look at a WMI script that connects to a remote computer (atl-ws-01) and retrieves the name of the operating system installed on that computer. Furthermore, it does this by running under the Administrator account, which in this example has a password of 4rTGh2#1:

Const WbemAuthenticationLevelPktPrivacy = 6

strComputer = "atl-ws-01"
strNamespace = "root\cimv2"
strUser = "Administrator"
strPassword = "4rTGh2#1"

Set objWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objwbemLocator.ConnectServer _
(strComputer, strNamespace, strUser, strPassword)
objWMIService.Security_.authenticationLevel = WbemAuthenticationLevelPktPrivacy

Set colItems = objWMIService.ExecQuery _
("Select * From Win32_OperatingSystem")
For Each objItem in ColItems
Wscript.Echo strComputer & ": " &
objItem.Caption
Next

As you can see, we start by creating a constant named WbemAuthenticationLevelPktPrivacy and assigning it the value 6; this value will be used to encrypt the communication between our computer and the remote computer. We then define four variables – strComputer, strNamespace, strUser, and strPassword – which hold the name of our remote computer; the WMI namespace we want to connect to; the user account we want to run the script under; and the password for that user account.

At this point we’re ready to connect to the remote computer using these alternate credentials. That’s what these lines of code do:

Set objWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objwbemLocator.ConnectServer _
    (strComputer, strNamespace, strUser, strPassword)
objWMIService.Security_.authenticationLevel = WbemAuthenticationLevelPktPrivacy

We begin by creating an instance of the SWbemLocator object, and then calling the ConnectServer method. ConnectServer gets passed four parameters, which just happen to correspond to the four variables we created a moment ago. We then set the Security_authenticationLevel property to WbemAuthenticationLevelPktPrivacy, giving us a more secure connection between the two computers.

From that point on, the rest of the code is the same old WMI code you know and love.

One important note: this approach, in which you run a script under alternate credentials, works only on remote machines. For some reason, WMI won’t let you run a script under alternate credentials on your own computer. Go figure.

Now let’s take a look at the ADSI version of this script. This sample script binds to the Ken Myer user account in Active Directory and tells us whether or not this account is disabled. The script connects to Active Directory using the fabrikam\Administrator, which has a password of 4rTGh2#1:

Const ADS_SECURE_AUTHENTICATION = 1
Const ADS_USE_ENCRYPTION = 2

strPath = "LDAP://cn=kenmyer,ou=Finance,dc=fabrikam,dc=com"
strUser = "fabrikam\Administrator"
strPassword = "4rTGh2#1"

Set objDSO = GetObject("LDAP:")
Set objUser = objDSO.OpenDSObject _
(strPath, strUser, strPassword, _
ADS_USE_ENCRYPTION OR
ADS_SECURE_AUTHENTICATION)

Wscript.Echo objUser.AccountDisabled

In this script, we create two constants (ADS_SECURE_AUTHENTICATION and ADS_USE_ENCRYPTION) to ensure that the information passed between our computer and the domain is encrypted. We then create three variables – strPath, strUser, and strPassword – which host the ADsPath to the object we want to bind to in Active Directory (in this case, the Ken Myer user account); the account name we want to use when connecting to Active Directory; and the password for that account.

Next we bind to the LDAP provider; we start here because the LDAP provider accepts anonymous bindings. Having connected to the LDAP object, we can then call the OpenDSObject method to bind to the Ken Myer user account. Note that we must pass OpenDSObject four parameters: the ADsPath to the Active Directory object; the user name we want to use when connecting to Active Directory; the password for that account; and the two constants we defined earlier. As with WMI, as soon as we make the connection, the rest of the script is the same as any other ADSI script.

By the way, OpenDSObject also works with local user accounts; the primary difference is that you bind to WinNT provider instead of the LDAP provider (and, of course, the ADsPath will be different). Here are the relevant lines of code from a script that connects to a local computer:

strComputer = "WinNT://atl-ws-01"
strUser = "Administrator"
strPassword = "4rTGh2#1"

Set objDSO = GetObject("WinNT:")
Set objComputer = objDSO.OpenDSObject _
(strComputer, strUser, strPassword, _
ADS_SECURE_AUTHENTICATION OR
ADS_USE_ENCRYPTION)

One thing we should add is that we don’t recommend you hardcode passwords (especially Administrator passwords) in your scripts. Instead, you should make allowances to enter the password as a command-line argument or via an Input box or whatever works best for you.


How Can I Run a Windows PowerShell Script from the Run Dialog Box or a Shortcut?

Hey, Scripting Guy! How can I run a Windows PowerShell script from the Run dialog box or from a shortcut? In the Run dialog box I type something like c:\scripts\test.ps1 but my script just opens up in Notepad.

-- RS

Hey, RS. We have just one thing to say: University of Washington 29, UCLA 19. And this after the Huskies trailed 16-0 late in the first half. Granted, it's still early, but at 3-1 the UW already has as many wins this year as they've had in the last two seasons combined.

But that’s all we’re going to say. After all, the Scripting Guy who writes this column knows enough about sports to know better than to jinx a team just when things are starting to look up. So let's just say that it's always nice to beat UCLA and leave it at that. See you all tomorrow!

Pardon? How can you run a Windows PowerShell script from the Run dialog box or from a shortcut? Um, didn't we tell you that we only had one thing to say? And we already said it: University of Washington 29, UCLA 19.

Could we put you on hold for a second? Really? You’re kidding, right? Wow; we wouldn’t have guessed that in a million years.

OK. Apparently, or at least from what we’ve been told, not everyone in the world is a college football fan. That’s hard for us to believe: people would rather know how to run a Windows PowerShell script from the Run dialog box or from a shortcut than talk about Saturday’s Husky-UW game? But, just in case, we’ll see if we can answer your question, RS.

No, no need to thank us; it’s no problem. Besides, we have a few hours to kill before the Husky Highlight show comes on anyway.

So why do your Windows PowerShell scripts keep opening up in Notepad? That’s because the Windows shell – bless its heart – doesn’t know what to do with Windows PowerShell scripts. The last time we installed Windows PowerShell (and remember, the application is still in beta, so the installation process might have changed since then) script files (files with a .ps1 file extension) were automatically associated with Notepad. That’s probably why your script files keep opening up in Notepad: ours do, too, simply because that’s what the Windows shell does with .ps1 files.

Of course, that might lead you to believe that all you have to do is associate .ps1 files with the Windows PowerShell executable file and everything will be hunky-dory. That’s what we thought, too, but that doesn’t seem to work. Instead, in order to start a Windows PowerShell script from the Run dialog box or from a shortcut (or, for that matter, from Cmd.exe) you need to explicitly start Windows PowerShell and then pass the path to the script file as a startup parameter. In other words, this command will start Windows PowerShell and run the script C:\Scripts\Test.ps1:

powershell.exe c:\scripts\test.ps1

Of course, if you immediately ran off and tried that command, without bothering to read the rest of the column (for shame!) you were likely disappointed. “Wait a second; that doesn’t work, either,” you’re likely grumbling. “I saw a brief flash and then, nothing. Nice try, Scripting Guys, but no cigar!”

To tell you the truth, your command probably did work; the problem is that Windows PowerShell started, the script ran, the script finished, and then Windows PowerShell automatically terminated, just like that. You can verify that this is what’s happening by running a script that saves data to a text file. Start the script using the method outlined above, and then check to see if the text file was created and the data saved. If it was, then the command worked; it’s just that Windows PowerShell did its thing and then, like the Lone Ranger, disappeared before you could even thank it.

Now, that’s all fine and dandy … unless you need to read the output onscreen. In that case we have a problem, because everything typically happens way too fast for the human eye to follow. (Granted, the Scripting Editor insists that she can spot dangling participles and split infinitives even at that speed. But, remember, we said it was too fast for the human eye to follow.) If you need to read output onscreen what you have to do is tell Windows PowerShell to keep its command window open after the script finishes running. That’s something you can do by adding the –noexit parameter to your command:

powershell.exe –noexit c:\scripts\test.ps1

Give that a try and see what happens. When you do, make sure you leave –noexit as the first parameter. In our experience, switching the parameters around (that is, first specifying the script path and then tacking on –noexit) won’t work.

But, then again, you should always do everything exactly the way the Scripting Guys tell you.

OK, good point, even though the first explosion wasn’t entirely our fault; if dolphins are so smart then they should known better. But at least type your Windows PowerShell command exactly the way we told you.

We hope that solves your problem, RS. If you need more information about Windows PowerShell you might take a look at our Windows PowerShell center; among other things, you’ll find a new excerpt from the forthcoming book Windows PowerShell in Action, written by Microsoft’s own Bruce Payette. And stay tuned for Windows PowerShell Week, a series of five webcasts (aimed squarely at beginners) scheduled for November 6-10. We’ll have more information on Windows PowerShell Week very soon.

Most important of all, next week the Huskies travel to Arizona to play the Wildcats. Could the Huskies really start the season 4-1, including 2-0 in conference play? Let’s put it this way: if they do, it’s a pretty safe bet you’ll be able to read about it here. Will you also be able to read about scripting-related stuff here? In the immortal words of Bart Simpson, we can’t promise to try. But we promise to try to try.


How Can I Send Data from One Script to Another?

Hey, Scripting Guy! A while back you showed how we could use the InputBox function to prompt a user to enter a value we could then use in our script. What I’d like to know is, is there any way I can take that value and use it in another script?

-- JW

Hey, JW. We’re guessing here, but apparently you’d like to have a script that, among other things, prompted the user for some data. You’d then like to be able to start a second script, and enable that second script to access data the user entered in the first script. And, of course, you’d also like to know if such a thing is even possible.

And the answer is (the suspense is killing you, isn’t it?): sort of. Suppose you start Script A, and you enter a value. Can you now independently start Script B, and somehow have Script B peer into Script A and retrieve that value? No; that seems more akin to magic than to scripting. However, you can do this: start Script A and have the user enter the data. Then use Script A to start Script B; when you do that, you can pass the data along to Script B. That way, Script B will have full access to any information the user entered.

To better explain this, let’s take a look at a very simple example. We need two scripts: Script A and Script B. When we start Script A, the script will prompt the user to enter a number. Script A will then launch Script B; at the time it launches Script B, it will also pass along the number the user entered. Script B will then take that number, determine the square root, and report the answer.

Note. In a case like this, wouldn’t it be easier to just have one script that both accepted the user input and calculated the square root? Yes. Remember, though, this is just a demonstration script designed to show you a way to pass data from one script to another; it’s not intended to be an actual script you’d make use of in real life.

Here’s Script A. Notice that it starts by creating an instance of the WSH Shell object; this object is used later on to launch Script B (which we’ve named output.vbs; we’ll name Script A input.vbs). We then use the InputBox function to ask the user to enter a number:

Set objShell =
CreateObject("Wscript.Shell")

intValue = InputBox("Please enter a number:")
strCommandLine = "output.vbs " & intValue
objShell.Run(strCommandLine)

Notice what we do after the user enters a number (we’ll pretend that the user entered the number 2). We have this line of code:

strCommandLine = "output.vbs " & intValue

This code constructs a command for starting Script B. Suppose you wanted to start Script B and pass it the number 2 as a command-line argument. To do that, you would type this from the command prompt:

output.vbs 2

What we’re doing here is building a string that contains these two items: the name of the script we’re starting (output.vbs) and the command-line argument we’re passing (2). Of course, we don’t know that the argument is actually 2; we’re just passing along whatever value the user entered. That’s why we use the variable intValue instead. Notice, too, that output.vbs (plus a blank space) is included inside double-quote marks, and intValue is outside those double-quote marks. Why isn’t intValue inside the double-quotes? That’s easy; because then we would have constructed this command, which wouldn’t pass the number 2 (or any other number the user entered), but would instead pass the string value intValue:

output.vbs intValue

Not what we want.

After constructing the command line, we then use the Run method to launch Script B, passing the value entered by the user as a command-line argument.

So then what does Script B look like? Well, it looks an awful lot like this:

intValue = Wscript.Arguments.Item(0)
intSquareRoot = Sqr(intValue)

Msgbox "The square root of " & intValue & _
    " is " & intSquareRoot & "."

As you can see, we assign the first (and in this case, the only) command-line argument to the variable intValue. We then use the Sqr function to determine the square root of that value. Finally, we display the answer. In this case we used VBScript’s Msgbox function (as opposed to Wscript.Echo) to ensure that the answer appears in a message box. Had we used Wscript.Echo and Cscript was your default script host, a command window would open for a split second, the answer would appear on screen, and then the window would disappear before you had a chance to actually see the answer. Again, not what we wanted. The Msgbox function always displays data in a message box, regardless of the script host, so we went with Msgbox.

Again, a very simple example, but it could easily be expanded to send 2, 3, 4 or any number of command-line arguments from Script A to Script B. This definitely works, although you could theoretically run into the limitations on the length of a command string if you tried to pass too much data. In that case, your best bet would be to have Script A save data to a text file, and then have Script B read the data from that same file.

You also have to be careful about scripts that send command-line arguments that include spaces. For example, suppose you wanted to send the name Ken Myer to Script B. This command won’t work; it send two arguments (Ken and Myer) rather than the single argument Ken Myer:

output.vbs Ken Myer

Because of the space between Ken and Myer (and the space is the way WSH determines where one argument ends and the next begins), you need to put Ken Myer in double quote marks, like this:

output.vbs "Ken Myer"

In turn, that means the command-string you construct must includes double-quote marks as well. Here’s one way to do that; for more information, see this archived Hey, Scripting Guy! column:

strCommandLine = "output.vbs " & chr(34) & strValue & chr(34)

In this example, each instance of chr(34) inserts a double-quote mark. We thus end up with output.vbs plus " plus Ken Myer plus ", or:

output.vbs "Ken Myer"


How Can I Stop a Script Partway Through?

Hey, Scripting Guy! Is there a way to make a script stop itself if certain conditions aren’t met? Like say I’m going to copy files to a remote computer, but then the remote computer isn’t available. Can I write code that tells the script to just go ahead and quit at that point?

-- WK, Palo Alto, CA

Hey, WK. This is one of the easiest questions we’ve ever had to answer: you can stop a script at any time by using a single line of code:

Wscript.Quit

That’s it; as soon as that line is executed, the script stops right then and there.

So how would you actually make use of this command? Well, let’s take your scenario, in which you try to connect to a remote computer and you fail; in that case, an error will be generated. You can then check the value of the Err object and if it’s anything other than 0 (0 means no error occurred) you can exit the script right then and there:

If Err <> 0 Then
    Wscript.Quit
End If

Or maybe before quitting you might want to echo a message explaining why the script quit:

If Err <> 0 Then
    Wscript.Echo "Unable to contact remote computer; " & _
        "the script will now quit."
    Wscript.Quit
End If

For more information about the Quit method, see this portion of the Windows Script Host documentation on MSDN.


How Can I Take Action Based on the First Line of a Text File?

Hey, Scripting Guy! How can I read the first line of every file in a folder and then move each file if the line contains thisdomain.com or thatdomain.com?

-- AZ

Hey, AZ. We’ll get to your question in a moment. However, before we do that we have something important to say: flight of cheeses.

OK, we can go on now. Thanks for waiting.

What’s that? Flight of cheeses? What are you talking about; that doesn’t even make any sense, it – oh, right; we were the ones who brought that up, weren’t we? Well, as it turns out, Mary G., the lovely and talented technical writer who single-handedly turned the WMI SDK into one of the most useful pieces of documentation Microsoft has ever produced, is leaving the SDK to take a new position at Microsoft. Good news for Mary, bad news for those of us who rely on the WMI SDK to get us through each and every day of the week.

Yes, even Sunday. Especially Sunday.

Note. So if she’s so good at what she does then how come Mary G. (affectionately known as Mary Queen of Scripts) isn’t a Scripting Guy? Apparently we didn’t make ourselves clear: Mary is good at what she does. That doesn’t sounds like a Scripting Guy, at least not to us.

The truth is, we couldn’t add a female Scripting Guy until we found someone as incompetent as the rest of us. Glad to have you aboard, Jean Ross!

Anyway, at Mary’s farewell lunch the team went to a restaurant in which the Scripting Guy who writes this column was totally out of his league. (Which, come to think it, is true of most places he goes to.) To whit: buttered pasta rags (or, if you prefer, pasta rags with red kurri squash). Guinea hen confit with cracked green olive sauce. Your choice of rhubarb, lavender or lemongrass soda – sorry, dry soda. (Targeted, we assume, to people who feel that they’ve been getting too much liquid in their soda.)

And, of course, a “Flight of 3 artisan cheeses.” The equally lovely and talented June B. (PowerShell writer to the stars) took one look at the menu and said, “Hey, Scripting Guy! If you can find a way to work flight of cheeses into your next column I’ll buy you a latte.” Well, just in case you missed it, June, here it is again, this time in boldface:

Flight of cheeses.

A vanilla latte will be fine. Thanks, June.

Now, back to business:

Const ForReading = 1

strComputer = "."

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

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

Set objFSO = CreateObject("Scripting.FileSystemObject")

For Each objItem In colItems
    Set objFile = objFSO.OpenTextFile(objItem.Name, ForReading)
    strLine = objFile.ReadLine
    strLine = LCase(strLine)
    objFile.Close

    If InStr(strLine, "fabrikam.com") or InStr(strLine, "contoso.com") Then
        objFSO.MoveFile objItem.Name, "C:\Archive\"
    End If
Next

What we have here is a script that opens all the files in the folder C:\Scripts, reads the first line of each file, then determines whether the string fabrikam.com or the string contoso.com can be found in that initial line. If either value is found the script then moves that file to the folder C:\Archive. How does the script manage to do all that? Let’s see if we can figure it out.

We start out by creating a constant named ForReading and assigning it the value 1; we’ll need to use this constant when we open each of the files in C:\Scripts. Of course, before we can open each of those files we need to retrieve those files. That’s what this block of code is for:

strComputer = "."

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

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

What we’re doing here is connecting to the WMI service on the local computer, then using an Associators Of query to retrieve a collection of all the files (that is, all instances of the CIM_DataFile class) found in the folder C:\Scripts. Admittedly, there are other ways that we could get a collection of all the files in the folder C:\Scripts; we chose to use WMI because this same block of code can retrieve this information from a remote computer. (All you need to do is assign the name of that remote computer to the variable strComputer.)

Note. Of course, just because WMI can retrieve the files from a remote machine that doesn’t mean you can use the rest of the script as-is to actually read those files. Why not? Because the FileSystemObject, the technology used for reading from and writing to text files, doesn’t really let you do that. Is there a way to work around this issue? Maybe; take a look at this Hey, Scripting Guy! column for a suggested approach.

Speaking of the FileSystemObject, that’s the next step in the process: we create an instance of the Scripting.FileSystemObject. Once that’s done we set up a For Each loop to loop through each of the items (files) found in the folder C:\Scripts:

For Each objItem In colItems

Good question: what are we going to do inside that loop? Well, for starters, we’re going to use this line of code to open the first file in the collection; this code works because the WMI property Name actually refers to the complete file path (e.g., C:\Scripts\File1.txt):

Set objFile = objFSO.OpenTextFile(objItem.Name, ForReading)

As soon as the file is open we use the ReadLine method to read the first line, then use the LCase function to convert that initial line to all lowercase letters (just to make sure we don’t run into any problems with mixed case). That’s what these two lines of code are for:

strLine = objFile.ReadLine
strLine = LCase(strLine)

We then immediately close the file. Why? That’s easy: all we’re interested in is the first line (which we’ve already grabbed) so we don’t need the file any more. Therefore, we close it. That brings us to this line of code:

If InStr(strLine, "fabrikam.com") or InStr(strLine, "contoso.com") Then

What we’re doing here is using the InStr function to look at the value of strLine and see if that value contains either the string fabrikam.com or the string contoso.com. If true (that is, if either of those strings appear in strLine) we then use this line of code to move the file to the folder C:\Archive:

objFSO.MoveFile objItem.Name, "C:\Archive\"

What if neither string appears in strValue? No problem; we just loop around and repeat the process with the next file in the collection.

That should do it, AZ. And in case you’re worried, the Scripting Guy who writes this column (who is far more likely to frequent restaurants where you order meals by number) did manage to find something on the menu that he recognized and was willing to eat: a pulled pork sandwich. Granted, the coleslaw was brown (a somewhat disconcerting color for coleslaw) and it tasted brown; however, the sandwich was pretty good.

And, needless to say, that free latte will be even better.


How Can I Tell Whether a Logon Script is Running on a Workstation or on a Server?

Hey, Scripting Guy! How can I tell whether a logon script is running on a workstation or on a server? If the script is running on a server, I want to skip a portion of the script.

-- RT

Hey, RT. You know, a few years ago one of the Scripting Guys helped a fellow Microsoft employee set up a computer lab that this other Microsoft employee needed for testing courseware materials. After getting the lab set up the other Microsoft employee set out to test “computer lockdown,” using a combination of group policy, logon scripts, and command-line tools to see if he could keep anyone from doing anything to the workstation machines. His intention was to determine just how severely you could restrict activities on a computer.

Well, guess what: you can restrict activities so severely that no one can do anything on a computer, not even log on. In other words, the experiment was a complete success. Well, except for one thing. In his zeal to test lockdown tasks this employee didn’t differentiate between workstations and servers; as a result, he ended up locking down all the computers in the lab (including the domain controllers). That meant he couldn’t log on to any of the machines in his test domain. And, yes, that meant we had to reformat all the computers and set the lab up all over again.

Which is simply our way of saying, yes, sometimes it can be very useful to differentiate between workstations and servers.

So how can you tell whether or not a given machine is a workstation or a server? Here’s how:

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

Set colComputers = objWMIService.ExecQuery _
    ("Select * from Win32_ComputerSystem")

For Each objComputer in colComputers
    If objComputer.DomainRole < 2 Then
        Wscript.Echo "This is a workstation."
    Else
        Wscript.Echo "This is a server."
    End If
Next

This script starts off by connecting to the WMI service on the local computer. (We should point out that, although you specifically mentioned running this as a logon script, the script could also easily be modified to run against a remote computer). We then use this line of code to bind to the Win32_ComputerSystem class:

Set colComputers = objWMIService.ExecQuery _
    ("Select * from Win32_ComputerSystem")

Why the Win32_ComputerSystem class? Well, as it turns out this class has a property named DomainRole that can tell us the “role” played by a computer. DomainRole returns one of the following values:

Value | Description
0 | Standalone Workstation
1 | Member Workstation
2 | Standalone Server
3 | Member Server
4 | Backup Domain Controller
5 | Primary Domain Controller

As you can see, if the DomainRole is either 0 or 1 then we’re dealing with a workstation.

With that in mind, all we have to do is set up a For Each loop to walk through the collection of “computer systems” returned by our query (there will be only one item in the collection). Inside that For Each loop we check to see whether or not the value of DomainRole is 0 or 1 (or, in other words, if it is less than 2) and then echo the appropriate message:

For Each objComputer in colComputers
    If objComputer.DomainRole < 2 Then
        Wscript.Echo "This is a workstation."
    Else
        Wscript.Echo "This is a server."
    End If
Next

Of course, instead of just echoing back the computer role you’ll probably want to run a specific chunk of code depending on whether the machine is a workstation or server. But we’ll let you take care of that part yourself. Just try not to completely lock everyone out of all your computers, OK?

And, yes, you’re right: that’s something only a Microsoft employee would do. But – for once, at least – we didn’t do it. (For some reason, any time something dumb happens people immediately think of the Scripting Guys. Wonder why ….)


How Can I Tell Which Scripts are Running on a Computer?

Hey, Scripting Guy! How can I tell which scripts are running on a computer?

-- NW

Hey, NW. This is a question we’re always a little reluctant to answer, because there’s a 50-50 chance you’ll be disappointed with what you hear. If you are running Windows 2000 (or a previous version of Windows), the answer is pretty straightforward: you can’t. A script such as this one – which looks for all processes running under Cscript.exe or Wscript.exe – can tell that scripts are running on a computer:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'cscript.exe'" & _
        " OR Name = 'wscript.exe'")
For Each objItem in colItems
    Wscript.Echo objItem.Name
Next

However, this script can’t tell you the names of those scripts. In fact, as far as we know, nothing built into the operating system can identify the individual scripts running on a Windows 2000 computer; even Task Manager reports only the process names (Cscript.exe and Wscript.exe).

If you’re running Windows XP or Windows Server 2003, however, then we have a different – and better – answer for you. With Windows XP a new property – CommandLine – was added to the Win32_Process class. As the name implies, CommandLine tells you the actual command used to start a process. For example, suppose you started a script by typing cscript my_script.vbs at the command prompt. The CommandLine property for that script will be this:

cscript my_script.vbs

In other words, now you know that script named My_Script.vbs is running on this machine.

But what if you don’t use the command prompt to start a script, what if you double-click the file icon in Windows Explorer and run the script under Wscript? No problem. Say you double-click the file C:\Scripts\My_Script.vbs; you’ll get back a CommandLine value like this:

C:\WINDOWS\System32\WScript.exe "C:\Scripts\My_Script.vbs"

As you can see, we get the path to the Wscript executable file along with the path to the script that’s actually running. Now that’s more like it, huh?

At any rate, if you’re running Windows XP or Windows Server 2003, here’s a script that will do the trick for you:

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_Process Where Name = 'cscript.exe'" & _
        " OR Name = 'wscript.exe'")
For Each objItem in colItems
    Wscript.Echo objItem.CommandLine
Next


How Can I Use Alternate Credentials When Searching Active Directory?

Hey, Scripting Guy! How can I use alternate credentials when searching Active Directory?

-- PT

Hey, PT. So what’s the big idea here? Devoted readers of this column (both of them) know that we usually love tackling questions where the answer involves searching Active Directory. Why? That’s easy: because we don’t really answer those questions. Instead we simply hammer out a script and then, instead of explaining how the script works, tell people to read the two-part series Dude, Where’s My Printer? for more information. As you can tell, that makes for a nice, easy day at the office.

But your question is different: unfortunately for us, specifying alternate credentials isn’t mentioned anywhere in our series on searching Active Directory. That means we’re actually going to have to answer this question. Talk about unfair: not only do we have to come in to work, but now we’re expected to actually do something while we’re here!

Trust us: that is not the American way.

So we’ll make a deal with you: we’ll answer your question, but only by focusing on the part of the script where you specify alternate credentials. If you need more information about the rest of the code, or if you need more information about searching Active Directory, please see the two-part series Dude, Where’s My Printer?

Boy, if only we had a dollar for every time we’ve said that.

Here’s a script that uses alternate credentials in order to search Active Directory:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"

objConnection.Properties("User ID") = "fabrikam\kenmyer"
objConnection.Properties("Password") = "A2sXrco1Fq1#om!"
objConnection.Properties("Encrypt Password") = TRUE
objConnection.Properties("ADSI Flag") = 3

objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = _
    "SELECT Name FROM 'LDAP://DC=fabrikam,DC=com' WHERE " _
        & "objectCategory='user'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst
Do Until objRecordSet.EOF
    Wscript.Echo objRecordSet.Fields("Name").Value
    objRecordSet.MoveNext
Loop

Like we said, for today we’re going to focus on these four lines of code, the four lines where we specify the alternate credentials:

objConnection.Properties("User
ID") = "fabrikam\kenmyer"
objConnection.Properties("Password") = "A2sXrco1Fq1#om!"
objConnection.Properties("Encrypt Password") = TRUE
objConnection.Properties("ADSI Flag") = 3

Note that these four lines of code are required only if you want to conduct the search under alternate credentials; that is, only if you want to bind to Active Directory using a user account other than the one you used when logging on to Windows. If you want to do a search using your current logon credentials all you have to do is remove these four lines of code and the script is good to go.

As you can probably tell (the object reference objConnection is a dead giveaway), these four lines of code involve four properties of the ADO (ActiveX Data Objects) Connection object. The properties User ID and Password should be self-explanatory: these are simply the user name and the password for the account you want to use when binding to Active Directory. In this example, we’ve specified the User ID using the domain\user name syntax. However, we could also specify the user name as the logon name itself (e.g., kenmyer) or as the user’s UPN (Universal Principal Name): kenmyer@fabrikam.com. That’s entirely up to you.

As for the password, we’ve hard-coded the password into the script for educational purposes. Needless to say, however, that’s not the way we recommend that you do things. Instead, you should probably have the script prompt you for a password each time the script is run. If you’re not sure how to do that, well, don’t worry about it: as we are wont to do, we’ll simply refer you somewhere else. (In this case, a previous Hey, Scripting Guy! column on prompting for – and masking – passwords.)

That leaves us with just two properties to dispose of. Setting Encrypt Password to True simply tells the script to encrypt the password when sending it across the network; by default, this value is set to False. The ADSI Flag property, meanwhile, is a bitmask property used to specify authentication options. The value 3 is actually a bitmask value composed of two separate properties:

Constant | Value | Description
ADS_SECURE_AUTHENTICATION | 1 | Requests secure authentication. When this flag is set, Active Directory will use Kerberos, and possibly NTLM, to authenticate the client.
ADS_USE_ENCRYPTION | 2 | Requires ADSI to use encryption for data exchange over the network.

You can find more information about the ADSI Flag property in the ADSI SDK.

That’s really all you have to do: configure the appropriate values for those 4 properties and away you go.

Now, if you’ll excuse us, we need to take a break. After all, the last thing we expected to have to do at work today was work!


How Can I Use Both Command-line Arguments and a Default Value?

Hey, Scripting Guy! I’d like to have a script that accepts computer names as command-line arguments and then runs against each of those computers. However, if you don’t enter any command-line arguments, I’d like it to default to running against the local computer. Can you help me out here?

-- TS

Hey, TS. Sure, we can help you out here. Let’s say you started your hypothetical script by typing the following command at the command prompt:

cscript my_script.vbs atl-ws-01 atl-ws-02 atl-ws-03

In that case, you’d like the script to run against the three specified computers: atl-ws-01; atl-ws-02; and atl-ws-03. But what if you typed in a command with no command-line arguments, like this:

cscript my_script.vbs

In that case, you’d like the script to say, “Oh, no command-line arguments, huh? Ok, then I’ll just run against the local computer instead.”

So how do you do that? Well, you do that by using code similar to this:

If Wscript.Arguments.Count = 0 Then
    arrComputers = Array(".")
Else
    Dim arrComputers()
    For i = 0 to Wscript.Arguments.Count - 1
        Redim Preserve arrComputers(i)
        arrComputers(i) = Wscript.Arguments(i)
    Next
End If

For Each strComputer in arrComputers
    Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
    Set colItems = objWMIService.ExecQuery _
        ("Select * from Win32_OperatingSystem")
    For Each objItem in colItems
        Wscript.Echo objItem.Caption
    Next
Next

Let’s take a moment to explain how this works. We start by seeing whether or not any command-line arguments were entered; we do that by checking the value of Wscript.Arguments.Count (as you might expect, this tells us the number of arguments that were supplied when the script started). Suppose Count is equal to 0. Well, that means you didn’t supply any command-line arguments. Therefore, we set the value of the array arrComputers to a dot (“.”). Why a dot? Well, in the wild and wonderful world of WMI, the dot represents the local computer.

And why an array? Well, we want to write as few lines of code as possible. By using an array, we don’t have to write one set of code that gets used when we don’t specify a command-line argument and a separate set of code that gets used when we do specify a command-line argument (or two). Instead, we use a simple If-Then statement to check the value of Wscript.Arguments.Count. As we noted, if the value is 0 (that is, if no command-line arguments were entered), we set the value of arrComputers to a single dot. We thus have an array consisting of a single item.

But what if Count does not equal 0? In that case, we want to take all the command-line arguments and stuff each of those into arrComputers. Here’s how we do that:

First, we create a dynamic array using this code: Dim arrComputers(). A dynamic array is an array in which we don’t specify the size in advance; instead, we dynamically grow the array as needed. We do that so that we don’t have to worry about how many command-line arguments were entered; we’ll just resize the array as needed to accommodate all of those arguments.

Next, we create a For Next loop that runs from 0 to Wscript.Arguments.Count – 1. This seems crazy, but we do this because the first item in a VBScript array is actually item 0; the last item, therefore, is the number of items minus 1. For example; suppose we have three items. The first item is item 0; the second is item 1; and the third is item 2 (3 – 1).

For each item, we resize the array using this code: Redim Preserve arrComputers(i). This sets the size of the array to the value of i (i is the loop variable, so it tells us which arguments in Wscript.Arguments we are working with). The first time through the loop, i is equal to 0, so we are – in effect – using this code: Redim Preserve arrComputers(0), which gives us an array with one element.

Incidentally, the Preserve part of this code ensures that we don’t lose all the existing each time we re-dimension the array. If we just said Redim arrComputers(), the array would get resized, but any data already in the array would be deleted.

We then add the argument in question to the array. If we supply atl-ws-01 and atl-ws-02 as command-line arguments, then at the end of this loop arrComputers will be an array with two elements: atl-ws-01 and atl-ws-02.

The rest is easy. We create a For-Each loop that loops through all the elements in the array arrComputers; as we know, arrComputers contains either a dot (representing the local machine) or a list of computer names pulled from the command-line arguments. Each time the loop runs in connects to the appropriate computer and retrieves the Caption property from the Win32_OperatingSystem class. And that’s all there is to it!


How Can I Use Long File Names as Parameters?

Hey, Scripting Guy! I’m trying to run a command-line tool from a script, but I need to pass it a long file name like C:\Documents and Settings\All Users\Desktop\logfile.txt. No matter how I try to pass that file name, however, my script blows up on me. Help!

-- GK, Birmingham, Great Britain

Hey, WK. Ah, yes: you’ve encountered the dreaded double double-quotes problem. What does that mean? Well, let’s say you’re trying to run Notepad and pass it a long file name. In order to do that, you need to type the following command from the command prompt:

notepad.exe "c:\my scripts\logfile.txt"

As you no doubt know, any time you need to do something from the command prompt that requires spaces in the path name, you need to enclose the entire path in double quote marks. Hence "c:\my scripts\logfile.txt".

So why does that cause problems with your script? Well, you’re trying to start Notepad using the Windows Script Host Run method. To start a program using this method, you must enclose the complete command in double quote marks:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "ping.exe 192.168.1.1"

That works fine with the Ping example, but here’s what you get when you try to run Notepad and pass it a long file name:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "notepad.exe "c:\my scripts\logfile.txt""

As you have discovered, this doesn’t work: you get an Expected end of statement error. That’s because the script interpreter sees "notepad.exe " as being a complete line of code; consequently, as soon as it sees the rest of the code it freaks out. So now we’ve got a problem: we need to enclose c:\my scripts\logfile.txt in double quote marks, but WSH freaks out when it sees double quotes inside of quotes. We’re doomed, right?

Right.

No, hey, just kidding. There are actually a couple of ways around this, and here’s one we like because it’s versatile: the same approach can be used to embed single quotes and other reserved characters into command-line parameters. As you probably know, all characters on the keyboard have an ANSI equivalent; that’s just a number that represents the character. For example, the lowercase letter a has an ANSI value of 97; the uppercase A has an ANSI value of 65. Still not impressed? Well it turns out that 34 is the ANSI value for double quote marks. Now you’re impressed, right?

Ok, then we’ll explain it a bit more. VBScript has a function named Chr that converts an ANSI value into its character equivalent. For example, try to guess what you get when you run this script:

Wscript.Echo Chr(65)

That’s right; an uppercase A is echoed to the screen.

Now to try to guess what happens when you run this script:

Wscript.Echo Chr(34)

Yep; double quote marks get echoed to the screen. Now here’s a tricky one:

strArgument = "notepad.exe " &
chr(34) & _
"c:\my scripts\logfile.txt" & chr(34)
Wscript.Echo strArgument

Before you give up, here’s the “equation” we’re using:

Notepad.exe + " + c:\my scripts.logfile.txt + "

Add them altogether and you get this:

notepad.exe "c:\my scripts.logfile.txt"

In other words, we now have the exact string we’d have to type at the command prompt in order to start Notepad and load in C:\My Scripts\Logfile.txt:

notepad.exe "c:\my scripts.logfile.txt"

Try this revised script, and see what happens:

Set objShell =
CreateObject("Wscript.Shell")
strArgument = "notepad.exe " & chr(34) & _
"c:\my scripts\logfile.txt" & chr(34)
objShell.Run strArgument


How Can I Use the RunAs Command to Run a Script Under Alternate User Credentials?

Hey, Scripting Guy! How can I use the RunAs command to run a script under alternate user credentials?

-- BE

Hey, BE. You know, in the business world one of the hot topics these days is synergy. For example, after The Mighty Ducks became a hit movie the Disney Company went out and bought themselves an NHL hockey team, renaming it the Anaheim Mighty Ducks. Coincidence? No, synergy, defined as “…the phenomenon in which two or more discrete influences or agents acting together create an effect greater than the sum of the effects each is able to create independently.” If there’s something more exciting than two or more discrete influences or agents acting together to create an effect greater than the sum of the effects each is able to create independently, well, we’d like to see it.

Synergy is important in the scripting world as well. For example, today we published the answer to a scripting puzzle that dealt with the issue of trying to run scripts under alternate credentials. In our answer we discussed the problems involved in getting WMI to run under alternate credentials when the script needs to run against the local computer. Our suggestion: read today’s Hey, Scripting Guy! column and learn how to run a script using the RunAs command.

Coincidence? No, laziness: we actually received this question several months ago, and we’re only just now getting around to answering it. But chalking this up to synergy sounded much better than chalking it up to lethargy!

As we noted in the weekly puzzle, you can use code similar to this snippet to run a script under alternate user credentials, provided that the script is running against a remote computer:

strComputer = "atl-fs-01"
strNamespace = "root\cimv2"
strUser = "kenmyer"
strPassword = "password"

Set objWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objwbemLocator.ConnectServer _
    (strComputer, strNamespace, strUser, strPassword)

Unfortunately, though, this script will fail if you try to run it against the local computer. That’s because the ConnectServer method does not allow you to pass user credentials (like a user name and password) to the local machine. And, no, to be honest we’re not sure why it won’t let you pass user credentials to the local machine; it just won’t.

Note. Think of ConnectServer as being like a three-year-old child. Nobody knows why three-year-olds will eat only boxed macaroni-and-cheese; they just won’t eat anything else, period. And there’s not much you can do about that except try to live with the situation, whether that means making boxed macaroni-and-cheese for dinner every night, or not passing user credentials any time ConnectServer runs against the local computer.

So is this really a problem? Well, it can be. After all, Microsoft recommends that – for security reasons – you never log on to a computer using an Administrator account. Instead, we suggest that you log on using a regular old user account, then use a utility such as RunAs any time you need to run a program as an Administrator. And so, seeing as how everyone always does whatever Microsoft tells you to do, you type the following command at the command prompt and try using the RunAs utility to run the script C:\Scripts\Test.vbs:

runas /profile /user:fabrikam\kenmyer "C:\Scripts\Test.vbs"

And here’s what happens when you press ENTER:

Attempting to start C:\Scripts\Test.vbs as user "fabrikam\kenmyer" ...
RUNAS ERROR: Unable to run - C:\Scripts\Test.vbs
193: C:\Scripts\Test.vbs is not a valid Win32 application.

It’s at this point that people typically do one of two things: either they give up, or they start debugging the script, trying to figure out why their code isn’t valid. But here’s the deal: the error message says nothing about the script or the script code being invalid, it simply states that Test.vbs is not a valid Win32 application. That’s because RunAs is expecting to run an executable file. Test.vbs is obviously not an executable file, so the command fails. Most likely your code is just fine; the problem is that you’re trying to run a script rather than a .exe file.

So if RunAs accepts only executable files then obviously we can’t run a script using RunAs, right? Well, not necessarily. Although Test.vbs might not be an executable file, the two Windows script hosts – Cscript.exe and Wscript.exe – are executable files. The secret to running a script under RunAs is to call one of the two script hosts, passing along the name of the script as a command-line argument. For example, this RunAs command will fire up Cscript.exe and then run the script C:\Scripts\Test.vbs:

runas /profile /user:fabrikam\kenmyer "cscript.exe C:\Scripts\Test.vbs"

You can see how easy that is. You simply call the RunAs command along with the following parameters:

        • /profile, which causes the appropriate user profile to be loaded. This is optional, but makes it more likely RunAs will be able to do what it needs to do.
        • /user:fabrikam\kenmyer, which is the user account (in the form domain\user_name) under which the process is to run.
        • “cscript.exe C:\Scripts\Test\.vbs”, which is the process you want RunAs to execute. Note that the entire command must be enclosed in double quote marks, and should be typed the same way you would type the command from the command prompt.

That, by the way, is the second problem people encounter when trying to run a script under RunAs. As long as your scripts are in the folder C:\Scripts you won’t have any problems; that’s because C:\Scripts has no spaces in the path name. But suppose your scripts are in C:\Documents and Settings\kenmyer\Scripts. If you were at the command prompt and you wanted to run a script in this folder you would need to surround the path in double quotes; that’s because the path includes blank spaces. In other words:

cscript.exe "C:\Documents and Settings\kenmyer\Scripts\Test.vbs"

Logically enough, then, you might then expect the RunAs equivalent to look something like this:

runas /profile /user:fabrikam\kenmyer "cscript.exe "C:\Documents and Settings\kenmyer\Scripts\Test.vbs""

Unfortunately, that’s not the case: the double quotes nested inside double quotes simply causes RunAs to give up and display the usage screen. Tossing double quotes inside double quotes won’t work. Nor will it work to try double double quotes or triple double quotes or any other variation of quotes inside of quotes. Like a three-year-old child (or many thirty-three-year-old adults) RunAs wants things to be exactly right or it won’t even try to help you.

Fortunately, it’s fairly easy to make RunAs happy. All you need to do is use the \ character to “escape” any double quote marks that must to be embedded within the process path. This command will work:

runas /profile /user:fabrikam\kenmyer "cscript.exe \"C:\Documents and Settings\kenmyer\Scripts\test.vbs"\"

Notice how we put our path together. We start out with a double quote mark and then the name of the script host:

"cscript.exe

At this point we need to insert the path, which includes blank spaces (and thus needs to be surrounded by double quotes). To do that we simply put a \ immediately before the first double quote and another \ immediately after the second double quote:

"cscript.exe \"C:\Documents and Settings\kenmyer\Scripts\test.vbs"\

All we have to do then is tack on the ending double quote and we’re done:

"cscript.exe \"C:\Documents and Settings\kenmyer\Scripts\test.vbs"\"

It’s simple, but it works. Just remember to bracket long file paths and any other item requiring double quotes and you’ll be home free.

So there you have it, BE: you can now use the RunAs command to run a script. As we noted earlier, you might want to check out the weekly puzzle for more information on running scripts under alternate credentials. And, in the spirit of synergy, stay tuned: it’s just a matter of time before the Anaheim Mighty Scripters come to a hockey rink near you. (And, yes, this would be the time when the Scripting Guy who writes this column would usually note that hockey isn’t even a real sport. But he’s decided not to say anything controversial ever again, so you won’t hear a word from him about that.)


How Can I Add a Function to a Windows PowerShell Script?

Hey, Scripting Guy! How can I add a function to a Windows PowerShell script?

-- TT

Hey, TT. You know, we’re going to do something a little unusual today: we aren’t going to mention the fact that the University of Washington defeated Washington State University in the annual Apple Cup football game. Granted, that’s the kind of thing we typically would bring up in this column. As it turns out, however, the Scripting Guys’ manager is an alumnus of Washington State University, and, in deference to him, we’re not going to mention the fact that UW won, or note that the Huskies now lead the all-time series 64 wins to 29 wins. After all, it’s bad enough that the poor guy had to attend WSU; there’s no need for us to add insult to injury.

Instead, we’re going to do something really unusual and just talk about scripting today. How can you add a function to a Windows PowerShell script? Well, here’s one way:

function multiplynumbers
    {$args[0] * $args[1]}

multiplynumbers 446 282

As you can see, there really isn’t much to this process. To create a function we simply call the function keyword followed by the name of the function; in this case, we’re naming our function multiplynumbers. Inside a pair of curly braces we then include the code we want the function to carry out. In this case that’s pretty simple: all we’re going to do is multiply the two values provided as function parameters. Hence this block of code:

{$args[0] * $args[1]}

That’s a good point: the $args variable typically does represent the command-line arguments supplied when you start a script. In this case, however, $args serves a dual purpose. The variable – when used outside the function – still contains any command-line arguments used when the script was started. Inside the function, however, $args represents the parameters supplied when calling the function. As you might expect, $args[0] represents the first parameter in the set while $args[1] represents the second parameter. It’s a tad bit confusing, but it works: the function uses the parameters passed to it, and the command-line arguments are still available.

Speaking of parameters, to call the function all we have to do is specify the function name followed by those parameters. In this case, that means typing multiplynumbers followed by the two numbers that we want to multiply:

multiplynumbers 446 282

All of that results in a script that echoes back the following:

125772

Pretty cool, huh?

The main thing to watch out for here is that you need to define the function before you call it. Suppose we didn’t do that, suppose we tried running this script instead:

multiplynumbers 446 282

function multiplynumbers
    {$args[0] * $args[1]}

Is the preceding script going to multiply the numbers 446 and 282 for us? Nope. Instead, it’s going to give us the following error message:

The term 'multiplynumbers' is not recognized as a cmdlet, function, operable program, or script file.
Verify the term and try again.
At C:\scripts\test.ps1:1 char:16
+ multiplynumbers  <<<< 446 282

Why the error message? One simple reason: the script called the function before the function had been defined. Define your functions first, right at the beginning of the script, and you’ll avoid these kinds of problems.

Now here’s a neat trick for you. Instead of defining the function in your script you can define it at the command prompt. Try this: type the following at the Windows PowerShell command prompt and then press ENTER:

function multiplynumbers {$args[0] * $args[1]}

So what, you ask? Well, believe it or not you now you have a function that you can call from any script, without having to define the function within the script itself. That means the following single-line script will return the value 125772:

multiplynumbers 446 282

In fact, you can even type the above command at the command prompt and you’ll get back the correct answer. Give it a try and see for yourself.

We told you that was a neat little trick.

Of course, this function will last only as long as your current Windows PowerShell session. If you’d like multiplynumbers to be a permanent addition to Windows PowerShell then add the function to your Windows PowerShell profile. To do that, type notepad $profile to bring up your default profile, add the function call, and then save the profile.

It’s so easy even a Cougar could do it.

That should get you started, TT. And you’re right: many UW alums would take the opportunity to gloat a little here, especially since – based on their abysmal performance a week ago – no one expected the Huskies to give the Cougars much of a battle, let alone win the game. In fact, this would typically be the time when Huskies would taunt the Cougars, usually by insinuating that the very best job a graduate of WSU could aspire to would be grocery bagger or fast food restaurant worker. But that won’t happen today. After all, the Scripting Guys’ boss doesn’t bag groceries or work at a fast food restaurant; instead, he’s a manager at Microsoft.

Which, it goes without saying, is nowhere near as good a job as bagging groceries. But when you’re a graduate of WSU you pretty much have to take whatever job you can get, don’t you?

Go Huskies!


How Can I Add or Subtract Two Hexadecimal Numbers?

Hey, Scripting Guy! How can I add or subtract two hexadecimal numbers?

-- PS

Hey, PS. You know, according to the old saying there are two things you should never watch being made: laws and sausages. We’d like to add a third item to that list: Hey, Scripting Guy! columns.

We knew you’d say that. But remember, you’re seeing the finished product, a column that has been fine-tuned to perfection. You never see the process involved in creating that column, a process that doesn’t exactly scream craftsmanship, if you know what we mean.

Take this question, for example. The Scripting Guy who writes this column was pretty sure that you couldn’t directly add two hexadecimal numbers; that is, there was no way to do something similar to this:

hexNumber3 = &H1C8 + &HD201

But he wasn’t fazed by that; after all, he knew there was a way to convert hexadecimal numbers to decimal. Was that helpful? You bet it was: that means you can add two hexadecimal numbers together simply by converting those numbers to their decimal equivalent and then adding the decimal values. Child’s play.

Of course, to paraphrase Groucho Marx, what this Scripting Guy really needed right about then was a child. Convinced that the name of the VBScript function he was looking for was either Decimal or Dec or something like that he began a long and fruitless journey to try and convert hexadecimal numbers using code similar to this:

intDecimal = Decimal("&H1C8")

Not being the type to give up, even though he was hopelessly wrong, he tried innumerable variations on Decimal, Dec, and DecToHex before finally deciding to take a look at the VBScript Language Reference. And even then he couldn’t figure it out, again because, like Ponce de Leon searching for the Fountain of Youth, he was fixated on finding a function named Decimal.

Whether such a function really existed or not.

Several years later the light bulb finally went on, he realized which function he really needed to use, and he finally wrote the code for today’s column.

But, as far as you guys are concerned, our hero took one look at this question and immediately sat down and wrote the following script:

hexNumber1 = "&H1C8"
hexNumber2 = "&HD201"

int1 = CLng(hexNumber1)
int2 = CLng(hexNumber2)

int3 = int1 + int2

Wscript.Echo "Decimal sum: " & int3
Wscript.Echo "Hexadecimal sum: " &
Hex(int3)

As usual, the final product turned out to be remarkably easy. The script begins by assigning a pair of hexadecimal values to two variables; the variable hexNumber1 gets assigned the value &H1C8 (456) and the variable hexNumber2 gets assigned the value &HD201 (53761):

hexNumber1 = "&H1C8"
hexNumber2 = "&HD201"

There’s nothing special here; we just needed a pair of hexadecimal numbers we could play with.

Once we have those numbers we need to convert the values to their decimal equivalents. That’s the very task our … hero … was trying to perform by calling the non-existent function Decimal. As it turns out, it’s way better to use real VBScript functions – like CLng – than it is to use make-believe VBScript functions. In other words:

int1 = CLng(hexNumber1)
int2 = CLng(hexNumber2)

Note. Actually, the terms CLng and Decimal are so similar you can see why the Scripting Guy who writes this column got confused. No doubt many, many people make the same mistake each and every day.

As soon as we’ve converted the hex values to their decimal equivalents we can then add them (or subtract them or multiply them or divide them or …). How? By using code similar to this:

int3 = int1 + int2

After that all we have to do is echo back the value of the variable int3 (which contains the sum of the two hexadecimal numbers). In our sample script we echo back the decimal value of the sum and then, just for the heck of it, we use the Hex function to convert the sum back into a hexadecimal value and echo that value:

Wscript.Echo "Decimal sum: " & int3
Wscript.Echo "Hexadecimal sum: " & Hex(int3)

When we run the script, we should get back something similar to this:

Decimal sum: 54217
Hexadecimal sum: D3C9

Note. We don’t blame you; we don’t trust him, either. To double check his math, open up Calculator, set the View to Scientific, and enter the equation yourself.

There you have it, PS; considering the amount of time it took to finally finish this column we hope you find the end result useful. To be honest, we wish the process would have gone a little smoother ourselves. But, then again, this is a column on system administration scripting; just think how boring that would be if one of the other Scripting Guys (that is, someone who knew what they were doing) wrote it. You guys don’t know how lucky you are!


How Can I Create a New WMI Namespace Under the Root Namespace?

Hey, Scripting Guy! How can I create a new WMI namespace under the root namespace?

-- MT

Hey, MT. So how are you doing, MT? We hope you’re doing well; the Scripting Guys hope that everyone is doing well. (OK: we hope that most everyone is doing well. There are a few exceptions to that general rule.) However, if it turns out that you aren’t doing well (and we hope that you are) we thought we should mention that you can now get funerary urns and caskets that reflect the colors and logo of your favorite major league baseball team.

Admittedly, this isn’t the sort of thing that the Scripting Guy who writes this column would typically talk about; he’s really not all that keen on the whole death thing. And typically he would ignore a subject like this, except that while thumbing through a magazine the other day, he saw an ad for baseball funerary urns. “Supplies are limited,” said the ad. “Act now.”

Act now? To be honest, the Scripting Guy who writes this column wasn’t sure how to interpret that. If you want to spend eternity in a Boston Red Sox funerary urn does that mean you should hurry up and die now, while you still have the chance? (After all, who wants to run the risk of having to spend eternity in an LA Dodgers funerary urn instead?) Granted, you probably don’t have to use your funerary urn right away; you could probably save it for later. But, in this day and age, can you really take that chance? For example, Seattle’s very own NBA team, the Supersonics, are on the verge of moving to Oklahoma City. Suppose you went out today and bought a funerary urn featuring the Sonics logo only to have the team move to Oklahoma City. Then what? Do you really want to spend eternity in the funerary urn of a defunct team? That’s not the Scripting Guys’ definition of heaven.

Note. To be fair, we haven’t really come to a consensus on this matter. However, it’s safe to say that the Scripting Guys’ definition of heaven includes far more doughnuts and far less being holed up inside a funerary urn.

OK, sure, that would be different if it was a Tampa Bay Devil Rays funerary urn. But the Tampa Bay funerary urn isn’t available yet. If you want to spend an eternity of forlorn nothingness with the Tampa Bay Devil Rays you’ll just have to attend one of their games instead.

Which, come to think of it, isn’t the Scripting Guys’ definition of heaven, either.

Now, we know what you’re thinking. You’re thinking, “I don’t know, Scripting Guys; I mean, a Philadelphia Phillies funerary urn?!? That’s pretty tempting. What can you possibly offer us that could make life more attractive than death?” That’s a good question, and we’re not sure we can top a Philadelphia Phillies funerary urn. (Interesting note: Microsoft Word’s spellchecker doesn’t recognize Phillies as being a valid word.) But what if we offered you a script that creates a new WMI namespace under the root namespace:

strComputer = "."

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

Set objItem = objWMIService.Get("__Namespace")
Set objNamespace = objItem.SpawnInstance_

objNamespace.Name = "ScriptCenter"
objNamespace.Put_

Note. If you run this script on Windows Vista with normal administrator privileges, you’ll receive an error message:

C:\scripts\hey0725.vbs(9, 1) SWbemObjectEx: Access denied

If you actually want this script to work on Windows Vista, you need to run with elevated privileges. That means you should run the script from the command prompt: right-click Command Prompt and select Run As Administrator.

Admittedly, you might not ever need to create a new WMI namespace under the root namespace (or anywhere else for that matter). Nevertheless, we found this question interesting, simply because it demonstrates how cool WMI scripting really is; after all, you can even write scripts that modify WMI itself. (Although, as far as we know, you can’t yet outfit WMI in the colors and logo of your favorite major league baseball team.) Creating a new namespace might not be the most practical use for WMI scripting, but it’s definitely an interesting one. If you want more practical – and more typical – uses for WMI, then take at the WMI Scripting Primer in the Microsoft Windows 2000 Scripting Guide.

As for the script itself, we start out by binding to the WMI service on the local computer (although, as usual, you can run this script against a remote machine simply by assigning the name of that computer to the variable strComputer). After making the connection to WMI we then use the Get method to bind us to the __Namespace class (note the two underscores preceding the word Namespace):

Set objItem = objWMIService.Get("__Namespace")

The __Namespace class contains instance data for all the namespaces located in – in this case, anyway – the root namespace. Because we want to create a brand-new namespace our next step is to call the SpawnInstance_ method, which creates a new, “blank” namespace:

Set objNamespace = objItem.SpawnInstance_

And because our new namespace needs (at minimum) a name, we then assign the value ScriptCenter to the Name property:

objNamespace.Name = "ScriptCenter"

So are we done yet? Just about; all we need to do now is call the Put_ method and write the new namespace (root\ScriptCenter) to the WMI repository:

objNamespace.Put_

Now we’re done.

Metaphorically-speaking, that is.

Of course, we know what you’re thinking now; now you’re thinking, “What if I change my mind and decide to delete that new namespace? Can I do that, or am I going to be stuck with this namespace just like I’d be stuck with a Seattle Supersonics funerary urn?” Relax. If you want to delete the namespace root\ScriptCenter all you have to do is run this script:

strComputer = "."

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

Set objItem = objWMIService.Get("__Namespace.Name='ScriptCenter'")
objItem.Delete_

Note. Needless to say, you should take care when running the preceding script. If you want to create and then delete a custom namespace named ScriptCenter, well, have at it. But you’re asking for trouble if you use this same approach to delete one of WMI’s built-in namespaces.

In this second script we once again bind to the WMI service; however, this time around we use the following line of code to bind directly to the root\ScriptCenter namespace:

Set objItem = objWMIService.Get("__Namespace.Name='ScriptCenter'")

As you can see, we’re simply connecting to the instance of the __Namespace class that has a Name equal to ScriptCenter. Once we’ve made the connection we can then call the Delete_ method and remove the namespace from the WMI repository:

objItem.Delete_

Just that easy, just that quick.

As we noted earlier, we hope our faithful readers (neither of them) are in need of a funerary urn at the moment. (No, not even an urn featuring the team colors and logo of your favorite baseball team.) If you do need a funerary urn, however, keep in mind, at the moment, you can only get urns for the following teams:

· St. Louis Cardinals

· Atlanta Braves

· Boston Red Sox

· Chicago Cubs

· Detroit Tigers

· Los Angeles Dodgers

· New York Yankees

· Philadelphia Phillies

Although the final decision is up to you, the Scripting Guys recommend getting the Chicago Cubs urn. That’s because there’s such a nice symbolism there: you’re going to spend eternity in your Chicago urn, and the Cubs are likely to spend the same amount of time pursuing their next World Series championship.


How Can I Delete Duplicate Items From an Array?

Hey, Scripting Guy! How can I delete duplicate items from an array?

-- WR

Hey, WR. You know, if Microsoft has a weakness – and we’re not saying that it does, we’re just saying if it does – it’s this: we never make mistakes. (What’s that? What about Windows ME? That’s a weird name; couldn’t possibly be one of our products.)

Take arrays, for example. When someone at Microsoft creates an array it has exactly the right items in it; no more, no less. And that’s a bit of a problem: because our arrays are always perfect it never occurred to us to come up with an easy way to delete items from an array. After all, if you create a perfect array right off the bat then why would you ever need to delete anything from that array?

Of course, we also recognize that not everyone has a spotless record of success after success like we do. (Pardon? Microsoft Bob? No, sorry; never heard of him. Do you know what department he works in here?) Because of that, we do have workarounds for people who end up with duplicate items in their arrays. Can you delete duplicate items from an array? Sure. The process is a bit convoluted, but it’ll work.

Let’s show you a script that deletes duplicate items from an array named arrItems, then see if we can explain how it works:

Set objDictionary = CreateObject("Scripting.Dictionary")

arrItems = Array("a","b","b","c","c","c","d","e","e","e")

For Each strItem in arrItems
    If Not objDictionary.Exists(strItem) Then
        objDictionary.Add strItem, strItem
    End If
Next

intItems = objDictionary.Count - 1

ReDim arrItems(intItems)

i = 0

For Each strKey in objDictionary.Keys
    arrItems(i) = strKey
    i = i + 1
Next

For Each strItem in arrItems
    Wscript.Echo strItem
Next

As you can see, things start out in fairly straightforward fashion: in line 1 we simply create an instance of the Scripting.Dictionary object. (Why do we create a Dictionary object? Sit tight; we’ll explain that in a minute.) We then use this line of code to create an array named arrItems, an array that – alas – includes a bunch of duplicate items:

arrItems = Array("a","b","b","c","c","c","d","e","e","e")

As we intimated earlier, there really is no way – at least no simple, intuitive way – to delete items from an array, let alone to detect duplicate items within an array. Therefore we’re going to go a different route. What we decided to do here is take the items in the array and copy them into a Dictionary; we’re going to do that because it is possible to prevent duplicate items from being entered into a Dictionary. Once the Dictionary has been populated we’ll redimension our array and then (in essence) export the items in the Dictionary back to the array. Like we said, it’s a bit convoluted, but the net effect is what we’re looking for: when we’re all done the array arrItems will contain only unique values. No more duplicates.

Of course, all that hinges on us first getting the items in the array into the Dictionary. To do that we begin by setting up a For Each loop that loops through each and every item in the array:

For Each strItem in arrItems

Inside that loop we then use this line of code to determine whether the array item has already been added to the Dictionary:

If Not objDictionary.Exists(strItem) Then

It’s kind of crazy syntax, but If Not objDictionary.Exists(strItem) Then can be read like this: “If the array item does not already exist in the Dictionary then do the following.” And what is the following? This line of code, which adds the array item to the Dictionary, using the same value for both the Dictionary key and item:

objDictionary.Add strItem, strItem

Note. If you aren’t familiar with the Dictionary object, or with terminology like key and item, then check out this section of the Microsoft Windows 2000 Scripting Guide or the Sesame Script article title, appropriately, The Dictionary Object.

That’s fine, but what if the item does exist in the Dictionary? No problem; in that case we simply loop around and tackle the next item in the array.

Let’s take a brief timeout to show you this works. The first item in our array is a. When we check the Dictionary the first time through the loop, we won’t find a Dictionary key equal to a. Therefore, we add a to the Dictionary. We then loop around and repeat this process, this time with item b. That makes sense, right?

Now, notice that the third item is the array is also a b, a duplicate item. What happens when we encounter this duplicate item on our third time through the loop? Nothing; because b is already in the Dictionary we won’t add a second instance of b. (Actually we can’t add a second instance of b; the Dictionary object doesn’t allow duplicate keys.) Instead, we simply loop around and repeat this process with the fourth item in the array.

Etc.

When we’re all done we’ll have a Dictionary containing the following keys:

a
b
c
d
e

Those are your unique items right there, WR, which means you could simply work with the Dictionary object at this point. Just for the heck of it, though, let’s talk about how you could reconfigure the array arrItems so that it contains just those 5 values.

In order to reconfigure arrItems we first need to redimension the array; that is, we need to reset arrItems so that it will hold only as many items as we have in the Dictionary. To do that we first use this line of code to determine the number of items in the Dictionary, minus 1:

intItems = objDictionary.Count - 1

Note. Why “minus 1?” Well, when you redimension an array you must specify the index number for the last allowable item. The first item in an array is always given the index number 0; that means that, in an array with 5 items, the last item will have an index number of 4 (the total number of items minus 1). Thus we take total number of items and subtract 1; that gives us the index number that will be assigned to the last item in the array.

After assigning the variable intItems the number of items in the Dictionary (that is, the Dictionary Count) minus 1, we then use this line of code to redimension the array arrItems and, while we’re at it, delete all the existing data in the array:

ReDim arrItems(intItems)

In turn, that gives us an empty array that has reserved spots for 5 values. Now all we have to do is fill each of those 5 spots.

Is that going to be hard? No, not really. After assigning the value 0 to a counter variable named i we set up a For Each loop to loop through each of the keys in the Dictionary:

For Each strKey in objDictionary.Keys
    arrItems(i) = strKey
    i = i + 1
Next

Inside that loop we assign the first item in the array (remember, the first item has an index number of 0) the value of the first key in the Dictionary. We then increment our counter variable by 1, loop around, and repeat the process, this time assigning the second item in the array (index number 1) the value of the second key in the Dictionary. We simply repeat this process until each key in the Dictionary has been assigned a spot in the array.

Is that really going to work? You bet it will. But just in case you don’t believe us, we’ve tacked some code to the end of the script that echoes back all the items in the array arrItems. Here’s what we get back when we execute that block of code:

a
b
c
d
e

Absolutely perfect. But, then again, what else would you expect from Microsoft?


How Can I Display a Message Box That Has No Buttons, and That Disappears After a Specified Period of Time?

Hey, Scripting Guy! I would like to display a message box for a set amount of time and then have it disappear. However, I’d prefer that this message box not have a close button, or have anything else that would allow the user to close the message, minimize the message, etc. How can I do that?

-- D

Hey, D. You know, this question takes us back to the beginning days of Hey, Scripting Guy! Back when we first started, it seemed like each and every column would go like this:

Hey, Scripting Guy! How can I do x in a script?

-- SP

Hey, SP. You can’t.

But, wait, don’t go yet: we have a workaround for you. All you have to do is ….

The first time we told people they couldn’t do something and then immediately turned around and told them how to do it, well, that was pretty cute; by the 138th time, however, it wasn’t quite so cute anymore. Because of that we swore on Peter’s grave that we would never use such a corny opener ever again.

Which didn’t make Peter all that happy, considering the fact that he’s still very much alive. In our defense, though, with Peter it’s sometimes hard to tell.

So instead of saying that you can’t display a message box that doesn’t have a button on it (and then – after dashing your hopes – telling you that we have a workaround anyway) we’ll just talk about something else. You know… we’ll talk about … stuff. Right. Oh: baseball. What’s up with that? And, that one thing, the one that was in the newspaper? That was really something, wasn’t ….

Oh, what the heck: the truth is, D, you can’t display a message box (for any amount of time) that doesn’t include a button of some kind. But, wait, don’t go yet: we have a workaround for you.

Note. Sorry. But that just had to be said.

So what kind of workaround do we have? This kind:

<html>
    <HTA:APPLICATION
        Caption="no"
    >

    <script language = "VBScript">
        Sub Window_OnLoad
            idTimer = window.setTimeout("PausedSection", 5000, "VBScript")
        End Sub

        Sub PausedSection
            window.close
        End Sub
    </script>

    <body>
        <p>Your message goes here</p>
    </body>
</html>

What we’ve got here is a simple little HTA (HTML Application). An HTA is similar to an HTML file; the big difference is that HTAs use the file extension .HTA and are designed to run locally instead of over the Internet. In addition, you can create an HTA that does not include a close button (or even a title bar), and you can display any message you choose to display. Technically it’s not a message box, but we’re assuming that doesn’t matter: you just want something to display on screen for a set amount of time and then disappear. On top of that, you don’t want to give the user an obvious way to dismiss that message before the time expires. The little HTA we’re about to show you meets all those criteria.

Note. Where can you learn more about HTAs? Funny you should ask ….

So how do we get this all to work? Well, to begin with, copy the preceding code, paste it into Notepad or some other script editor, and then save the file with a .HTA file extension. First things first.

Now let’s talk about the file you just created. The HTA itself is divided into three sections: basic properties for the HTA, the message displayed in the body of the HTA, and the code that causes the HTA to dismiss itself after a set amount of time (in this example, after 5 seconds). Let’s start off by looking at the section where we configure the HTA properties:

<HTA:APPLICATION
    Caption="no"
>

As you can see, we set only one property here: we set the Caption property to No. What that does is remove the title bar, the close button, the minimize and maximize buttons, and just about everything else from the HTA window, leaving you with something that looks like this:

Nice, huh? (You’re right, it does look a lot like the list of Scripting Guy achievements over the past year or so, doesn’t it?)

If that’s a bit too severe for you, then use this code and set the SysMenu property to No:

<HTA:APPLICATION
    SysMenu="no"
>

When you do that you’ll still have the title bar, but no close, minimize, or maximize buttons, so there’s still no obvious way to close the HTA. (To dismiss it you’d need to terminate the Mshta.exe process.)

If you go this route you might also want to use the <TITLE> tag to give the page a title:

<head>
    <title>Test</title>
</head>

Just insert that right under the <HTML> tag. That way you’ll have a custom caption (in this case, Test) that appears in the title bar. If you don’t specifically assign a title then the file path will appear in the title bar.

If you thought it was easy to configure the HTA properties, well, have we got news for you: it’s even easier to specify a message for your HTA:

<body>
    <p>Your message goes here</p>
</body>

Just type your message in the space labeled Your message goes here, and you’re on your way. And because this is HTML you can change fonts and font sizes, add graphics, and do anything else that can be done in HTML. But we’ll leave that up to you. (Hey, it wouldn’t be any fun for you if we did everything, now would it?)

That brings us to the <SCRIPT> section, which is where we control the amount of time that the HTA appears onscreen. To do that, we use these two subroutines:

Sub Window_OnLoad
    idTimer = window.setTimeout("PausedSection", 5000, "VBScript")
End Sub

Sub PausedSection
    window.close
End Sub

You’re right: there isn’t much happening here, is there? To begin with, we have a subroutine named Window_OnLoad; as you probably know, the Window_OnLoad subroutine is designed to automatically run any time an HTA is loaded or refreshed. As you can see, we don’t do much inside this subroutine; in fact, all that we do do is create a timer named idTimer. That timer is designed to wait a specified number of seconds and then call a second subroutine (PausedSection). What will this second subroutine do? You’ll find out in just a second.

Here’s what the code looks like:

idTimer = window.setTimeout("PausedSection", 5000, "VBScript")

As it turns out, to create a timer all we have to do is call the setTimeout method, passing three parameters:

· PausedSection, the subroutine we want to call after the five-second pause.

· 5000, the number of milliseconds to pause. As you probably figured out, 1,000 milliseconds is equal to one second. Want to display the message for 20 seconds? No problem; just change this parameter to 20000.

· VBScript, the script language we’re using.

In other words, each time we load the HTA the Window_OnLoad subroutine sets a timer. That timer will click off 5,000 milliseconds and then run a subroutine name PausedSection. In turn, that subroutine does one thing and one thing only; it closes the HTA:

Sub PausedSection
    window.close
End Sub

The net result? An “unclosable” HTA will pop up on screen, display your message, and then automatically disappear after 5 seconds. Which sounds like exactly the sort of behavior you were hoping to get.

Note. Good point: by default the HTA window is a little big, and isn’t centered on the screen, either. Fortunately, we have a column here and another column here that can help you resolve those issues.

After you’ve created the .HTA file all you have to do within your script (that is, within the .vbs file) is include code that will call and display this HTA. Something along these lines ought to do the trick:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "no_button.hta"

We hope that helps, D. Granted, it’s not exactly what you asked for; for one thing, it’s not a message box. But seeing as how you can’t display a message box that doesn’t have any buttons, well, this seems like the next best thing.

And, yes, some people would call this a workaround. But the Scripting Guys would never do that.


How Can I Format a Series of Numbers as a Phone Number?

Hey, Scripting Guy! How can I format a series of numbers as a phone number in Windows PowerShell?

-- HR

Hey, HR. You know, formatting is all about making things look a little nicer and a little more presentable. That’s why the Scripting Guy who writes this column always finds it strange when people ask him formatting questions. A little nicer and a little more presentable? Right now the Scripting Guy who writes this column is wearing a Homer Simpson T-shirt that says, “If something is hard it’s not worth doing.” He’s also wearing a ratty old long-sleeved shirt over the T-shirt; a pair of jeans; and a pair of three-year-old tennis shoes (although the shoelaces are brand-new). His entire ensemble is topped off by a half-smashed baseball hat that has Japan written across the front. And you want him telling you how to make things look a little nicer and a little more presentable?

Note. Here’s something really scary: if you think the Scripting Guy who writes this column looks bad, well, you should see the other Scripting Guys.

No, on second thought, maybe you shouldn’t.

But what the heck, HR; let’s see if we can help you with your formatting question anyway. Suppose you’ve type the following command into the Windows PowerShell console window:

$a = 5551234567

That’s nice, but what you’d really like to do is reformat $a so that it looks like this:

(555) 123-4567

So how can you do that? Why, by using this custom formatting string, of course:

$b = "{0:(###) ###-####}" -f $a

Let’s see if we can explain how this works. When it comes to formatting numbers and dates Windows PowerShell has no dedicated Cmdlets equivalent to the VBScript FormatDate or FormatNumber functions; instead, PowerShell taps into the .NET Framework when formatting values. The drawback to using .NET is that it requires the crazy-looking syntax shown above. The advantage to using .NET? You can create custom formats for displaying data in all sorts of different ways, including taking a series of numbers and displaying them as a phone number.

In order to understand this a little better let’s dissect our formatting command. What we’re doing with this command is applying a format to the variable $a; the gobbledygook between the double quote marks is the format being applied (details to follow), and the –f parameter is the signal we use to tell Windows PowerShell that we’re doing some formatting. The value to be formatted ($a) comes after the –f parameter, and the newly-formatted value is assigned to a variable named $b. And no, we don’t actually have to assign the newly-formatted value to a variable; if we wanted to we could use a command like this and simply display that value:

"{0:(###) ###-####}" -f $a

Now, what about the crazy-looking syntax? As you can see, the entire formatting string is nested first between a pair of double quote marks and then between a pair of curly braces: "{0:(###) ###-####}" We then have a 0 followed by a colon: 0:. That simply tells Windows PowerShell that we want to format the first item (item 0). For now consider that to be the default, and just use 0: without worrying too much about it.

The part that you do need to worry about (or at least the part you need to understand) is the actual formatting string itself: (###) ###-####. If you’re thinking, “You know, that kind of looks like a phone number,” well, that’s good; after all, it’s supposed to look like a phone number. In this formatting string the pound signs (#) each represent a digit; the parentheses and hyphens represent parentheses and hyphens. What we’re saying here is that we want to use the following format:

· An open parentheses followed by three digits.

· A closed parentheses and a space.

· Three digits and a hyphen.

· The final four digits.

That means we’re going to take a value like 5551234567 and display it like this:

(555) 123-4567

Lo and behold, there’s our phone number.

As long as we’re on the subject here are a couple of other custom formatting commands you might find useful. For example, suppose you want to format a value using leading zeroes. Well, here’s an example for you:

$a = 123
"{0:000000}" -f $a

What are you going to get when you run these two commands? This:

000123

How about a fixed number of decimal places? Are three decimal places enough? Okey-doke:

$a = 123.456789
"{0:#.000}" -f $a

That will result in output that looks like this (note that the value has been rounded up to the nearest decimal place):

123.457

Here’s one that uses U.S. currency as the format; note the $ symbol included at the beginning of the formatting string:

$a = 123.45
"{0:$#.00}" -f $a

That yields a value the looks like this:

$123.45

You can do similar things for dates. But that’s another story for another day.

We hope that helps, HR. And remember, if you ever need any other advice on making things look nicer and a little more presentable the Scripting Guys will be here. Take interior decorating, for example. The Scripting Guy who writes this column has an office that has nothing hanging on the walls other than a map of the world, a map which is hidden behind his door and is hanging there simply because the previous tenant left it hanging there. He has no plants, no photographs, no personal mementoes, nothing.

Note. Well, OK, with one exception: taped to his door is a fortune from a Chinese fortune cookie that says, “Others take notice of your radiance. Share your happiness.” The other Scripting Guys agree that they’ve never heard a more apt description of the Scripting Guy who writes this column.

Best of all, following the last office move he didn’t even bother to unpack his things; instead, two months after the move all his unopened boxes remain piled up in one corner of the office. If you’d like to have an office like that, well, just let him know; we’re sure he’d be happy to help. After all, sharing his happiness is what he does best.


How Can I Play a Sound Each Time a Message Box is Displayed?

Hey, Scripting Guy! I was using a script written by one of my colleagues, and I noticed that any time this script displayed a message box a sound was played as well. How can I play a sound each time I display a message box?

-- KI

Hey, KI. You’ll have to give us a second here; the Scripting Guy who writes this column got home a little late last night, having ventured into the wilds of Seattle to watch the Mariners play the Angels. (And yes, now that the Scripting Son’s baseball season is finally over, he and his father celebrated by, uh, going to a baseball game.) As it turned out, however, the two saw a pretty good game: not only did the Mariners win 2-0, but the Scripting Guy who writes this column correctly predicted that the green hydroplane would win the animated Hydro Challenge. Not a bad evening no matter how you look at it.

In addition, father and son also had the pleasure of sitting in front of a guy who knew everything about baseball, and who kept up a non-stop commentary all night long. To give you a sample of what the night was like, here are a few of his more interesting pronouncements:

“Richie Sexson, now there’s a guy who was really hurt by the crackdown on steroids. He had that one year where he hit 75 home runs, but after the crackdown, nothing. Not even close.”

Note. In case you’re wondering, Richie Sexson has never hit 75 home runs in a season; in fact, the record for most home runs in a season (held by Barry Bonds) is 73. The best Richie Sexson ever did was hit 45 homers in one year, something he accomplished twice while playing for the Milwaukee Brewers.

“Hank Aaron was the only clean baseball player, ever. Everyone before him and after him was all drugged-up. They’re all a bunch of liars, cheats, and thieves.”

“Oh, sure, baseball executives, politicians, businessmen – they all have to pretend that the problem never existed. If they owned up to the fact that the players have all been taking steroids then they’d have to give all the fans their money back, and they’d have to refund all the money paid out for advertising and TV rights and everything else. That would wreck the entire economy, absolutely wreck it. Because of that, everyone pretends that there’s no such thing as steroids. That’s why they’ve never cracked down on any of this stuff.”

Note. For an interesting comparison, see the first quote, the one about baseball having cracked down on steroids.

And, of course, our very favorite:

“Come on, Beltre; hit the ball! You know what? I think Adrian Beltre needs Viagra for his batting brain!”

No, we have no idea what that means, either. Send your suggestions to scripter@microsoft.com.

On second thought, don’t bother; all things considered, we’d rather not know what that means.

In all fairness, however, we must admit that the guy did say at least one thing that made a lot of sense:

“Come on, blue: open your eyes! Don’t you know that you can play a sound any time a message box appears simply by including an icon in that message box?”

In other words:

Msgbox "An error has occurred. Do you want to continue?", _
    vbYesNo + vbCritical, "Path Not Found"

So did this guy really know what he was talking about? Well, let’s find out. As you can see, in this example we’ve simply called the Msgbox function and passed in three parameters:

· “An error has occurred. Do you want to continue?”

· vbYesNo + vbCritical

· “Path Not Found”

The first parameter is easy to figure out; that’s simply the message we want displayed in the message box. The third parameter is also pretty straightforward: that’s the title we want to assign to our message box. That leaves us with this: vbYesNo + vbCritical.

As it turns out, we’re actually defining two things here. First, we’re using the VBScript constant vbYesNo to indicate that we want to include a Yes button and a No button in our message box. Are we limited to just Yes and No buttons in a message box? Of course not; here are all the button combinations available to you:

Constant | Buttons Displayed
vbOKOnly | OK
vbOKCancel | OK and Cancel
vbAbortRetryIgnore | Abort, Retry, and Ignore
vbYesNoCancel | Yes, No, and Cancel
vbYesNo | Yes and No
vbRetryCancel | Retry and Cancel

But notice that we aren’t just defining the set of buttons to be displayed in our message box; we’re also defining an icon to be displayed. That’s what the constant vbCritical is for; it displays a Critical Message icon (a big red circle with an X in it) in the message box. You know, like this:

Ah, but that’s not all. When you assign an icon you get the icon, sure, but you also get the system sound associated with the event (in this case, Critical Message). That’s how you can play a sound each time a message box is displayed: just be sure and assign an icon to the message box. Don’t like either the sound or the icon for Critical Message? Then try one of these constants instead:

Constant | Icon Displayed/Sound Played
vbCritical | Critical Message
vbQuestion | Warning Query
vbExclamation | Warning Message
vbInformation | Information Message

See how this works? Suppose you wanted to display OK and Cancel buttons along with the Warning Message icon (and sound). That’s fine; in that case your call to the Msgbox function would look like this:

Msgbox "An error has occurred. Do you want to continue?", _
    vbOKCancel + vbExclamation, "Path Not Found"

Etc.

Here’s another nifty trick. Try adding the constant vbSystemModal to the Msgbox function:

Msgbox "An error has occurred. Do you want to continue?", _
    vbOKCancel + vbExclamation + vbSystemModal, "Path Not Found"

Why would you want to do that? Well, if you make this a system modal message box, then the message box remains on top of all your other windows, even if you switch to a different application. (You can still use that application, but the message box remains on top. Give it a try and you’ll see what we mean.) Is that useful? It can be; if nothing else, it makes it a little bit harder for users to ignore your message box.

Before you ask, yes, you are limited to those four icons and the system sounds associated with those events. (And again, before you ask, there’s really no way to change those system sounds programmatically.) If you’d rather play a different sound each time a message box is displayed then you might want to take a look at one of our previous Hey, Scripting Guy! columns. The method employed there is far from foolproof, but you might find it useful. Give it a try and see for yourself.

That should do it, KI. As for the baseball game, we’ll let the Scripting Son have the last word. Mystified by a pair of ball and strike calls, the Scripting Son sighed in exasperation. “Come on, blue,” he called to the ump. “Try turning home plate over and reading the instructions.” Although the Scripting Guy who writes this column has hung around baseball fields all his life, he’d never heard that one before.


How Can I Read the Contents of a Text File and Then Declare the Contents as Variables?

Hey, Scripting Guy! How can I read the contents of a text file, and then declare those contents as variables?

-- TC

Hey, TC. Before we begin, did you really ask us this question? We’re positive that you did, but, the way things have been going lately, we can’t be too sure.

For example, by now everyone has heard that Scripting Guy Jean Ross went down to San Diego for the MMS conference, only to return with nearly a dozen surgical staples in her head.

Note. If you haven’t heard, Jean was taking a leisurely stroll through the San Diego Zoo when she noticed that a group of kindergarteners, on a field trip, had accidentally wandered into the grizzly bears’ compound. With complete and utter disregard for her own well-being Jean leaped over the fence, fought off the bears, and carried each and every child to safety. As it turned out, during the battle one of grizzlies took a swipe at Jean’s head and managed to inflict a nasty gash. After the children were all safe and sound, Jean – true to form – went back into the compound, performed first aid on the grizzly’s paw, and only then went to the hospital and had her own wound tended to.

But here’s the weird part (interestingly enough, for Jean getting her head stapled together wasn’t the weird part). The other day our Scripting Hero called the insurance company with a question about her claim; the insurance company told her that they had no record of her being treated in San Diego. Apparently she hadn’t had her head stapled back together while in San Diego!

It gets weirder. Last week the Scripting Guy who writes this column was writing this column when his computer died. When he tried restarting it (and he tried many times) he always got the same message: Non-system or disk error. At one point he did manage to get into the BIOS, thinking he would run the hard disk diagnostic test. What happened? Nothing: that option was grayed-out. Apparently the computer didn’t have a hard disk.

At that point the Scripting Guy who writes this column did what Scripting Guys always do in times of crisis: he went to lunch. When he came back he decided to try the computer one more time, just for the heck of it. Needless to say, it booted up just fine. He restarted it again, and again. No problem. He got back into the BIOS and discovered that his computer did have a hard disk; on top of that, it passed all the diagnostic checks with flying colors. Apparently his computer hadn’t died after all.

Very weird.

So, anyway, we can’t be sure, TC, that you actually asked a question about reading lines from a text file and then declaring that information as variables. However, if you did ask a question about reading lines from a text file and then declaring that information as variables here’s the answer.

Unless, of course, it turns out that we didn’t answer your question after all. But let’s try the following and see what happens:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Do Until objFile.AtEndOfStream
    strLine = objFile.ReadLine
    Execute strLine
Loop

objFile.Close

Wscript.Echo strFirstName
Wscript.Echo strLastName

OK. If we understand your question (that is, the question you either did or did not ask), you have a text file with lines similar to this:

strFirstName = "Ken"
strLastName = "Myer"

As you can see, each line looks an awful lot like a line of code assigning a value to a variable; in fact, if you typed these lines as-is into a script you’d end up with a variable named strFirstName (with a value of Ken) and a variable named strLastName (with a value of Myer). Of course, you don’t want to type those lines as-is into a script; instead, you want to read those lines from a text file and then execute each line as if you had typed it into the script. In other words, you want your script to function as though you had typed in those two lines of code, even though you never actually typed in those two lines of code.

See? We told you everything was getting weird!

So how are we going to do all that (whatever it is)? Well, we’re going to start off by defining a constant named ForReading and setting the value to 1; that’s a constant we’ll need when we open the text file. After the constant has been defined we next create an instance of the Scripting.FileSystemObject object and then open the file C:\Scripts\Test.txt:

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Now for the good part. What we need to do next is read in the text file line-by-line, and then execute each line in the text file as though it was a line of code we had typed into our script. (Why do we keep using the verb “execute?” You’ll find out in just a second.) To do that, we first set up a Do Until loop designed to run until we reach the end of the text file (that is, until the file’s AtEndOfStream property is True). Inside that loop we use the ReadLine method to read the first line from the text file and then store that value in a variable named strLine:

strLine = objFile.Readline

Got that? That simply means that strLine is equal to this:

strFirstName = "Ken"

Is that going to help us? You bet it is. To create a variable named strFirstName, with a value of Ken, all we have to do now is call the Execute statement followed by the value to be executed:

Execute strLine

The Execute statement is a little-known yet interesting feature of VBScript; among other things, this statement enables you to execute a string value (or variable) as if you had typed that value into your script as a line of code. Confusing? Yes. But take a look at this sample script:

strMessage = "Hi"
strCode = "Msgbox strMessage"
Execute strCode

Here we’ve stored the text value Hi into a variable named strMessage. We’ve then assigned the following value to a variable named strCode:

Msgbox strMessage

And you’re right, that does look like a line of code for displaying the value of strMessage in a message box. And there’s a good reason for that: it is a line of code for displaying the value of strMessage in a message box. That’s the whole idea. In order to now run that line of code (that is, in order to display the message box) all we have to do is call the Execute statement followed by the string value (in this case, the variable strCode) we want to execute.

Give it a try and you’ll see what we’re talking about.

At any rate, when we call the Execute statement we end up executing this value:

strFirstName = "Ken"

And guess what? That’s going to create a variable named strFirstName and assign it the value Ken. And then we’re going to loop around and repeat this process with the next line in the text file.

Admittedly, this all sounds crazy. But look at what we get back when we echo back the values of strFirstName and strLastName:

Ken
Myer

Obviously strFirstName and strLastName are valid variables. And how do you suppose they got assigned their respective values? You got it: from the information found in the text file. Pretty cool, huh?

Admittedly, we’re not sure how often you’ll need to read lines of code from a text file and then declare those lines of code as variables. Nevertheless, there are times when the Execute statement is invaluable. What times? Well, for at least one example, see this event from the 2007 Winter Scripting Games.

Anyway, we hope that answers your question, TC. If it turns out that you didn’t actually ask us a question, well, that’s OK: the ways things have been going lately we’re not so sure we answered the question anyway. Have we mentioned the two Scripting Guys who signed up for TechEd 2007 and received emails confirming their registration, then a few days later received emails telling them that they hadn’t registered? Only then to be told that they did register after all. Only – well, never mind. Most likely none of that happened, either. Either way, we’ll see everyone at TechEd 2007.

Assuming, of course, that there is such a thing as TechEd 2007.


How Can I Remove All the Non-Alphabetic Characters in a String?

Hey, Scripting Guy! How can I remove all the non-alphabetic characters in a string?

-- CD

Hey, CD. You know, typically the Scripting Guys don’t play favorites; we treat all our readers and all the questions we receive exactly the same. (OK, so, technically that means that, for the most part, we simply ignore all our readers as well as all the questions we receive. But at least we’re treating everyone the same!)

This time, however, we’re going to make an exception: CD, this is a great question, one of the best we’ve ever received. Thank you so much for asking how you can write a script that removes all the non-alphabetic characters in a string. This is a truly great, great question.

So, anyway, the best way to remove all the – what’s that? Is this a great question because it addresses the concerns of so many system administration scripters? Well, to tell you the truth, we’re not really sure how many people need to remove all the non-alphabetic characters in a string, although we have received questions along similar lines. So then is this a great question because it provides the Scripting Guys an opportunity to explain an important yet little-understood concept about system administration scripting? Well, maybe. But that isn’t why we thought this was a great question. No, we thought this was a great question because we knew we could answer it in just a few minutes, and with just a few lines of code. In turn, that means we can get today’s column out of the way and move on to something that everyone really cares about: tonight’s college football game between the University of Washington Huskies and the Syracuse Orange.

Note. If you go to the official site of Syracuse University Athletics you’ll learn that the favorite fruit of volleyball player Mindy Stanislovaitis is the strawberry, and that her favorite superhero is Spiderman. You’ll also learn that the Orange are apparently playing the Washington Post rather than the Washington Huskies in tonight’s opening game. Of course, if you go 5-18 over the past two years, as Syracuse has, well, then it probably makes sense to open the season against a newspaper rather than another college football team.

At any rate, kickoff is just a few hours away, so we better get started on our answer for today’s question (which, by the way, is a great, great question):

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "[^A-Za-z]"

strSearchString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz!@#$%&*(_+"

strSearchString = objRegEx.Replace(strSearchString, "")

Wscript.Echo strSearchString

See? We told you this was going to be quick. And easy.

Although there are other ways we could accomplish this task, this easiest approach (in our minds, anyway) is to use a regular expression. Therefore, the very first thing we do in this script is execute the following line of code, a line of code that creates an instance of VBScript’s regular expression object (VBScript.RegExp):

Set objRegEx = CreateObject("VBScript.RegExp")

Once we have a regular expression object in hand, we then set two important properties of the object. First, we set the Global property to True; we do this so that when we execute the Replace method this ensures that the regular expression will search the entire string value. What if we didn’t set the Global property to True? In that case, our regular expression would find the first instance of the target text (that is, the first non-alphabetic character) and then stop. Admittedly, there are times when all you need to know is whether or not there is at least one non-alphabetic character in a string. However, this isn’t one of those times.

Next, we need to define the regular expression Pattern, the text we’re going to search for. That’s what this line of code does:

objRegEx.Pattern = "[^A-Za-z]"

The key to working with regular expressions is this: don’t let the cryptic syntax scare you off. Too often people look at a construction like [^A-Za-z] and think, “Well, I’m not sure who that’s intended for, but it’s definitely not intended for me.” As if turns out, however, this is actually a pretty straightforward little statement. With regular expressions, you can search for a range of characters simply by enclosing those characters in square brackets. Want to search for all the characters between A and Z (uppercase letters), inclusive? Then use this syntax: [A-Z]. What about all the lowercase characters, those between a and z, inclusive? No problem: [a-z]. And if you want to search for either uppercase characters or lowercase characters, well, one way to do that is include both character ranges in a single set of square brackets: [A-Za-z].

Like we said, simple and straightforward.

OK, but what about the little caret symbol (^)? Well, any time the caret appears alongside a character range that caret is equivalent to the word “not.” What does [^A-Za-z] mean? It means this: search for any characters that are not in the range A-Z or a-z. In other words, search for any non-alphabetic characters.

Which, coincidentally enough, is just exactly what we want to search for.

Believe it or not, from here on the task gets even easier. To begin with, we assign a value to a variable named strSearchString; note the many non-alphabetic characters in the value:

strSearchString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz!@#$%&*(_+"

After that we call the Replace method, passing the method two parameters: the value we want to search (strSearchString) and the replacement text. In this case, of course, we want to replace non-alphabetic characters with absolutely nothing; therefore, we use an empty string ("") as the replacement text:

strSearchString = objRegEx.Replace(strSearchString, "")

That line of code should remove all the non-alphabetic characters in strSearchString. (That is, it should locate each of these characters and replace them with absolutely nothing.) So does that line of code remove all the non-alphabetic characters in strSearchString? Let’s see what happens when we echo back the value of strSearchString:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

Well, what do you know?

In case you’re wondering, yes, you can use this same approach to replace all the non-alphabetic characters in a text file; in fact, we threw together a sample script that does just that. Note that, for this script, we added something to our Pattern: \n\r. This tells the script that we want to exclude the newline character (\n) and the carriage return character (\r) from the search scope. In turn, that means that our script will not remove any line breaks from the text file. If you don’t care about preserving line breaks, well, then just remove the \n\r from the Pattern.

Without any further ado (or explanation), here’s the script:

Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForReading)

strSearchString = objFile.ReadAll
objFile.Close

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "[^A-Za-z\n\r]"

strSearchString = objRegEx.Replace(strSearchString, "")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt",
ForWriting)
objFile.WriteLine strSearchString

objFile.Close

Just a little bonus script for everyone. Needless to say, now that college football is upon us, the Scripting Guy who writes this column is happy. For a change.

Speaking of which, we should note that this same Scripting Guy is under no illusions about the upcoming season: as a long-time season ticket holder, he knows UW football about as well as anyone. (He also knows that they play the toughest schedule in the country this season, playing 9 teams that went to bowl games last year.) But that’s the great thing about sports: until they start playing games, everyone is undefeated, and everyone has a chance to be the national champion.

Especially those teams that open the season against the Washington Post.


How Can I Replace Null Values in an Array?

Hey, Scripting Guy! How can I loop through a 12x12 array and replace any null values with a hyphen?

-- AM

Hey, AM. Sorry for the delay in getting back to you; as it turns out, yesterday was Independence Day (more typically referred to as the Fourth of July) in the good old USA, and the Scripting Guys, along with their fellow Microsoft employees, demonstrated their patriotism by taking the day off. As most of you probably know, taking Independence Day off typically means picnics, parades, and fireworks, although a couple years ago the Scripting Guy who writes this column got so swept up in the excitement of the day that he celebrated by burning his house down.

Note to all readers, but especially to those who work for the insurance company. Technically, it was only the garage that burned down; in addition the Scripting Guy who writes this column wasn’t the one was actually burned it down. Officially the cause of the fire was listed as “unknown,” although the fire inspector strongly suspected that an errant firework shot off by one of the neighbors landed in the garage, smoldered for a day or two, and then caught on fire. Even if that was the case, though, the Scripting Guy who writes this column holds no grudges against his neighbors. After all, had the shoe been on the other foot and he lived next door to himself, he’d probably try to burn his house down, too.

Note to all readers, but especially to those who work for the insurance company: No, he wouldn’t really do that. He’s just joking. Fortunately, insurance companies are known for their sense of humor.

At any rate, if you aren’t from the US you probably didn’t get to see any fireworks yesterday. But that’s OK; here’s the next-best thing to a fireworks display, a script that can loop through an array and replace any empty values with a hyphen:

Dim arrNumbers(2, 2)

arrNumbers(0,1) = 100
arrNumbers(1,2) = 200
arrNumbers(2,1) = 300
arrNumbers(2,2) = 400

For x = 0 to 2
For y = 0 to 2
If arrNumbers(x, y) = "" Then
arrNumbers(x, y) = "-"
End If
Next
Next

For x = 0 to 2
For y = 0 to 2
Wscript.Echo x & "," & y & ": " &
arrNumbers(x, y)
Next
Next

Before we go much further we have to confess something. (No, not to that. We already told you: someone else burned down the house.) Although AM asked for a 12x12 array our sample script actually creates a 2x2 array. Why? Well, we used a 2x2 array simply so we could display all the array values (and demonstrate that the script actually worked) without having to publish script output that scrolled on for miles and miles. But that’s OK. After all, to create a 2x2 array we simply use the Dim statement followed by the array name (arrNumbers) and the array dimensions (2x2):

Dim arrNumbers(2, 2)

Suppose we did want to create a 12x12 array. Then all we need to do is change the array dimensions as needed:

Dim arrNumbers(12, 12)

See? No big deal.

After we define the array and its dimensions the next four lines of code are used to assign values to a few of the items in that array. You can think of a 2x2 array as being a grid that looks something like this, with periods representing blank spaces (something we have to do because of the crazy way empty table cells get displayed on TechNet):

. | 0 | 1 | 2
0 | . | . | .
1 | . | . | .
2 | . | . | .

If we assign item 0,1 the value 100 (which is exactly what we do), that means the grid will look like this:

. | 0 | 1 | 2
0 | . | 100 | .
1 | . | . | .
2 | . | . | .

And after we finish assigning values to four different items the grid will look like this:

. | 0 | 1 | 2
0 | . | 100 | .
1 | . | . | 200
2 | . | 300 | 400

Does that make sense? Excellent. These four lines of code simply assign values to four of the items in the array:

arrNumbers(0,1) = 100
arrNumbers(1,2) = 200
arrNumbers(2,1) = 300
arrNumbers(2,2) = 400

Now, if you studied the grid carefully (and you did study the grid carefully, didn’t you?) you might have noticed that several of the items have no value whatsoever; for example, item 0,0 is blank. These are the items AM is interested in; he wants to identify all the empty squares (that is, items in the array that have no value) and replace those values with hyphens. In other words, he wants to modify the grid so that, when the script finishes, it looks like this:

. | 0 | 1 | 2
0 | - | 100 | -
1 | - | - | 200
2 | - | 300 | 400

Can we do that? Of course we can; it’s as easy as burning down a house.

Um, scratch that. Let’s go with “It’s as easy as falling off a log” instead.

So how do we identify array items that have no value, and then replace those empty values with hyphens? Like this:

For x = 0 to 2
    For y = 0 to 2
        If arrNumbers(x, y) = "" Then
            arrNumbers(x, y) = "-"
        End If
    Next
Next

What we’ve got here is a nested For loop: a For Next loop inside another For Next loop. We start out with a For Next loop that uses a counter variable named x and that runs from 0 to 2:

For x = 0 to 2

Inside that loop we then have a second For Next loop that also runs from 0 to 2. The only difference? This loop uses a counter variable named y:

For y = 0 to 2

What’s the point of all that? Well, we need to run through all the items in the array; those items include the following:

· 0,0

· 0,1

· 0,2

· 1,0

· 1,1

· 1,2

· 2,0

· 2,1

· 2,2

If you follow the logic, you’ll see that our first loop covers all the possible values (0, 1, and 2) in the first dimension of the array; the second loop does the same thing for the array’s second dimension. By combining the two loops we end up hitting each and every item in the array.

So then what do we do to each and every item in the array? That’s easy: we burn them to the ground!

No, hey, just kidding. Instead, we look to see if the value of the item is equal to an empty string:

If arrNumbers(x, y) = "" Then

If the value is not equal to an empty string we simply loop around and repeat the process with the next item in the array. If the value is equal to an empty string we assign a hyphen as the new item value and then loop round and repeat the process with the next item in the array. Here’s how we assign a hyphen to the array item:

arrNumbers(x, y) = "-"

And that’s all we have to do. After we’ve looked at and, if necessary, modified each value in the array we then execute this block of code, which simply echoes back each item and its value:

For x = 0 to 2
    For y = 0 to 2
        Wscript.Echo x & "," & y & ": " & arrNumbers(x, y)
    Next
Next

That output should look like this:

0,0: -
0,1: 100
0,2: -
1,0: -
1,1: -
1,2: 200
2,0: -
2,1: 300
2,2: 400

As you can see, each empty value in the array has been replaced by a hyphen. It’s just like magic.

Except, for the fact that it isn’t anything at all like magic.

And there you have it, AM. Before we call it a day we should note, in the spirit of diversity and globalism, that the Fourth of July is important to all the people of the world, and not just to Americans. For example, on July 4, 836 the Pactum Sicardi was signed, bringing peace to the Principality of Benevento and the Duchy of Naples. Likewise, on July 4, 1359 Francesco II Ordelaffi of Forlì surrendered to the Papal commander Gil de Albornoz. The list goes on and on. The Fourth of July: a little something for everyone.


How Can I Split a String Only on Specific Instances of a Character?

Hey, Scripting Guy! I have a series of string values that I need to convert to an array, splitting the value on the ampersand (&). However, if the ampersand is followed by amp (in other words, if the string is &amp) I don’t want to split the string at that point. How do I do that?

-- AK

Hey, AK. We apologize for taking so long to post the answer to your question. We actually answered this a long time ago, but every time we sent it in for editing the Scripting Editor rejected it; the whole thing sounded so crazy she assumed we’d started drinking. But that’s not true; we actually started drinking a little over 6 years ago.

Note. Why, yes, as a matter of fact the Scripting Guy who writes this column did start working at Microsoft a little over 6 years ago. How did you know?

Editor’s Note: Keep in mind that by “drinking” we’re not necessarily referring to alcoholic beverages. One too many espresso shots have been known to have adverse effects on some of the Scripting Guys. Although so has one too few shots. It’s a delicate balance around here sometimes.

Admittedly, this is a somewhat crazy question. So let’s see if we can explain the situation a little better. Based on your email we’re assuming you have a string that looks something like this:

aaa&bbb&amp;ccc&aamp;ddd

What you’d like to do is convert this string into an array, splitting the string on the ampersand. However – and this is an important however – you don’t want to split on any ampersand that’s followed by the letters amp. In other words, you want to split the string only at the following locations:

aaa&bbb&amp;ccc&aamp;ddd

In the end, that would give you an array with the following elements:

· aaa

· bbb&amp;ccc

· aamp;ddd

It’s crazy, but we think we understand what you want to do. Granted, we’re not totally sure why you want to do it. But, then again, that’s not really any of our business, is it?

Actually, we’ve encountered similar situations before, particularly in Microsoft Word. On more than one occasion we’ve been given documents that have a paragraph return at the end of each line. We need to replace those paragraph returns with blank spaces. However, we can’t just go out and replace every single paragraph return in the document; if we did that then we’d replace the legitimate paragraph returns (that is, the ones between paragraphs) as well. To solve that problem in Microsoft Word we used an approach very similar to the approach we’re going to use today, which is why we considered ourselves at least semi-qualified to answer this question.

Oh, good point: what is the approach we’re going to use today? This:

strText = "aaa&bbb&amp;ccc&aamp;ddd"

strText = Replace(strText, "&amp", "@@@@")

arrText = Split(strText, "&")

For i = 0 to Ubound(arrText) - 1
    arrText(i) = Replace(arrText(i), "@@@@", "&amp")
Next

For Each strItem in arrText
    Wscript.Echo strItem
Next

Trust us; it’s supposed to look like that.

Before the rest of you start accusing us of drinking (as his long-suffering family will attest, the Scripting Guy who writes this column is too much of a cheapskate to be a drinker [yes, those lattes get expensive]) let’s explain what we’re doing here. In the first line of code we take our string value and assign it to a variable named strText. In other words, everything starts out pretty easy. But then it gets a little weird:

strText = Replace(strText, "&amp", "@@@@")

What’s the point of that line of code? Well, as we know, we want to split the line at each and every ampersand … provided, of course, that the ampersand isn’t followed by the letters amp. That means that the value &amp is a problem: it has an ampersand, which means we ought to split the line at that point. However, that ampersand is followed by the letters amp, which means we shouldn’t split the line at that point. Obviously what we need to do is find each ampersand, then check to see if the character is followed by the letters amp.

But, to tell you the truth, that sounded way too hard. (It can be done, but ….) And so we decided to use logic instead. If the value &amp is causing us problems then the logical thing to do is to get rid of that value. That’s why we used VBScript’s Replace function to replace all instances of &amp with @@@@.

Before you ask, no, we didn’t have to use @@@@; we could have used any string of characters that doesn’t appear elsewhere in the text. Our text doesn’t include the string !!!!, which means we could have used that value instead. However, our text does include the value bbb, which means that bbb would be a poor choice as a replacement value. Why? Hopefully that will become clear in just a moment.

So what have we gained by doing this? What we’ve gained is this: the value of strText is now equal to the following, with @@@@ replacing any instances of &amp:

aaa&bbb@@@@;ccc&aamp;ddd

That might not look like that big of a deal, but it is: after all, with the problem value &amp temporarily removed we’re free to split our string on all the remaining ampersands, something we do here:

arrText = Split(strText, "&")

In turn, that gives us an array consisting of the following items:

· aaa&

· bbb@@@@;ccc

· &aamp;ddd

See? That’s pretty good; in fact, if it wasn’t for the value @@@@ stuck in the middle of line 2 it would be perfect. But that’s OK; to paraphrase an age-old parental threat, we brought @@@@ into this world and we can take @@@@ out of this world (or at least out of our array). In order to restore the original text we set up a For Next loop that loops through each item in the array. For each of those items we use the Replace function to replace any instances of @@@@ with – you guessed it -- &amp. That’s what this block of code is for:

For i = 0 to Ubound(arrText) - 1
    arrText(i) = Replace(arrText(i), "@@@@", "&amp")
Next

To make a long story short, early on the value &amp was causing us problems; therefore, we temporarily removed that value from the string. All we’re doing now is restoring &amp to its rightful place. Make sense?

In fact, if you now echo back all the values in the array (something we do in the last block of code) you get back this:

· aaa

· bbb&amp;ccc

· aamp;ddd

Which, believe it or not, is exactly what we wanted to get back.

Whew. You know, we could really use a drink right about now.

Of water. Sheesh.


How Can I Tell Whether a Number is Even or Odd?

Hey, Scripting Guy! How can I tell whether a number is even or odd?

-- JO

Hey, JO. You know, just this morning the Scripting Guy who writes this column was driving in to work when he heard an advertisement for a store that sells HD TVs. “We don’t confuse you by stocking all the different HD TVs available,” said the ad. “No, sir. Instead, we make things easier on everyone by selling only the most popular models.”

Talk about a light going on! After all, each and every day the Scripting Guy who writes this column tries to come up with the best answer to a question, or the easiest answer to a question, or – at the least – a correct answer to a question. To be honest, there are times when that can be hard, very hard. But no more. Instead, from now on we’re going to make things easier on everyone by providing only the most popular answers to any question that gets submitted.

In case you’re wondering, for those of us who work at Microsoft the most popular answers to any question include the following:

· “I don’t know that. Besides, that’s not my job.”

· “That will be in a future release.”

· “You must be doing something wrong; it works fine on my computer.”

· “I have no idea. Why don’t you just use Goog – um, just use MSN Search and look it up?”

And, of course, the fifth most-popular answer to any question asked at Microsoft:

a = 13

intResult = a Mod 2

If intResult = 0 Then
    Wscript.Echo "This is an even number."
ElseIf intResult = 1 Then
    Wscript.Echo "This is an odd number."
Else
    Wscript.Echo "This does not appear to be a whole number."
End If

Hey, what do you know: by astonishing coincidence, the fifth most-popular answer also tells you whether a number is even or odd. The system works!

If you remember anything at all about math (other than your vow to forget everything you ever knew about math as soon as you were done with school) then you probably recall that it’s pretty easy to determine whether a number is even or odd: if a number can be evenly divided by 2 it’s even; if a number can’t be evenly divided by 2 then it’s odd. That’s all there is to it.

Well, OK, that’s all there is to it, provided that you can determine whether or not a number can be evenly divided by 2 in the first place. Or, in this case, provided your script can tell whether a number can be evenly divided by 2. Is that even possible?

Let’s put it this way: in scripting anything is possible. (Well, except for the things that aren’t possible, of course.) To determine whether or not a number can be evenly divided into a second number all you have to do is divide the numbers and see if the answer includes a remainder. For example, 25 cannot be evenly divided by 6: if you take 6 into 25, you’ll end up with 4 and a remainder of 1 (6 x 4 = 24; 24 + 1 = 25). By contrast, 25 can be evenly divided by 5: take 5 into 25 and you’ll get 5, with no remainder. If we can determine the remainder then we’re home free.

And guess what: as it turns out we can easily determine the remainder using VBScript’s Mod function. In fact, that’s the Mod function’s sole reason for being: it performs a division operation and then returns the remainder. For example, this script echoes back the remainder of 25 divided by 6:

Wscript.Echo 25 Mod 6

Run that little one-line script and it will echo back the remainder:

1

See? Just like we said it would.

Once you understand what the Mod operator does our sample script is pretty easy to figure out. We start out by assigning a numeric value (in this case, 13) to a variable named a. We then use this line of code to divide that value by 2, storing the remainder in a variable named intResult:

intResult = a Mod 2

All that’s left now is to take a look at intResult and figure out whether we’ll dealing with an odd number or an even number. If intResult is equal to 0 (no remainder) then we have an even number; if intResult is equal to 1 then we have an odd number. And if intResult is anything else that means that our first value – the one we assigned to the variable a – wasn’t a whole number; it was probably a decimal value like 13.35. (Technically, only whole numbers are considered even or odd.) All that fancy mathematical analysis gets carried out in our final block of code:

If intResult = 0 Then
    Wscript.Echo "This is an even number."
ElseIf intResult = 1 Then
    Wscript.Echo "This is an odd number."
Else
    Wscript.Echo "This does not appear to be a whole number."
End If

There you go, JO; that should do it for today’s question. We have no idea what tomorrow’s question will be, but that doesn’t matter; after all, thanks to our new system we already have tomorrow’s answer:

“No thanks, I already had lunch. But, now that you mention it, I still am a little hungry.”

Number 7 on the list of most popular answers here at Microsoft. (Right behind, “I’d like to, but I need to leave a little early today. My son has a baseball game.”)


How Can I Test a Dynamic Array to See if the Array is Empty?

Hey, Scripting Guy! How can I test a dynamic array to see if the array is empty?

-- SH

Hey, SH. Today’s column is dated Wednesday, July 5th. If there really is a column today that means that, unlike last year, the neighbors and their errant fireworks didn’t set fire to the Scripting House. If there isn’t a new column today, well, to paraphrase Britney Spears: oops, they did it again.

Note. In case you’re wondering, if the neighbors did set the house on fire two years in a row, this Scripting Guy probably wouldn’t say “oops.” Pardon? Well, yes, he probably would say that. And that. And - well, no not that. Not even if someone burned down his house!

As you might expect, last year’s Fourth of July was a bit more eventful than this year’s; things tend to be more memorable when they involve fires roaring through your garage and bedrooms filling up with smoke. It was a bit disconcerting, too; after all, why would anyone want to burn this Scripting Guy’s house down?

Wait, never mind: turns out that we can think of lots of reasons why someone might want to burn his house down. In fact, one or more of his fellow Scripting Guys is likely to bring that up as a topic for discussion at each and every team meeting. In fact, come to think of, SH, that idea might even begin to appeal to you if we don’t quit rambling on and answer your question.

Trust us, one house fire is enough for anyone. With that in mind, here’s a script that can tell you whether or not a dynamic array is empty (that is, whether or not the array has any items in it):

On Error Resume Next

Dim arrTest()
ReDim Preserve arrTest(0)

intUpper = Ubound(arrTest)

If Err = 0 Then
    Wscript.Echo "This array is not empty."
Else
    Wscript.Echo "This array is empty."
    Err.Clear
End If

Dim arrTest2()

intUpper2 = Ubound(arrTest2)

If Err = 0 Then
    Wscript.Echo "This array is not empty."
Else
    Wscript.Echo "This array is empty."
    Err.Clear
End If

Before we begin, we should probably note that there might very well be some other, really cool way of determining whether an array has any items in it or not; however, we didn’t know what that might be. Therefore, we came up with this solution: all we do is try and retrieve the upper bound (i.e., the index number of the last item) in the array. If the call to the Ubound function succeeds, then the array must have at least one item in it. If the call fails, then the array must be empty. It’s not the most sophisticated script ever written, but it works.

Speaking of the script, it starts off with the On Error Resume Next statement. This is important. As we noted, we’re going to check the upper bound of the array in question. If the array is empty, the Ubound function triggers an error; in turn, that will cause the script to come to a screeching halt. That is, it will come to a screeching halt unless we’ve implemented On Error Resume Next.

Did we mention that that was important? It is.

That brings us to these two lines of code:

Dim arrTest()
ReDim Preserve arrTest(0)

All we’re doing here is creating a dynamic array named arrTest; that’s what happens in line 1. In line 2, we then use the ReDim statement to resize arrTest, giving it a size of 1. (Remember, the number of items in an array is always one more than the subscript passed to the Dim or ReDim statements. Thus a command like ReDim Preserve arrTest(0) gives us an array of one item [0 + 1]).

Once we have our one-item array we then call the Ubound function, storing the upper bound in a variable named intUpper:

intUpper = Ubound(arrTest)

We don’t really care what the upper bound is, we’re just interested in knowing if the array has an upper bound. In this case, it should: because arrTest isn’t empty, the Ubound function should work just fine and no error should occur. We can verify that by checking the value of the Err object using this block of code:

If Err <> 0 Then
    Wscript.Echo "This array is empty."
    Err.Clear
Else
    Wscript.Echo "This array is not empty."
End If

If Err equals 0, we echo back the fact that the array is not empty; remember, had the array been empty an error would have occurred and Err would be equal to something other than 0. (Any time Err equals 0 that means no error has occurred.) If Err is equal to something other than 0, that must mean the array is empty. Accordingly, we echo back that fact, then use the Err.Clear method to reset the Err object. That’s all we have to do; just like that we’ve determined whether or not the array is empty.

Note. What’s that? You don’t understand why we need to reset the Err object? For the answer, take a few minutes out from your busy schedule and take a look at Hey, Scripting Guy! The Movie .

Of course, there is a second part to the script, but that section is primarily a repeat of part 1. There is, however, one important exception: this time around we create a dynamic array name arrTest2, but we never add anything to that array. It’s an empty array, and, with any luck, the script should tell us that.

Hope that helps, SH. As for us, we need to run home for a minute and make sure everything is OK. After all, the next Fourth of July is now less than a year away, and we’re still a little gun-shy.

Note. OK, so technically we’re not really worried about the house; we just felt like leaving work and going home. But no one has to know that, right?


How Can I Verify That All the Characters in a String are Either A-to-Z (Uppercase Letters) or the Digits 0-through-9?

Hey, Scripting Guy! In an HTA, how can I verify that all the characters are either A-to-Z (uppercase letters) or the digits 0-through-9?

-- MB

Hey, MB. You want to talk irony?

Oh. Well, we’re going to talk irony anyway. In yesterday’s column we tried to be cute, posing as the illegitimate children of a non-existent defense minister and trying to talk people into helping us smuggle $50 million out of the make-believe country of Freedonia. Ironically, today we could actually use that $50 million, or at least part of it.

That’s because the Scripting Son has decided to play baseball for a local select team next season, a decision that will cost the Scripting Dad $1,800. To his credit, the Scripting Dad remained outwardly calm and stoic as the financial obligations were explained to him. “Tut, tut,” he said. “What’s mere money when it comes to a child’s happiness?”

It was only after they left the building that he asked the Scripting Son to please call 911, and right away.

Note. Which, of course, the Scripting Son declined to do. After all, once the check was signed he had no more use for the Scripting Dad anyway.

So what does all that mean? Well, for one thing, it means the Scripting Dad can’t really afford to get fired in the next year. Which, in turn, means he should give you an answer to your question:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "[^A-Z0-9]"

strSearchString = "Ken Myer"

Set colMatches = objRegEx.Execute(strSearchString)

If colMatches.Count > 0 Then
    Wscript.Echo "The following characters are not allowed:"
    For Each strMatch in colMatches
        Wscript.Echo  strMatch.Value
    Next
End If

What we just showed you is a simple VBScript script for determining whether anything other than an uppercase letter or a digit (0 through 9) appears in a string. We’ll use this simple little script to explain how the code works, then, at the end, we’ll show you an example of how this might be embedded in an HTA.

The script itself starts out by creating an instance of the VBScript.RegExp object; this is the object that allows us to use regular expressions in VBScript. (Note: You say you don’t know what regular expressions are? Then take a look at this on-demand Scripting Guys webcast.) We then configure the following two properties of our regular expressions object:

objRegEx.Global = True
objRegEx.Pattern = "[^A-Z0-9]"

Setting the Global property to True tells the script to match all occurrences of our search pattern. To be honest, we don’t really need to do that here. After all, it probably suffices just to know that one invalid character appears in the search string; you probably don’t need to know about all the invalid characters. But, what the heck: consider this a Scripting Guys bonus. (And if you like that sort of service, well, we do accept tips. Or at least one of us does, as of today anyway.)

Second, we specify the pattern we are searching for. There are actually three pieces to our pattern:

· The square brackets enclosing the search string. That means we’re searching for a set of characters as opposed to a single character.

· The ^ character. This is, officially, the “negation” character. That just means that we’re searching for anything that isn’t part of our character set.

· The character set A-Z0-9. As you can see, there are actually two components to this character set. Our two components are the characters A-Z (uppercase) and the numbers 0-9. If a character in the string is neither an uppercase letter nor a number it will trigger a match.

Ah, good question: why did we jam A-Z and 0-9 together; why didn’t we separate them with a blank space, like so:

[^A-Z 0-9]

Well, we could have. However, by doing so we would have defined a third component for our character set: a blank space. In that case, the script would match anything that wasn’t an uppercase A-Z, a number, or a blank space. Because blank spaces weren’t part of the original criteria, we left them out of the character set.

After defining our search string (the string value Ken Myer) we then call the Execute method and kick off the regular expression search:

Set colMatches = objRegEx.Execute(strSearchString)

After the search is complete, any characters that match the search pattern (again, that’s any character that isn’t an uppercase letter or a number) will be placed in the Matches collection. We can determine whether or not the string contains any invalid characters simply by checking the value of the collection’s Count property; if the Count is greater than 0 that means a match (and, by extension, an invalid character) was found. Here’s the code that checks the value of the Count property:

If colMatches.Count > 0 Then

If the Count is greater then 0 we echo back a message reporting that invalid characters were found; we also echo back which invalid characters were found. That’s what these lines of code do:

Wscript.Echo "The following characters are not allowed:"
For Each strMatch in colMatches
    Wscript.Echo  strMatch.Value
Next

As you can see, we simply loop through all the items in the Matches collection, echoing back the Value of each item. In the case of the string Ken Myer, that results in output like this:

The following characters are not allowed:
e
n

y
e
r

Now, suppose we change our string value to this: KENMYER. Take a look at the output we get in that case:

That’s right, no output at all. That’s because our new string doesn’t have any invalid characters.

As for wrapping this all into an HTA, here’s a very simple example, an HTA that contains a text box for entering data and a button that, when clicked, calls a subroutine that validates all the characters typed into that text box:

<Script language="VBScript">
        Sub VerifyInput
        Set objRegEx = CreateObject("VBScript.RegExp")

        objRegEx.Global = True
        objRegEx.Pattern = "[^A-Z0-9]"

        strSearchString = BasicTextBox.Value

        Set colMatches = objRegEx.Execute(strSearchString)

        If colMatches.Count > 0 Then
            strMessage = "The following characters are not allowed:" & vbCrLf
            For Each strMatch in colMatches
                strMessage = strMessage &  strMatch.Value & vbCrLf
            Next
            Msgbox strMessage
        End If
    End Sub
</Script>

<body>
    <input type="text" name="BasicTextBox" size="50"><p>
    <input id=runbutton  type="button" value="Verify Input" onClick="VerifyInput">
</body>

Hopefully that’s what you had in mind, MB.

Note. If you don’t know what HTAs are, let alone how to build them, take a look at our HTA Developers Zone.

As for next year’s baseball season, well, the Scripting Dad has decided to view this as an investment in the future, After all, several years ago he and the Scripting Son were watching Sports Center about the same time that Edgar Martinez signed a new multi-year contract with the Seattle Mariners. “You know, Dad, someday I’m going to sign a $100 million contract with the Mariners,” said the Scripting Son, who was 9 or 10 at the time, “And when I do, I’m going to give you $10,000.”

True. But hey, $10,000 is $10,000, right?


Can I Change the Command Window Title When Running a Script?

Hey, Scripting Guy! When I run a batch file, I can use the Title command to change the caption of the command window. Can I change the caption of the command window from within a script?

-- AA, Yokohama, Japan

Hey, AA. Well, yes, as long as you’re willing to open up a new command window. If you are, then you can use code similar to this:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run _
    ("%comspec% /K title My Command Window |ping.exe 192.168.1.1"), _
        1, TRUE

Notice what we’ve done here. We’ve created an instance of the Wscript.Shell object, and then we’ve used the Run method to run Ping.exe. But see the big, long command string we used to run the script? What we’ve done is call %comspec% to open a new command window. We used the /K switch to ensure that the window remains open after the script finishes, and we’ve set the title of the window to My Command Window. That’s fairly straightforward.

The tricky part comes when we run Ping; we have to use the pipe separator (the | character) to essentially open a command window and run Ping all in one motion. What we’re saying here is, “Open a command window, set the window title, and run Ping.exe, all at the same time.” Without the piping, the script would open a command window titled My Command Window, but then open a second command window (with the default title) and run Ping in that second window. If you want to see what we’re talking about, try this:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("%comspec% /K title My Command Window "), 1, TRUE
objShell.Run("ping.exe 192.168.1.1"), 1, TRUE

By the way, if you want to get really fancy (or at least as fancy as the command shell allows you to get), you can change the foreground and background colors of the window at the same time you change the title. Dying to have light yellow text on a green background? Then run this script:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run _
    ("%comspec% /T:2E /K title My Command Window|ping.exe " & _
        "192.168.1.1"), 1, TRUE

The secret here is the mysterious little switch /T:2E, which enables you to change the foreground and background colors. For more information, open up a command window and type this:

color /?


How Can I Call the Dir Command?

Hey, Scripting Guy! I’m trying to do something that seems pretty simple: call the dir command from a script. It doesn’t seem to work, though. How come?

-- CR, Mexico City, Mexico

Hey, CR. It appears from your mail that you’re trying to do a simple thing like this:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("dir"), 1, TRUE

However, instead of getting a list of everything in the current folder, you’re getting the message The system cannot find the file specified. What gives?

Well, what gives is this: there really isn’t a file named dir on your computer. Search for dir.exe or dir.com; you won’t find it. Instead, dir is actually a command built into the command shell (cmd.exe or command.exe, depending on the version of Windows you are running). That means dir is available only when you are running the command shell. To demonstrate this, open up a command window, type dir, and press ENTER. You should get a listing of all the files and folders in the current directory. Now, bring up the Run dialog box, type dir, and press ENTER. You should get an error message like this:

But that doesn’t mean you’re out of luck. As it turns out there actually is a way to call the dir command from within a script; you just have to be clever about it. Because dir is a command built into the command shell, what you do is call the command shell and pass dir as a command-line argument. Let’s take a look at a script that does the trick, then we’ll explain how it works:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run("%comspec% /k dir"), 1, TRUE

The first line of the script simply creates an instance of the WSH Shell object, and the second line uses the Run method to actually call the dir command. But note that we don’t specify dir directly; instead we specify %comspec% /k dir. This command string is broken down like this:

Item | Description
%comspec% | Opens up a command window. %comspec% is an environment variable that points to the current command shell. By using %comspec% you don’t have to worry about whether the command shell is cmd.exe or command.exe; %comspec% will pick the correct one.
/k | Ensures that the window stays open after the dir command is called. That’s what the /k is for. If we wanted to make sure that the command window automatically closes after the dir command is called then we would change the /k (keep) to a /c (close).
Dir | Runs the dir command.


How Can I Pass a Drive Letter From a Script to the Command Shell?

Hey, Scripting Guy! How can I pass the drive letter of a drive from a VBScript script to the command shell that called that script?

-- JF

Hey, JF. You know, as kids grow up they pass through a phase where they believe that their parents are capable of doing – and of fixing – anything. Admittedly, children quickly learn the cold, harsh facts, but, at least for a little while, kids believe that parents can do no wrong.

Of course, as a parent that’s incredibly heart-warming; at the same time, however, it places a heck of a lot of pressure on you. After all, you feel obligated to live up to the faith that has been placed in you. As a result, you end up spending a ridiculous amount of time fiddling with duct tape and Super Glue, all in an often-futile attempt to fix something that you know can’t be fixed. But with little Billy or little Betty counting on you, well, what else can you do?

Note. To tell you the truth, we have no idea why little Billy and little Betty are always bringing their broken toys over here. Why don’t they ask their own parents for help?

Here at Hey, Scripting Guy! we think of our readers as being like our own children. Granted, children who never call, children who never remember our birthdays, and children who only come around when they need something from us. But, hey, children nonetheless. And so, as Scripting Guys, we feel obligated to get out the duct tape and Super Glue and see if we can figure out a way to pass the drive letter of a drive from a VBScript script to the command shell.

Before anyone asks, we should mention that JF noted that he can’t write the drive letter to a file or the registry. That took care of our Plan A. And while he didn’t specifically say this, we’re also assuming that he’d like the script to actually work; that eliminated our Plan B. Fortunately, Plan C seems to do the trick:

strDrive = "D:"
strDrive = Left(strDrive,1)
intDrive = Asc(strDrive) - 64

Wscript.Quit(intDrive)

What we have here is a very simple – but semi-clever – way to work around the limitations placed on us. We were aware that you can use Windows Script Host’s Quit method to set the value of the errorlevel environment variable. As far as we knew, however, you could write only numeric values to errorlevel. As it turns out – and for once – we were right about that.

Which meant that we had to find a way to work around that limitation as well. But hey, Scripting Guys – like parents – can do anything, right?

The script itself starts out by assigning an arbitrary drive letter to a variable named strDrive. (We’re assuming that, in a real script, you’d have real code that goes out and determines the drive letter, perhaps WMI code that identifies the drive letter for your CD-ROM drive.) After assigning the value D: to strDrive we then use this line of code to grab just the first letter in that value:

strDrive = Left(strDrive,1)

Why? That’s an easy one: we only need the drive letter; we have no use for the colon that follows the drive letter.

Now, as we noted earlier, we can’t write a string value (like D) to the errorlevel environment variable. Therefore, we use the VBScript Asc function to convert D to its ASCII (numeric) equivalent:

intDrive = Asc(strDrive) - 64

Good question: why do we subtract 64 from the ASCII value? Admittedly, we don’t have to. However, the ASCII value of the letter A is 65; if we subtract 64 from 65 we get 1. That means, in our script, the value of A ends up being 1; coincidentally enough, A also happens to be letter number 1 in the English alphabet. The ASCII value of D is 68. If we subtract 64 from 68 we get 4, and D just happens to be letter number – right; you got it.

From there all we have to do is call the Quit method to terminate the script, making sure to pass our “drive letter” (or at least its numeric equivalent) as the sole parameter to the Quit method:

Wscript.Quit(intDrive)

What does all that gain us? Well, suppose we run our script, and then run the following little batch file:

echo off
echo %errorlevel%

The batch file will echo back the value 4, which happens to be the very same value we derived from our script. Neat, huh? All you have to do now is modify the batch file so that it converts the value 4 back to drive letter D. But that’s something you’ll have to do on your own. After all, we Scripting Guys can’t do everything for you.

And would it hurt you to call once in awhile? And maybe get a haircut? And just how long do you plan to spend at the bowling alley before you start looking for a real job? After all the money it cost to send you to college and get you that fancy degree, you end up …


How Can I Create a New Registry Key?

Hey, Scripting Guy! How can I create a new registry key?

-- DL

Hey, DL. First of all, for those of you who get all your news from the Hey, Scripting Guy! column (as if there was any other source for news and information?) we are happy to report that the University of Washington men’s basketball team defeated a very good Northern Iowa squad 70-61 to win the Basketball Travelers Classic. Admittedly, it wasn’t always pretty: the Huskies are an incredibly young team, occasionally putting 5 freshmen on the floor at the same time. And the officiating was … interesting … to say the least: the refs allowed the players to shake hands before the game started, but that was pretty much the only time any contact was allowed. (We think a couple fouls were even called because a player “violated someone’s personal space.”) But it was fun, and while they are apt to experience growing pains along the way, the Huskies should be a pretty good team come NCAA tournament time.

What’s that? Other news and information? No, as far as we know the Washington-Northern Iowa game was pretty much the only thing of interest going on in the world.

Trivia note. In case you’re wondering, it’s the Northern Iowa Panthers. Of course, the Scripting Guy who writes this column already knew that, just as he knew that it’s the Pepperdine Waves and the Nicholls State Colonels. Depending on how you want to look at it, that’s either very impressive (Delaware Fighting Blue Hens, anyone?) or kind of sad.

Wait, hold everything: we’ve just been handed an important news flash! Sources tell us that this is how you create a new registry key:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Script Center"
objRegistry.CreateKey HKEY_CURRENT_USER, strKeyPath

According to a spokesperson for the Scripting Guys – who asked that his name not be used – the script starts out by defining a constant named HKEY_CURRENT_USER and setting the value to &H80000001; this results in the new registry key being created in the HKEY_CURRENT_USER portion of the registry. What if you wanted to create the new registry key in HKEY_LOCAL_MACHINE? No problem; just change the first line of code to look like this, and replace all instances of HKEY_CURRENT_USER in the script with HKEY_LOCAL_MACHINE:

Const HKEY_LOCAL_MACHINE = &H80000002

After defining the constant we next connect to the WMI service on the local computer. And sure, you can create a registry key on a remote machine; just assign the name of that remote computer to the variable strComputer. Something like this:

strComputer = "atl-fs-01"

One thing to watch out for: make sure you specify \root\default when connecting to the StdRegProv class. In other words, don’t use \root\cimv2, like you do for most WMI scripts. Why not? That’s easy: the StdRegProv class doesn’t live in \root\cimv2; it’s housed in \root\default instead.

Believe it or not, at this point we’re almost done. Our next chore is to assign the registry path for the new registry key. We want to create a key named SOFTWARE\Script Center in HKEY_CURRENT_USER; therefore, we assign SOFTWARE\Script Center to a variable named strKeyPath:

strKeyPath = "SOFTWARE\Script Center"

That’s a good point. It’s not surprising that we can create a key named SOFTWARE\Script Center; after all, the SOFTWARE key already exists. But what if we wanted to create a key named SOFTWARE\Script Center\My Registry Keys\Test Key? Do we need to create a Script Center key, then create a My Registry Keys key, and then, finally, create a Test Key key?

Fortunately, the answer is no. Instead, just assign the entire path to the variable strKeyPath:

strKeyPath = "SOFTWARE\Script Center\My Registry Keys\Test Key"

When you run the script WMI will automatically create any and all the required keys and subkeys.

Speaking of which, to actually create the new registry key we simply call the CreateKey method, passing as parameters the registry hive (HKEY_CURRENT_USER) and the path to the new key (strKeyPath):

objRegistry.CreateKey HKEY_CURRENT_USER, strKeyPath

Just like that, we’ve got ourselves a brand-new registry key.

You know, we’ve got a few minutes here, so why don’t we take our new registry key out for a test spin; after all, there’s no point in having a registry key if you don’t get to do fun things like create new registry values in that key. Here’s a sample script that adds a new DWORD value – B – to our new registry key, assigning My DWORD Value the value 13:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer
& "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Script Center"
strValueName = "My DWORD Value"
dwValue = 13

objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue

We won’t explain this script in any detail today; for more information on working with the registry, see the Microsoft Windows 2000 Scripting Guide. For now, we’ll just note that we can create a new registry value simply by specifying the path to that value and then calling – in this case – the SetDWORDValue method. If the registry value in question already exists the script will modify that value; in this example, that means setting the value to 13. If the registry value doesn’t exist then the script will automatically create the new value and then set it to 13. Could there be anything easier? Maybe, but we haven’t found it yet. (And trust us, we Scripting Guys are always on the lookout for easy things.)

Don’t want to create a DWORD value? No problem; just use one of these methods to create a different type of registry value:

· SetStringValue

· SetExpandedStringValue

· SetMultiStringValue

· SetBinaryValue

Hope that helps, DL. You say you have another question? Doesn’t the University of Washington also have a football team, one that – five weeks into the season – looked like they were finally going to turn things around and return the Huskies to the college football prominence they enjoyed for so long? Sadly, the answer is no: as near as we can tell, the UW no longer has a football team. Thank goodness for basketball, huh?


How Can I Delete a Single Registry Value?

Hey, Scripting Guy! How can I delete a single registry value? I don’t want to delete the entire registry key, just one value.

-- BF

Hey, BF. Registry terminology can be a bit confusing, thanks in large part to those of us here at Microsoft (we tend to use a different set of terms when we talk about the registry than the rest of the world does). So before we answer this question, let’s make sure everyone is clear what we’re talking about. As you can see in the following figure (well, assuming you put this Web page under a microscope; click here if you’d like to see a full-size image), we have a registry key named Test in the left pane and two registry values – Sample Value 1 and Sample Value 2 – in the right pane:

You’d like to delete one of these values (say, Sample Value 1) without deleting the other value and without deleting the entire key.

No problem: WMI has a DeleteValue method designed to do that very thing. If you want to get rid of Sample Value 1, then just use a script like this:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & _
strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Test"
strValueName = "Sample Value 1"

objRegistry.DeleteValue HKEY_CURRENT_USER, strKeyPath, strValueName

As we usually do when dealing with the registry, we begin by defining a constant (in this case HKEY_CURRENT_USER) that will tell our script which portion of the registry we want to work with. We then connect to WMI and the Standard Registry Provider; as usual, note that the StdRegProv class is found in the root\default namespace. Most of the WMI classes you work with for system administration are found in root\cimv2; that’s not the case with the Standard Registry Provider.

Next we define two variables. The first – strKeyPath – represents the path within the registry. We’re dealing with a key – Software\Test – that we made up ourselves; if you want to try this script as-is, you’ll need to create this key and the two sample values. Our second variable – strValueName – represents the name of the registry value that we want to delete.

Finally, we call the DeleteValue method, passing three parameters: the registry hive we’re working with (HKEY_CURRENT_USER); the registry path (strKeyPath); and the actual value to be deleted (strValueName). That’s all we have to do. Run the script and Sample Value 1 will be deleted, but everything else will be left alone.

And seeing as how someone’s bound to ask, what if you did want to delete the entire registry key? Again, no problem: just use the WMI DeleteKey method. Here’s a script that deletes the Software\Test registry key and everything in it:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & _
strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Test"

objRegistry.DeleteKey HKEY_CURRENT_USER, strKeyPath

As you can see, this is even easier than deleting a specific value; all we have to do is let DeleteKey know the registry hive and the registry path, and it takes care of the rest. If only everything in scripting was that easy!

P.S. What’s that? You say you’d like to try this out, but you’re too … busy … to create the sample registry key and values? Okey-doke; here’s a script that’ll do that for you. (And you’re right: we do spoil you guys, don’t we?)

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry=GetObject("winmgmts:\\" & _
strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Test"

objRegistry.CreateKey HKEY_CURRENT_USER, strKeyPath

strValue = ""
strValueName = "Sample Value 1"
objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName,
strValue

dwValue = 0
strValueName = "Sample Value 2"
objRegistry.SetDWORDValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue


How Can I Tell Whether a Value Exists in the Registry?

Hey, Scripting Guy! How can I tell whether a value exists in the registry on a remote computer?

-- AL

Hey, AL. Full disclosure time: even though we go by the name The Scripting Guys, we don’t claim to know everything there is to know about scripting. This question is a good example of that. When we first read it we thought, “Well, obviously there’s some kind of If Exists method in WMI that can do this.” Imagine our surprise when we found out there wasn’t; in fact, we couldn’t find any way to see whether or not a registry value exists. We even – gasp! – resorted to reading the documentation, thinking maybe we had missed something. No such luck.

After failing to find an If Exists method, we thought, “OK, we’ll just try to read the value. Surely trying to read a non-existent value will trigger an error. We can then trap the error, and determine that the value exists.”

As it turns out, if you use WSH’s RegRead method to read a value that doesn’t exist, an error will be triggered. Bu the RegRead method only works on the local computer; you can’t use it to read the registry on a remote computer. For that you need to use WMI. And guess what: if you use WMI to try and read a value that doesn’t exist, no error is generated. Instead, WMI keeps plugging along as if nothing had gone wrong. Crazy.

But then we noticed something. When you use WMI to read from the registry, the value is returned as an “out parameter.” When we checked the actual value of that out parameter, we discovered that any time you try to read a non-existent value, you get back a Null. That means that all we have to do is check for a Null value, and we’ll know whether or not the registry value exists.

Got all that? Here’s a sample script that looks for the registry value HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Test Value:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."
Set objRegistry = GetObject("winmgmts:\\" & _
    strComputer & "\root\default:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion"
strValueName = "Test Value"
objRegistry.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

If IsNull(strValue) Then
    Wscript.Echo "The registry key does not exist."
Else
    Wscript.Echo "The registry key exists."
End If

As you can see, we begin by setting the constant HKEY_LOCAL_MACHINE to &H80000002; we’ll use this value to connect to the HKEY_LOCAL_MACHINE portion of the registry. We then connect to the WMI service, and to the StdRegProv class (which, as you can see, is in the root\default namespace).

After stashing the registry path and the registry value in a pair of variables, we then call the GetStringValue method. Note the four parameters for this method: our constant HKEY_LOCAL_MACHINE; the variable strPath (representing the registry path); the variable strValueName (representing the registry value we’re trying to read); and strValue. strValue is our out-parameter: the actual string value recorded in the registry will be stored in strValue and returned to us.

And that’s how we can tell if registry value exists: if strValue is Null, then the value could not be found. We tried this on a number of registry values, and it seemed to work each time. It also worked on REG_DWORD values, although – of course – you have to use the GetDWORDValue method to read one of those values. (For detailed information about how to work with the registry using WMI, check out the Registry chapter in the Microsoft Windows 2000 Scripting.)

Now, we know what you’re thinking: you’re thinking, “Ok, but what if I create a registry value and then just leave it blank? Won’t the out parameter come back as Null even though the registry value actually exists?” Believe it or not, that doesn’t appear to be the case. Instead, a blank registry value is returned as “empty;” that is, the value is equal to an empty strong (“”). And while we recognize that this can be confusing, in this case nothing (an empty string) is not the same as, well, nothing (a Null value). Empty means that the registry value exists, but is blank; Null means that the registry value doesn’t exist.

Like we said, we tried this out on a random assortment of registry values, and it seemed to work each time. If you run into situations where it doesn’t work, please let us know.


How Can I Write Binary Data to the Registry?

Hey, Scripting Guy! How can I write binary data to the registry?

-- FG

Hey, FG. WMI’s Standard Registry Provider includes a method – SetBinaryData – that makes it relatively easy to write binary data to the registry. There’s one little catch to be aware of, but we’ll let you know what that is.

First, though, let’s take a look at a script that writes the numbers 1 through 10 to a binary registry value named BinaryTest found in HKCU\Software. If the referenced registry value exists then the script will simply replace the existing value with the new value (in this case, the numbers 1 though 10). If the value doesn’t exist, the script will create BinaryTest and then assign it the numbers 1 through 10. In other words, you don’t need to create BinaryTest before trying this sample script; if BinaryTest doesn’t exist, WMI will create it for you.

Here’s the script:

Const HKEY_CURRENT_USER = &H80000001

strComputer = "."

Set objRegistry = GetObject _
("winmgmts:\\" & strComputer &
"\root\default:StdRegProv")

strKeyPath = "Software"
strValueName = "BinaryTest"
arrValues = Array(1,2,3,4,5,6,7,8,9,10)

errReturn = objRegistry.SetBinaryValue _
(HKEY_CURRENT_USER, strKeyPath, strValueName, arrValues)

The script begins by defining a constant named HKEY_CURRENT_USER and assigning it the value &H80000001; this tells the Standard Registry Provider that we want to work with the HKEY_CURRENT_USER portion of the registry. We then connect to the WMI service; note that the Standard Registry Provider is found in the root\default namespace. That’s different from the vast majority of WMI classes used for system administration, most of which are found in root\cimv2.

We assign the key name Software to a variable named strKeyPath; this is the registry path within HKCU. A second variable – strValueName – is assigned the value BinaryTest, which happens to be the registry value we want to change. Finally we use this line of code to assign the numbers 1 through 10 to an array variable we named arrValues; this, of course, represents the binary data we want to write to the registry:

arrValues = Array(1,2,3,4,5,6,7,8,9,10)

Having defined our constant and assigned values to all our variables, all that’s left is to call the SetBinaryValue metod to write these 10 values to the registry:

errReturn = objRegistry.SetBinaryValue _
    (HKEY_CURRENT_USER, strKeyPath, strValueName, arrValues)

So what’s the catch? Well, if you’re familiar with binary values in the registry, you know that numbers are represented by hexadecimal values. You don’t see the number 1 in a binary registry value; you see 01. Likewise you don’t see the number 10 in a binary registry value; you see 0A. The registry works with hexadecimal values, but the SetBinaryValue method requires us to pass regular decimal numbers. That can be a little bit confusing if you’re following instructions for setting a registry value and the instructions say, “Set the value to 0A.” Before you can use the SetBinaryValue method you’ll need to convert 0A to its decimal equivalent. (An easy way to do that? Use Calculator in Scientific mode.)

Other than that, though, setting a binary value in the registry is no different – and no more difficult – than setting a string value or a numeric value.


Registry Script Doesn't Work

Hey, Scripting Guy! I tried writing a script to read data from the registry, but I keep getting this error:

Do you have any idea what's wrong? I'm running Windows XP Professional, Service Pack 1, and this is the code I'm using:

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set oReg=GetObject("winmgmts:\\" &_
    strComputer & "\root\cimV2:StdRegProv")

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon"
strValueName = "UIHost"
oReg.GetExpandedStringValue HKEY_LOCAL_MACHINE,strKeyPath,_
strValueName,strValue
Wscript.Echo "The Windows logon UI host is: " & strValue

-- LW, Omaha, NE

Hey, LW. The error you are getting (80041010) means that the specified class (in this case, root\cimv2:StdRegProv) is invalid; in other words, there's no such class. How could that be? Well, believe it or not, there's a simple reason for this: there really is no such class as root\cimv2:stdRegProv. Instead, the standard registry provider (stdRegprov) is actually found in root\default, not, in root\cimv2. Change your code to look like this, and you should be fine:

Set oReg=GetObject("winmgmts:\\" &_
    strComputer & "\root\default:StdRegProv")

By the way, how did we know that error number 80041010 meant that you referenced an invalid class? Easy: we just looked up the error number in the WMI SDK.


How Can I Incrementally Rename All the Files in a Folder?

Hey, Scripting Guy! I have an unknown number of files in a folder. I need to rename them using this format: ABC_fx.inv, where x is an incremental number. How can I do that?

-- RN

Hey, RN. You know, one of the Scripting Guys once had an idea for a very different kind of restaurant. Would you walk into this restaurant, sit down at a table, and then order from the menu? No way! Instead, you’d walk in and pay a set price (say, $10) and you would never order from the menu. Instead, you’d spin a wheel, and whatever item the wheel landed on is what you’d get.

Now, admittedly, that could be bad: if the wheel lands on a grilled-cheese sandwich, well, then you just spent $10 on a grilled-cheese sandwich. But suppose the wheel lands on steak-and-lobster? Now you’ve gotten steak-and-lobster for just $10; that’s not a bad deal at all. Looking for a unique dining experience? Look no further.

So what does that have to do with incrementally renaming all the files in a folder? Nothing; we just wanted to mention the restaurant idea in case any venture capitalists happen to be reading this column. Guys, you know how to reach us.

You know, that’s a good idea: while we sit here and wait for the venture capital to start rolling in we might as well kill some time by answering RN’s question. OK, RN, here’s a script that will incrementally rename all the files in the folder C:\Data:

strComputer = "."

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

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

i = 1

For Each objFile in colFiles
    strNewName = "C:\Data\ABC_f" & i & ".inv"
    errResult = objFile.Rename(strNewName)
    i = i + 1
Next

And yes, we’d be happy to explain how this script works. (Note to potential investors: when’s the last time you heard about Emeril or Mario Batali taking the time to explain how a system administration script works?) As you can see, we start off by connecting to the WMI service on the local computer. Could we use this same script to rename files on a remote computer? Of course we could; all we have to do is assign the name of the remote computer to the variable strComputer. For example, this line of code causes the script to rename files on the remote computer atl-fs-01:

strComputer = "atl-fs-01"

Fine dining, scripts that connect to remote computers, and plenty of free parking. What more could you ask for?

After connecting to the WMI service we then use this line of code to retrieve a collection of all the files in the folder C:\Data:

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

We agree: it is a weird-looking line of code. But don’t worry too much about that. If you’d like to better understand how Associators Of queries work, then take a look at the WMI SDK. Otherwise, just be content with the knowledge that if you’d like to work with a different folder all you have to do here is replace C:\Data with the path to that folder. Want to work with D:\Accounting\Invoices? OK:

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

As soon as we have a collection of all the files in the folder (and it doesn’t matter how many files there are, this query grabs each and every one) we’re ready to start renaming these babies. To do that, we first assign the value 1 to a counter variable named i:

i = 1

Why do we give this variable the value 1? Well, we want to incrementally name all the files in the folder, with the first file being file 1 (ABC_f1.inv), the second file being file 2, etc. Because we’re using i as a way keep track of which number needs to be assigned to which file we logically need to start off with i being equal to 1.

Note. Would Rachael Ray know that we should set up a counter variable named i, and that we should set the value of i to 1? Well, we’re not saying that she wouldn’t know to do that; we’re just pointing out that she doesn’t talk much about counter variables on her cooking show.

Seems kind of strange to us, too.

Our next step is to set up a For Each loop to loop through all the files in the collection (which, of course, corresponds quite nicely to all the files in the folder C:\Data). Inside that loop we execute these three lines of code:

strNewName = "C:\Data\ABC_f" & i & ".inv"
errResult = objFile.Rename(strNewName)
i = i + 1

In line 1 we’re constructing a new file name (or, to be a bit more accurate, a new file path). To do that we simply combine the following items:

· C:\Data\ABC_f

· The counter variable i (which, the first time through the loop, is equal to 1)

· .inv

Add them altogether and you get this:

C:\Data\ABC_f + 1 + .inv = C:\Data\ABC_f1.inv

As it turns out, C:\Data\ABC_f1.inv is exactly the file path we need to use for the first file in the collection. And note that we do need to use the complete path when using WMI to rename files; the file name alone won’t do us any good.

Once we have the complete path renaming the file is as easy as calling the Rename method and passing this method the file path we just constructed:

errResult = objFile.Rename(strNewName)

So much for file 1. In order to rename file 2 in the collection we increment the value of i by 1 (meaning i is now equal to 2), then loop around and repeat this process with file 2. This continues until we have incrementally renamed each and every file in the folder.

A few quick notes here. First, by default files returned using a WMI query come back in alphabetical order. That means that the file that comes first alphabetically – for example, AAAA.txt – is going to be the first file to be renamed (and thus get the name ABC_f1.inv). Would it be possible to use a different order when it comes to renaming files, such as making the oldest file in the folder ABC_f1.inv? Yes, but not without adding additional code throughout the script. If that’s something someone out there needs to do, let us know.

Second, this script does what it’s supposed to do: it renames all the files in the folder. But suppose you already have a file named ABC_f1.inv. In a case like that, you might want to leave that file alone, and thus give the first file you do rename the moniker ABC_f2.inv. Will this script help you out in that situation? No. But, again, if a script like that would be useful to you just let us know.

Finally, you’re right: it’s highly unlikely that Martha Stewart would ever explain how you could write a script to handle any of these scenarios. But, in all fairness, that might not be Martha’s fault. One of the Scripting Guys actually wanted to ask Martha Stewart to make a guest appearance during the Scripting Week 2 webcasts, but he was told he couldn’t bring in outside guests. For all we know Martha Stewart could explain how to write a script; after all, she seems to know how to do pretty much everything else. She just never got the chance. Like we were told: no “guest stars” on TechNet webcasts.

Note. Of course, this Scripting Guy was also told that no one would go to a restaurant where they spun a wheel and then had to order whatever item the wheel pointed to. We’ll see, both about the new restaurant and about Martha Stewart.