Copyright Notice
This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.This text has appeared in an edited form in SysAdmin/PerformanceComputing/UnixReview magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.
Please read all the information in the table of contents before using this article.
Unix Review Column 21 (Aug 1998)
Most modern algorithmic languages have some various ways of doing ``decision'' and ``iteration'', or they'd be rather useless (or useful only for trivial programs). Perl has many different ways of choosing and evaluating loops. Let's look at some of them.
The basic element for choice is the if statement:
if ($somecondition) { true_branch; true_branch; } else { false_branch; false_branch; }
along with its cousin, the elsif
clause:
if ($cond_1) { true_branch_for_1; true_branch_for_1; } elsif ($cond_2) { true_branch_for_2; true_branch_for_2; } elsif ($cond_3) { true_branch_for_3; true_branch_for_3; } else { all_false_branch; all_false_branch; }
Each of the conditions here are executed in turn, and the first one that results in true causes its corresponding branch to be executed, skipping all the others. A handy construct.
But there's also some other ways of getting at the same thing. Many
of these are listed in ``man perlsyn'' (or ``perldoc perlsyn'' if you
don't have the manpages properly installed). For example, another
if
statement might be written as:
$cond && do { true_branch; true_branch; };
Or, we might even flip it around with a ``backwards-if'', as in:
do { true_branch; true_branch; } if $cond;
Personally, I don't use that one often. It seems to be too backwards for me. However, its much-simpler form of:
$action_expression if $cond;
is something I use frequently. For example:
print "a is $a, b is $b\n" if $debug;
Here, I'd be setting $debug at the beginning of the program, and all of these statements spring to life.
To get some more choices for choice, let's first look at the building
blocks of iteration. The basic while
loop gets us repeated executions
of a block until a condition fails:
while ($cond) { body; body; }
We can break out of this loop early with last
, as in:
while ($cond) { body; last if $cond_2; body; }
Here, a backwards-if breaks us out of the loop when $cond_2 is true.
Because last
counts as an expression, we can use it in comma-expressions
to do multiple things:
while ($cond) { body; ($a = "something"), ($b = "something else"), last if $cond_2; body; }
Once again, I prefer keeping the left side of a backwards-if rather simple, so I use something like this sparingly. But consider the somewhat noisier alternative:
while ($cond) { body; if ($cond_2) { $a = "something"; $b = "something else"; last; } body; }
which works, but takes up a few extra lines visually. There's
also the next
operator, bouncing forward when we see something we
don't want to process:
foreach $item (split) { body_1; next if $item =~ /^a/; body_2; }
Here, the items that begin with ``a'' are processed with body_1
, but
not body_2
. The next
operator automatically respects a continue
block, which can be handy sometimes:
foreach $item (split) { body_1; next if $item =~ /^a/; body_2; next if $item =~ /z$/; body_3; } continue { body_4; }
Here, items that start with ``a'' do only body_1
and body_4
, while
those that end in ``z'' do body_1
, body_2
, and body_4
, and
everything else does all 4 body steps. Pretty flexible. A pratical
use for this is getting the $. variable correct while processing the
<> filereading. You need to close ARGV
when you've seen
end of file, but only after you've done all the processing on that
line. So the simplest assured loop looks like:
while (<>) { next unless /perl/i; print "$ARGV:$.:$_"; } continue { close(ARGV) if eof; }
By having the close
be inside the continue
, we are guaranteed to
execute it even if we've done a next
.
Then there's the redo
operator, which restarts the current
iteration. Here's a nice little practical example for redo
--
handling continuation lines in a configuration file:
while (<>) { s/#.*//; next unless /\S/; if (s/\s*\\\s*\n//) { $_ .= <>; redo; } ...; }
Here, we're reading a line at a time into $_. If the line contains comments, they get stripped away. If the line is completely blank (doesn't contain any non-blank characters), it's ignored. The tricky part is the substitution. Any backslash near the end of the line (optionally surrounded by whitespace) will be stripped, and we'll append the next line from the input.
However, that now leaves us with a line that could need
comment-stripping, blank-checking, or possibly additional backslash
processing, so the easiest way is to redo
, which jumps back up to
the top of the loop. Nice and compact.
The last
, next
, and redo
operators apply to a ``naked block''
as well as the iterators (while
, until
, for
, and foreach
).
With a naked block (one that's not part of a larger construct), we can
make rather arbitrary control structures. For example, let's add all
the numbers that are entered one at a time, letting the word ``end''
stop us.
{ print "number? "; chomp($n = <STDIN>); last if $n eq "end"; $sum += $n; redo; }
Here, the naked block serves as the boundaries for the enclosed
last
and redo
operations. This problem can be solved in other
ways, but we've essentially created a loop that enters in the middle,
or exits in the middle, depending on how you look at it. We can also
create the multiway if
tests using naked blocks. Let's look
at some rudimentary @ARGV processing:
{
last unless $ARGV[0] =~ /^-/;
$_ = shift;
last if /^--$/;
$verbose++, redo if /^-v$/;
$radians++, redo if /^-r$/;
$expr = ($1 || shift), redo
if /^-e(.*)$/;
$file = ($1 || shift), redo
if /^-f(.*)$/;
die ``unknown arg: $_'';
}
Here, we process @ARGV entries until we don't have an initial dash.
If the first element starts with a dash, we shift it off into $_. If
that's exactly a double dash, we stop looking as well. However, if
it's a -v
or a -r
, we set the corresponding variable, and jump
back up to the top. If it's -e
or -f
, we take the remainder of
the arg (or the next arg if no further text) as a parameter. Finally,
if we make it through the maze and don't see something we like, we can
bail out.
You could also flip this around with the and
and or
operators,
as in:
{
$ARGV[0] =~ /^-/ or last;
$_ = shift;
/^--$/ and last;
/^-v$/ and $verbose++, redo;
/^-r$/ and $radians++, redo;
/^-e(.*)$/ and
$expr = ($1 || shift), redo;
/^-f(.*)$/ and
$file = ($1 || shift), redo;
die ``unknown arg: $_'';
}
This does exactly the same thing as the previous example. While I was
starting to write this example, I went ``yuck'', because I'm somewhat
opposed to using the and
and or
operators in a void context
(that is, simply for their side effects of conditionally executing
their right-hand operand). However, after looking at this code, I'm
actually more in favor of it, because it looks cleaner to me. The
downside is that someone looking at this code might not understand how
and
and or
work, so a comment above to that effect might be
handy.
Well, I hope that gives you a few more choices for choices, and lets you cycle through a few more forms for iterations. Always remember the Perl motto: There's more than one way to do it! See you next time.