1.5 A Stroll Through PerlWe begin our journey through Perl by taking a little stroll. This stroll presents a number of different features by hacking on a small application. The explanations here are extremely brief; each subject area is discussed in much greater detail later in this book. But this little stroll should give you a quick taste for the language, and you can decide if you really want to finish this book rather than read some more Usenet news or run off to the ski slopes. 1.5.1 The "Hello, World" ProgramLet's look at a little program that actually does something. Here is your basic "Hello, world" program: #!/usr/bin/perl -w print ("Hello, world!\n"); The first line is the incantation that says this is a Perl program. It's also a comment for Perl; remember that a comment is anything from a pound sign to the end of that line, as in many interpreter programming languages. Unlike all other comments in the program, the one on the first line is special: Perl looks at that line for any optional arguments. In this case, the -w switch was used. This very important switch tells Perl to produce extra warning messages about potentially dangerous constructs. You should always develop your programs under -w .
The second line is the entire executable part of this program. Here we see a
When you invoke this program, the kernel fires up a Perl interpreter, which parses the entire program (all two lines of it, counting the first, comment line) and then executes the compiled form. The first and only operation is the execution of the
Soon you'll see Perl programs where 1.5.2 Asking Questions and Remembering the Result
Let's add a bit more sophistication. The
One kind of place to hold values (like a name) is a
scalar variable
. For this program, we'll use the scalar variable
The program needs to ask for the name. To do that, we need a way to
prompt and a way to accept input. The previous program showed us how to prompt: use the print "What is your name? "; $name = <STDIN>;
The value of chomp ($name);
Now all we need to do is say print "Hello, $name!\n"; As with the shell, if we want a dollar sign rather than a scalar variable reference, we can precede the dollar sign with a backslash. Putting it all together, we get: #!/usr/bin/perl -w print "What is your name? "; $name = <STDIN>; chomp ($name); print "Hello, $name!\n"; 1.5.3 Adding Choices
Now, let's say we have a special greeting for Randal, but want an ordinary greeting for anyone else. To do this, we need to compare the name that was entered with the string #!/usr/bin/perl print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name eq "Randal") { print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting }
The
The 1.5.4 Guessing the Secret WordWell, now that we have the name, let's have the person running the program guess a secret word. For everyone except Randal, we'll have the program repeatedly ask for guesses until the person guesses properly. First the program, and then an explanation: #!/usr/bin/perl -w $secretword = "llama"; # the secret word print "What is your name? "; $name = <STDIN>; chomp $name; if ($name eq "Randal") { print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while ($guess ne $secretword) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } }
First, we define the secret word by putting it into another scalar variable, Of course, this is not a very secure program, because anyone who is tired of guessing can merely interrupt the program and get back to the prompt, or even look at the source to determine the word. But, we weren't trying to write a security system, just an example for this section. 1.5.5 More than One Secret WordLet's see how we can modify this to allow more than one valid secret word. Using what we've already seen, we could compare the guess repeatedly against a series of good answers stored in separate scalar variables. However, such a list would be hard to modify or read in from a file or compute based on the day of the week.
A better solution is to store all possible answers in a data structure called a
list
, or (preferably) an
array
. Each
element
of the array is a separate scalar variable that can be independently set or accessed. The entire array can also be given a value in one fell swoop. We can
assign a value to the entire array named @words = ("camel","llama","alpaca");
Array variable names begin with
@words = qw(camel llama alpaca);
These mean exactly the same thing; the
Once the array is assigned, we can
access each element using a
subscript reference. So #!/usr/bin/perl -w @words = qw(camel llama alpaca); print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name eq "Randal") { print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); $i = 0; # try this word first $correct = "maybe"; # is the guess correct or not? while ($correct eq "maybe") { # keep checking til we know if ($words[$i] eq $guess) { # right? $correct = "yes"; # yes! } elsif ($i < 2) { # more words to look at? $i = $i + 1; # look at the next word next time } else { # no more words, must be bad print "Wrong, try again. What is the secret word?"; $guess = <STDIN>; chomp ($guess); $i = 0; # start checking at the first word again } } # end of while not correct } # end of "not Randal"
You'll notice we're using the scalar variable
This program also shows the
1.5.6 Giving Each Person a Different Secret WordIn the previous program, any person who comes along could guess any of the three words and be successful. If we want the secret word to be different for each person, we'll need a table that matches up people with words:
Notice that both Betty and Wilma have the same secret word. This is fine.
The easiest way to store such a table in Perl is with a
hash
. Each element of the hash holds a separate scalar value (just like the other type of array), but the hashes are referenced by a
key
, which can be any scalar value (any string or number, including noninteger and negative values). To create a hash called %words = qw( fred camel barney llama betty alpaca wilma alpaca ); Each pair of values in the list represents one key and its corresponding value in the hash. Note that we broke this assignment over many lines without any sort of line-continuation character, because whitespace is generally insignificant in a Perl program.
To find the secret word for Betty, we need to use Betty as the key in a reference to the hash Putting all this together, we get a program like this: #!/usr/bin/perl %words = qw( fred camel barney llama betty alpaca wilma alpaca ); print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name eq "Randal") { print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting $secretword = $words{$name}; # get the secret word print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while ($guess ne $secretword) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } }
Note the lookup of the secret word. If the name is not found, the value of [... rest of program deleted ...] $secretword = $words{$name}; # get the secret word if ($secretword eq "") { # oops, not found $secretword = "groucho"; # sure, why a duck? } print "What is the secret word? "; [... rest of program deleted ...]
1.5.7 Handling Varying Input Formats
If I enter
Suppose I wanted to look for any string that began with if ($name =~ /^Randal/) { ## yes, it matches } else { ## no, it doesn't } Note that the regular expression is delimited by slashes. Within the slashes, spaces and other whitespace are significant, just as they are within strings.
This almost does it, but it doesn't handle selecting When put together with the rest of the program, it looks like this: #!/usr/bin/perl %words = qw( fred camel barney llama betty alpaca wilma alpaca ); print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name =~ /^randal\b/i) { print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting $secretword = $words{$name}; # get the secret word if ($secretword eq "") { # oops, not found $secretword = "groucho"; # sure, why a duck? } print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while ($guess ne $secretword) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } }
As you can see, the program is a far cry from the simple Perl provides every regular expression feature found in every standard UNIX utility (and even some nonstandard ones). Not only that, but the way Perl handles string matching is about the fastest on the planet, so you don't lose performance. (A grep -like program written in Perl often beats the vendor-supplied[ 6 ] C-coded grep for most inputs. This means that grep doesn't even do its one thing very well.)
1.5.8 Making It Fair for the Rest
So, now I can enter To be fair to Barney, we need to grab the first word of whatever's entered, and then convert it to lowercase before we look up the name in the table. We do this with two operators: the substitute operator, which finds a regular expression and replaces it with a string, and the translate operator, to put the string in lowercase.
First, the substitute operator: we want to take the contents of $name =~ s/\W.*//;
We're using the same
Now, to get whatever's left into lowercase, we translate the string using the
$name =~ tr/A-Z/a-z/;
The slashes delimit the searched-for and replacement character lists. The dash between
Putting that together with everything else results in: #!/usr/bin/perl %words = qw( fred camel barney llama betty alpaca wilma alpaca ); print "What is your name? "; $name = <STDIN>; chomp ($name); $original_name = $name; #save for greeting $name =~ s/\W.*//; # get rid of everything after first word $name =~ tr/A-Z/a-z/; # lowercase everything if ($name eq "randal") { # ok to compare this way now print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $original_name!\n"; # ordinary greeting $secretword = $words{$name}; # get the secret word if ($secretword eq "") { # oops, not found $secretword = "groucho"; # sure, why a duck? } print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while ($guess ne $secretword) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } }
Notice how the regular expression match for With just a few statements, we've made the program much more user-friendly. You'll find that expressing complicated string manipulation with a few keystrokes is one of Perl's many strong points.
However, hacking away at the name so that we could compare it and look it up in the table destroyed the name that was entered. So, before the program hacks on the name, it saves it in Perl has many ways to monitor and mangle strings. You'll find out about most of them in Chapter 7, Regular Expressions , and Chapter 15, Other Data Transformation . 1.5.9 Making It a Bit More ModularNow that we've added so much to the code, we have to scan through many detailed lines before we can get the overall flow of the program. What we need is to separate the high-level logic (asking for a name, looping based on entered secret words) from the details (comparing a secret word to a known good word). We might do this for clarity, or maybe because one person is writing the high-level part and another is writing (or has already written) the detailed parts. Perl provides subroutines that have parameters and return values . A subroutine is defined once in a program, and can be used repeatedly by being invoked from within any expression.
For our small-but-rapidly-growing program, let's create a subroutine called sub good_word { my($somename,$someguess) = @_; # name the parameters $somename =~ s/\W.*//; # get rid of everything after first word $somename =~ tr/A-Z/a-z/; # lowercase everything if ($somename eq "randal") { # should not need to guess return 1; # return value is true } elsif (($words{$somename} || "groucho") eq $someguess) { return 1; # return value is true } else { return 0; # return value is false } }
First, the definition of a subroutine consists of the reserved word
The first line within this particular definition is an assignment that copies the values of the two parameters of this subroutine into two local variables named The next two lines clean up the name, just like the previous version of the program.
The A return statement can be used to make the subroutine immediately return to its caller with the supplied value. In the absence of an explicit return statement, the last expression evaluated in a subroutine is the return value. We'll see how the return value is used after we finish describing the subroutine definition.
The test for the ($words{$somename} || "groucho") eq $someguess
The first thing inside the parentheses is our familiar hash lookup, yielding some value from
In any case, whether it's a value from the hash, or the default value
So, expressed as a rule, if the name is Now let's integrate all this with the rest of the program: #!/usr/bin/perl %words = qw( fred camel barney llama betty alpaca wilma alpaca ); print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name =~ /^randal\b/i) { # back to the other way :-) print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while (! good_word($name,$guess)) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } } [... insert definition of good_word() here ...]
Notice that we've gone back to the
regular expression to check for
The big difference is the
The value returned by the subroutine (either 1 or 0, recalling the definition given earlier) is logically inverted with the prefix
Note that the subroutine assumes that the value of the
Such a cavalier approach to global variables doesn't scale very well, of course. Generally speaking, variables not created with 1.5.10 Moving the Secret Word List into a Separate File
Suppose we wanted to share the secret word list among three programs. If we store the word list as we have done already, we will need to change all three programs when Betty decides that her secret word should be
So, let's put the word list into a file and then read the file to get the word list into the program. To do this, we need to create an I/O channel called a
filehandle
. Your Perl program automatically gets three filehandles called
Here's a small chunk of code to do that: sub init_words { open (WORDSLIST, "wordslist"); while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); $words{$name} = $word; } close (WORDSLIST); } We're putting it into a subroutine so that we can keep the main part of the program uncluttered. This also means that at a later time (hint: a few revisions down in this stroll), we can change where the word list is stored, or even the format of the list. The arbitrarily chosen format of the word list is one item per line, with names and words, alternating. So, for our current database, we'd have something like this: fred camel barney llama betty alpaca wilma alpaca
The
The
If you were running with
-w
, you would have to check that the return value read in was actually defined. The empty string returned by the while ( defined ($name = <WORDLIST>) ) {
But if you were being that careful, you'd probably also have checked to make sure that
On the other hand, the normal case is that we've read a line (including the newline) into
The final line of the
Once the file has been read, the filehandle can be recycled with the
This subroutine definition can go after or before the other one. And we invoke the subroutine instead of setting #!/usr/bin/perl init_words(); print "What is your name? "; $name = <STDIN>; chomp $name; if ($name =~ /^randal\b/i) { # back to the other way :-) print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while (! good_word($name,$guess)) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess); } } ## subroutines from here down sub init_words { open (WORDSLIST, "wordslist") || die "can't open wordlist: $!"; while ( defined ($name = <WORDSLIST>)) { chomp ($name); $word = <WORDSLIST>; chomp $word; $words{$name} = $word; } close (WORDSLIST) || die "couldn't close wordlist: $!"; } sub good_word { my($somename,$someguess) = @_; # name the parameters $somename =~ s/\W.*//; # delete everything after # first word $somename =~ tr/A-Z/a-z/; # lowercase everything if ($somename eq "randal") { # should not need to guess return 1; # return value is true } elsif (($words{$somename} || "groucho") eq $someguess) { return 1; # return value is true } else { return 0; # return value is false } }
Now it's starting to look like a full grown program. Notice the first executable line is an invocation of
The 1.5.11 Ensuring a Modest Amount of Security"That secret word list has got to change at least once a week!" cries the Chief Director of Secret Word Lists. Well, we can't force the list to be different, but we can at least issue a warning if the secret word list has not been modified in more than a week.
The best place to do this is in the sub init_words { open (WORDSLIST, "wordslist") || die "can't open wordlist: $!"; if (-M WORDSLIST >= 7.0) { # comply with bureaucratic policy die "Sorry, the wordslist is older than seven days."; } while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); $words{$name} = $word; } close (WORDSLIST) || die "couldn't close wordlist: $!"; }
The value of The rest of the program remains unchanged, so in the interest of saving a few trees, I won't repeat it here. Besides getting the age of a file, we can also find out its owner, size, access time, and everything else that the system maintains about a file. More on that in Chapter 10 . 1.5.12 Warning Someone When Things Go Astray
Let's see how much we can bog down the system by sending a piece of email each time someone guesses their secret word incorrectly. We need to modify only the The mail will be sent to you if you type your own mail address where the code says "YOUR_ADDRESS_HERE." Here's what we have to do: just before we return 0 from the subroutine, we create a filehandle that is actually a process ( mail ), like so: sub good_word { my($somename,$someguess) = @_; # name the parameters $somename =~ s/\W.*//; # get rid of stuff after # first word $somename =~ tr/A-Z/a-z/; # lowercase everything if ($somename eq "randal") { # should not need to guess return 1; # return value is true } elsif (($words{$somename}||"groucho") eq $someguess) { return 1; # return value is true } else { open MAIL,"|mail YOUR_ADDRESS_HERE"; print MAIL "bad news: $somename guessed $someguess\n"; close MAIL; return 0; # return value is false } }
The first new statement here is
The next statement, a
Finally, we close the filehandle, which starts mail sending its data merrily on its way. To be proper, we could have sent the correct response as well as the error response, but then someone reading over my shoulder (or lurking in the mail system) while I'm reading my mail might get too much useful information. Perl can also open filehandles, invoke commands with precise control over argument lists, or even fork off a copy of the current program, and execute two (or more) copies in parallel. Backquotes (like the shell's backquotes) give an easy way to grab the output of a command as data. All of this gets described in Chapter 14, Process Management , so keep reading. 1.5.13 Many Secret Word Files in the Current Directory
Let's change the definition of the secret word filename slightly. Instead of just the file named echo *.secret to get a brief listing of all of these names. As you'll see in a moment, Perl uses a similar wildcard-name syntax.
Pulling out the sub init_words { while ( defined($filename = glob("*.secret")) ) { open (WORDSLIST, $filename) || die "can't open wordlist: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp $name; $word = <WORDSLIST>; chomp $word; $words{$name} = $word; } } close (WORDSLIST) || die "couldn't close wordlist: $!"; } }
First, we've wrapped a new
So if the current directory contains
Within the
Note that if there are no files that match 1.5.14 Listing the Secret WordsWell, the Chief Director of Secret Word Lists wants a report of all the secret words currently in use and how old they are. If we set aside the secret word program for a moment, we'll have time to write a reporting program for the Director.
First, let's get all of the secret words, by stealing some code from the while ( defined($filename = glob("*.secret")) ) { open (WORDSLIST, $filename) || die "can't open wordlist: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); ### new stuff will go here } } close (WORDSLIST) || die "couldn't close wordlist: $!"; }
At the point marked "new stuff will go here," we know three things: the name of the file (in format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<< @<<<<<<<<<<< $filename, $name, $word .
The format definition begins with
We invoke this format with the
#!/usr/bin/perl while ( defined($filename = glob("*.secret")) ) { open (WORDSLIST, $filename) || die "can't open wordlist: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); write; # invoke format STDOUT to STDOUT } } close (WORDSLIST) || die "couldn't close wordlist: $!"; } format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<< @<<<<<<<<<<< $filename, $name, $word .
When the format is invoked, Perl evaluates the field expressions and generates a line that it sends to the Hmm. We haven't labeled the columns. That's easy enough. We just need to add a top-of-page format, like so: format STDOUT_TOP = Page @<< $% Filename Name Word ================ ========== ============ .
This format is named
The first line of this format shows some constant text (
The third line of the format is blank. Because this line does not contain any fields, the line following it is not a field value line. This blank line is copied directly to the output, creating a blank line between the page number and the column headers below. The last two lines of the format also contain no fields, so they are copied as is directly to the output. So this format generates four lines, one of which has a part that changes from page to page. Just tack this definition onto the previous program to get it to work. Perl notices the top-of-page format automatically. Perl also has fields that are centered or right-justified, and supports a filled paragraph area as well. More on this when we get to formats in Chapter 11, Formats . 1.5.15 Making Those Old Word Lists More Noticeable
As we are scanning through the
Here's how the sub init_words { while ( defined($filename = glob("*.secret")) ) { open (WORDSLIST, $filename) || die "can't open wordlist: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); $words{$name} = $word; } } else { # rename the file so it gets noticed rename ($filename,"$filename.old") || die "can't rename $filename to $filename.old: $!"; } close (WORDSLIST) || die "couldn't close wordlist: $!"; } }
Notice the new Perl has a complete range of file manipulation operators; anything you can do to a file from a C program, you can also do from Perl. 1.5.16 Maintaining a Last-Good-Guess DatabaseLet's keep track of when the most recent correct guess has been made for each user. One data structure that might seem to work at first glance is a hash. For example, the statement $last_good{$name} = time;
assigns the current time in internal format (some large integer above 800 million, incrementing one number per second) to an element of But, the hash doesn't have an existence between invocations of the program. Each time the program is invoked, a new hash is formed. So at most, we create a one-element hash and then immediately forget it when the program exits.
The
dbmopen (%last_good,"lastdb",0666) || die "can't dbmopen lastdb: $!"; $last_good{$name} = time; dbmclose (%last_good) || die "can't dbmclose lastdb: $!";
The first statement performs the mapping, using the disk filenames of
The second statement shows that we use this mapped hash just like a normal hash. However, creating or updating an element of the hash automatically updates the disk files that form the DBM. And, when the hash is later accessed, the values within the hash come directly from the disk image. This gives the hash a life beyond the current invocation of the program - a persistence of its own.
The third statement disconnects the hash from the DBM, much like a file Although the inserted statements maintain the database just fine (and even create it the first time), we don't have any way of examining the information yet. To do that, we can create a separate little program that looks something like this: #!/usr/bin/perl -w dbmopen (%last_good,"lastdb",0666) || die "can't dbmopen lastdb: $!"; foreach $name (sort keys (%last_good)) { $when = $last_good{$name}; $hours = (time() - $when) / 3600; # compute hours ago write; } format STDOUT = User @<<<<<<<<<<<: last correct guess was @<<< hours ago. $name, $hours .
We've got a few new operations here: a
First, the
The
Finally, the Perl
The body of the Perl also provides easy ways to create and maintain text-oriented databases (like the Password file) and fixed-length-record databases (like the "last login" database maintained by the login program). These are described in Chapter 17, User Database Manipulation . 1.5.17 The Final ProgramsHere are the programs from this stroll in their final form so you can play with them. First, the "say hello" program: #!/usr/bin/perl init_words(); print "what is your name? "; $name = <STDIN>; chomp($name); if ($name =~ /^randal\b/i) { # back to the other way :-) print "Hello, Randal! How good of you to be here!\n"; } else { print "Hello, $name!\n"; # ordinary greeting print "What is the secret word? "; $guess = <STDIN>; chomp $guess; while (! good_word($name,$guess)) { print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp $guess; } } dbmopen (%last_good,"lastdb",0666); $last_good{$name} = time; dbmclose (%last_good); sub init_words { while ($filename = <*.secret>) { open (WORDSLIST, $filename)|| die "can't open $filename: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); $words{$name} = $word; } } else { # rename the file so it gets noticed rename ($filename,"$filename.old") || die "can't rename $filename: $!"; } close WORDSLIST; } } sub good_word { my($somename,$someguess) = @_; # name the parameters $somename =~ s/\W.*//; # delete everything after first word $somename =~ tr/A-Z/a-z/; # lowercase everything if ($somename eq "randal") { # should not need to guess return 1; # return value is true } elsif (($words{$somename} || "groucho") eq $someguess) { return 1; # return value is true } else { open (MAIL, "|mail YOUR_ADDRESS_HERE"); print MAIL "bad news: $somename guessed $someguess\n"; close MAIL; return 0; # return value is false } } Next, we have the secret word lister: #!/usr/bin/perl while ($filename = <*.secret>) { open (WORDSLIST, $filename) || die "can't open $filename: $!"; if (-M WORDSLIST < 7.0) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp ($word); write; # invoke format STDOUT to STDOUT } } close (WORDSLIST); } format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<< @<<<<<<<<<<< $filename, $name, $word . format STDOUT_TOP = Page @<< $% Filename Name Word ================ ========== ============ . And finally, the last-time-a-word-was-used display program: #!/usr/bin/perl dbmopen (%last_good,"lastdb",0666); foreach $name (sort keys %last_good) { $when = $last_good{$name}; $hours = ( time - $when) / 3600; # compute hours ago write; } format STDOUT = User @<<<<<<<<<<<: last correct guess was @<<< hours ago. $name, $hours .
Together with the secret word lists (files named | ||||||||||
|