
VBScript solution to Event 7 in the 2008 Winter Scripting Games.
Solutions are also available for Windows PowerShell and Perl.
This event tested your debugging skills. In the process, you needed to learn a little bit about how subroutines work, and a little bit about recursion, one of the more confusing concepts in scripting. Were you able to start with a script that’s completely broken and make it work, without starting from scratch? The script we started with was supposed to do the following:
| • | Find all the text files (files with a .txt file extension) in the C:\Scripts folder and its subfolders that were created more than 10 days ago. |
| • | Copy all those files to the C:\old folder. |
| • | Echo the names of all the files that were copied. |
| • | Echo the total number of files that were copied. |
Let’s start by taking a look at the broken script, then we’ll tell you what needed to be done to fix it:
Set objFSO = CreateObject("FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\scripts")
dtmOld = DateAdd("m", -10, Now)
AllFolders objFolder
CopyTextFiles objFolder
Sub AllFolders Folder
For Each objFolder in Folder.SubFolders
CopyTextFiles subfolder
AllFolders Subfolder
Next
End Sub
Sub CopyTextFiles(subFolder)
colFiles = subFolder.Files
For Each objFile in colFiles
arrSplitName = Split(objFile.Name, ".")
strExtension = arrSplitName(UBound(arrSplitName) - 1)
If strExtension = "txt" and objFile.DateCreated > dtmOld Then
objFSO.CopyFile objFile.Path, "C:\old\"
Wscript.Echo objFile.Name
i = i + 1
Edn If
Next
End
Wscript.Echo
Wscript.Echo "Total Files: " & i
We mentioned in the instructions that we had introduced 10 errors into this script. We’ll give you the list first, then walk through them one-by-one:
1. | Add Set to line 1 in CopyTextFiles |
2. | Change objFolder in AllFolders to subFolder |
3. | Initialize i outside sub |
4. | Remove "- 1" from the UBound function call |
5. | Change "m" to "d" in DateAdd |
6. | Fix typo in End If |
7. | Add Sub to End Sub |
8. | Change > to < |
9. | Add parentheses to AllFolders |
10. | Add "Scripting." to FileSystemObject |
Add Set to Line 1 in CopyTextFiles
The first line in the CopyTextFiles subroutine is this:
colFiles = subFolder.Files
If you look at our very first call to CopyTextFiles, you’ll see that we pass a Folder object in as the parameter:
CopyTextFiles objFolder
This means that within CopyTextFiles, the subFolder variable is a Folder object. In the statement in question we’re using the Folder object to get a collection of File objects. Any time you assign an object, including a collection of objects, to a variable, you need to use the Set statement. Our corrected line looks like this:
Set colFiles = subFolder.Files
Change objFolder in AllFolders to subFolder
Take a look at the For Each loop within our AllFolders subroutine:
For Each objFolder in Folder.SubFolders
CopyTextFiles subfolder
AllFolders Subfolder
Next
This loop assigns each instance within Folder.SubFolders to the variable objFolder. That’s fine, except now look at the two lines within the loop:
CopyTextFiles subfolder
AllFolders Subfolder
These two lines call the subroutines CopyTextFiles and AllFolders. Both of these subroutines take one parameter, and for both we’re passing in the variable subFolder (remember, VBScript is not case-sensitive). Wait a minute, where did subFolder come from? And where are we using objFolder? Well, there’s the mistake. We’re looping through instances of the subfolders so we can do something with them, which means we need to either change objFolder to subFolder or change both instances of subFolder to objFolder. We went with the former:
For Each Subfolder in Folder.SubFolders
CopyTextFiles subfolder
AllFolders Subfolder
Next
Initialize i Outside the CopyTextFiles Subroutine
One of the things we need to do is echo back the number of files that are copied. Inside the CopyTextFiles subroutine, we increment the counter i each time we copy a file. At the very end of the script, when we’re all done, we echo the value of i. And what comes back? This:
Total Files:
That’s right, nothing. The reason for this has to do with the way variables work in subroutines. If you use a variable inside a subroutine, that variable exists only in the subroutine. As soon as you leave the subroutine the variable no longer exists. This is called the “scope” of the variable. Variables can have “global scope” or “local scope.” Global scope means that the variable is declared in the main part of the script, outside any subroutines or functions, and so it can be used anywhere and its value will be the same no matter where it’s referenced in the script. A variable with local scope exists only in the subroutine or function where it’s used. If you try to access that same variable anywhere else, such as another subroutine, it will be considered a new variable and won’t reference the same value.
That’s a very long-winded way of saying that in order for the value of i to stay the same after you leave the subroutine, you have to initialize it outside the subroutine. We went ahead and initialized it at the very beginning of our script:
i = 0
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\scripts")
…
Remove "- 1" from the UBound Function Call
As we read through our list of files for each folder, our broken script runs these lines of code:
arrSplitName = Split(objFile.Name, ".") strExtension = arrSplitName(UBound(arrSplitName) - 1)
In these lines we’re looking for the file extension of each file so we can determine whether it’s a text file (in other words, it has a txt file extension). To find the file extension we call the Split function on the file name, splitting on the dot (.) that separates the name from the extension. That will give us an array (arrSplitName) that contains two elements: the file name and the file extension. Well, we should say it probably give us two elements. It’s possible to have a filename with multiple dots in it, such as Test.File.02.10.2008.txt. This isn’t necessarily recommended, but it’s possible; you never know what you’re going to run into when you’re reading through folders.
If we knew for sure there would be only one dot we could find the file extension by referencing the second array element:
strExtension = arrSplitName(1)
But because there could be more than two elements in the array, we want to make sure we get the last one, which will always be the extension. Therefore, we use the UBound function to return the last element in the array. Because arrays start indexing at 0 we subtract 1 from the results of UBound – after all, we don’t want to index the number of elements in the array, we want to index the last item, which is always the number of elements minus 1.
That logic makes sense, right? The only problem with it is that UBound doesn’t return the number of elements in the array, it returns the index number of the last element in the array. That means we need to remove the – 1 in the equation:
strExtension = arrSplitName(UBound(arrSplitName))
Change "m" to "d" in the Call to DateAdd
The instructions for this event said that we want all files that were created more than 10 days ago. So one of the first things we want to do in the script is find out when 10 days ago was. Thus we put in this line of code:
dtmOld = DateAdd("m", -10, Now)
The problem? This call to DateAdd gets the date 10 months ago, not 10 days ago. How can we tell? Well, the first parameter to DateAdd indicates what portion of the date we’re working with. An m indicates months. If we want to add (or subtract) days, the first parameter needs to be a d:
dtmOld = DateAdd("d", -10, Now)
Fix Typo in End If
This was an easy one; VBScript should have caught this one the minute you tried to run the script. There’s an If statement in the CopyTextFiles subroutine. Every If statement needs to end with an End If. Well, take a look at the End If in the broken script:
Edn If
Oops. Change Edn to End and this will work fine.
Add Sub to End Sub
Here’s another one VBScript should have caught for you. When you declare a subroutine, you specify the end of the subroutine with an End Sub statement. At the end of our CopyTextFiles subroutine we have this:
End
We forgot the Sub. Change this to End Sub and this particular problem is solved.
Change > to <
As we’ve mentioned, and as the event instructions mentioned, this script needs to find all text files with a creation date less than 10 days ago. However, look at our If statement that compares the date the file was created (objFile.DateCreated) to the date 10 days ago (dtmOld):
If strExtension = "txt" and objFile.DateCreated > dtmOld Then
This statement is checking to see if the creation date is greater than 10 days ago, not less than. Change the greater than sign to a less than sign and we’ll get the right set of files:
If strExtension = "txt" and objFile.DateCreated < dtmOld Then
Add Parentheses to the AllFolders Subroutine
In VBScript, any time a function or subroutine is declared, the parameters go inside parentheses. We did this correctly when we defined the CopyTextFiles subroutine:
Sub CopyTextFiles(subFolder)
But we forgot the parentheses when we defined the AllFolders subroutine:
Sub AllFolders Folder
Add parentheses around the parameter and this error goes away:
Sub AllFolders (Folder)
Add "Scripting." to FileSystemObject
Last, but certainly not least, is the line where we retrieve an object reference to the FileSystemObject object:
Set objFSO = CreateObject("FileSystemObject")
Yes, we frequently refer to this as the FileSystemObject, but when you create a reference to it you need to refer to the Scripting.FileSystemObject:
Set objFSO = CreateObject("Scripting.FileSystemObject")
That’s everything. Here’s the completed, working, script:
i = 0
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder("C:\scripts")
dtmOld = DateAdd("d", -10, Now)
AllFolders(objFolder)
CopyTextFiles objFolder
Sub AllFolders(Folder)
For Each Subfolder in Folder.SubFolders
CopyTextFiles subfolder
AllFolders Subfolder
Next
End Sub
Sub CopyTextFiles(subFolder)
Set colFiles = SubFolder.Files
For Each objFile in colFiles
arrSplitName = Split(objFile.Name, ".")
strExtension = arrSplitName(UBound(arrSplitName))
If strExtension = "txt" and objFile.DateCreated < dtmOld Then
objFSO.CopyFile objFile.Path, "C:\old\"
Wscript.Echo objFile.Name
i = i + 1
End If
Next
End Sub
Wscript.Echo
Wscript.Echo "Total Files: " & i