
Perl solution to Event 7 in the 2008 Winter Scripting Games.
Solutions are also available for VBScript and Windows PowerShell.
This event tested your debugging skills. 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 all its subfolders. |
| • | 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:
use File::Copy;
File::Find;
File::Find::find({wanted => \&wanted}, 'C:\Scripts');
print "\nTotal Files: " . $count;
exit;
sub wanted (
my $count = 0;
/\.txt$/ or return;
my $filename = $_;
print "$filename \n";
my $newfile = "C:\\old\\" . "$_";
copy($File::Find::name, $newfile) or die "Copy failed: $!";
$count = $cout + 1
)
We mentioned in the instructions that we had introduced 5 errors into this script. We’ll give you the list first, then walk through them one-by-one:
1. | Add "use" to File::Find |
2. | Replace () in the Subroutine with {} |
3. | Add ; to the End of the Last Line in the Subroutine |
4. | Change $cout to $count |
5. | Move $count Declaration Outside of the Subroutine |
Add "use" to File::Find
Let’s first take a look at the very first line in our bug-ridden script:
use File::Copy;
Notice in the first line we have the keyword use followed by File::Copy. File::Copy is a module containing classes, methods and properties that we’ll use to copy the files. A lot of functionality used in Perl isn’t included in the language itself, it’s included in modules such as this. You must include a use statement to be able to use the functionality provided by these modules. Now let’s look at the second line of our script:
File::Find;
We can tell by the double colons (::) that this in another module name we need to include. What’s wrong with this line? The first line gave us the clue: we need to add a use statement:
use File::Find;
Replace () in the Subroutine with {}
Next we have a basic syntax error. We’ve defined a subroutine named wanted. We’re going to call this subroutine as part of our find statement when we search for text files. Let’s ignore what the subroutine is doing for a moment and simply look at the definition itself:
sub wanted (
…
)
When you declare a function in Perl, the code within that function must be enclosed in curly braces {}, not parentheses. That’s a pretty simple change:
sub wanted {
…
}
Add ; to the End of the Last Line in the Subroutine
We’ll admit that this script was a little complicated for the Beginner division, which is why we didn’t actually ask you to write a script like this, we merely asked you to debug it instead. True, sometimes debugging someone else’s script can be harder than writing your own from scratch, but in this case we restricted the errors to the parts of the script that are easiest to understand. Which brings us to this next error. Yes, this is another syntax error; Perl will flag this one when you try to run the script. Look at the very last line in the wanted subroutine:
$count = $cout + 1
Notice anything missing? This is probably one of the most common mistakes you can make in Perl until you get used to it. Every executable line in Perl must end with a semicolon (;). Keep in mind that doesn’t include lines that identify beginning and ending of script blocks, such as the subroutine declaration we just looked at and if statements like this:
if ($a = 4)
{…}
Notice that there are no semicolons on those lines, only lines in the main body of the script and inside such script blocks need semicolons. That might sound a little confusing, but it’s pretty straightforward once you start working with Perl on a regular basis. So what should that line look like? Add a semicolon to the end and it looks like this:
$count = $cout + 1;
Change $cout to $count
The next error occurs on the same line, but at first glance it might not be quite as obvious. Take a look:
$count = $cout + 1;
Unlike the syntax errors we just looked at, your script will run just fine with this line of code in it. It just won’t give you the results you’re looking for. Here’s why:
One of the things we had to do in this script was keep track of the number of files we copied. We did this using a counter variable named $count. Each time we copied a file we added 1 to $count. Or did we? If you take another look at that line, you’ll see that we’re not incrementing $count by adding 1 to the existing value, we’re adding 1 to the variable $cout (notice there’s no “n” in the name). This variable is never even initialized or used anywhere, which means each time through the loop we’ll be adding 1 to nothing and assigning that value to $count. And that means that every time through the loop, no matter how many times we loop and how many files we copy, $count will continually be set to 1. Not exactly what we wanted. But as it turns out, this is a simple typo that’s easily fixed by adding an n:
$count = $count + 1;
Note: In this case we actually could have avoided this situation altogether by using Perl’s shorthand syntax for incrementing a variable by 1: $count++; |
Move $count Declaration Outside of the Subroutine
This last problem was probably the most difficult to track down. As we just mentioned, we’re using the $count variable to keep track of the number of files we copy. We’re keeping track of this variable inside the subroutine. Once we leave the subroutine, we run this line of code to display the number of files copied:
print "\nTotal Files: " . $count;
This looks right. We’re simply printing a newline character (\n) followed by the text “Total Files: “ followed by the value in the $count variable. But (assuming we’ve made all the preceding corrections to the script) this is what we get back:
Total Files:
Let’s see, we spelled $count correctly this time…what else could be the problem? As it turns out, the problem lies inside the subroutine, at the very first line:
my $count = 0;
When we declare a variable like this inside a subroutine, that variable is available only as long as we’re in the subroutine. This is what’s called a “local” variable. As soon as we leave the subroutine that variable disappears. So the $count variable in our print statement, which is in the main body of the script, is not the same as the $count variable in our subroutine.
The easiest way to fix this is to make $count a “global” variable, meaning the variable will keep its definition and its value throughout the entire script. We make a variable global by declaring it in the main body of the script. That means all we have to do is move this line out of the subroutine and place it towards the beginning of the script, like this:
use File::Copy; use File::Find; my $count = 0; …
Here, in its entirety, is the final, working script:
use File::Copy;
use File::Find;
my $count = 0;
File::Find::find({wanted => \&wanted}, 'C:\Scripts');
print "\nTotal Files: " . $count;
# exit;
sub wanted {
/\.txt$/ or return;
my $filename = $_;
print "$filename \n";
my $newfile = "C:\\old\\" . "$_";
copy($File::Find::name, $newfile) or die "Copy failed: $!";
$count = $count + 1;
}