Control Flow¶
Directing Program Logic¶
Version: 1.0 Year: 2026
Copyright Notice¶
Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.
Conditionals: if/elsif/else¶
Every program needs to make decisions. In Perl, the primary tool for branching is if:
my $temperature = 95;
if ($temperature > 100) {
print "Boiling!\n";
} elsif ($temperature > 80) {
print "Hot\n";
} elsif ($temperature > 60) {
print "Comfortable\n";
} else {
print "Cold\n";
}
A few things stand out immediately:
- The condition must be in parentheses.
- The body must be in braces, even for a single statement. Perl does not allow braceless
ifblocks (unlike C or JavaScript). elsifhas no seconde. This trips up every newcomer exactly once.
Comparison Operators¶
Perl has two complete sets of comparison operators - one for numbers and one for strings. Using the wrong set is one of the most common Perl bugs.
| Operation | Numeric | String |
|---|---|---|
| Equal | == |
eq |
| Not equal | != |
ne |
| Less than | < |
lt |
| Greater than | > |
gt |
| Less or equal | <= |
le |
| Greater or equal | >= |
ge |
| Comparison (three-way) | <=> |
cmp |
The three-way operators (<=> and cmp) return -1, 0, or 1. They are essential for custom sort comparators:
my @sorted = sort { $a <=> $b } @numbers; # numeric ascending
my @alpha = sort { $a cmp $b } @strings; # alphabetical
String vs. Numeric Comparison
"apple" == "banana" evaluates to true because == forces both strings to numeric context, converting them to 0. Use eq for string comparison. If use warnings is enabled (and it should be), Perl will warn you about this mistake.
Compound Conditions¶
Combine conditions with logical operators. Perl provides two styles:
| Meaning | Symbol (high precedence) | Word (low precedence) |
|---|---|---|
| AND | && |
and |
| OR | \|\| |
or |
| NOT | ! |
not |
# Symbol style - common in conditions
if ($age >= 18 && $age <= 65) {
print "Working age\n";
}
# Word style - common in flow control
open my $fh, '<', $file or die "Cannot open $file: $!";
The word operators (and, or, not) have lower precedence than assignment, which is why open ... or die works without parentheses. The symbol operators (&&, ||, !) bind more tightly and are the standard choice inside conditionals.
Precedence Rule of Thumb
Use &&/||/! inside if conditions. Use and/or/not for flow control at the statement level (like open ... or die). Mixing them in the same expression leads to surprises.
unless¶
unless is a negated if. It executes the block when the condition is false:
This reads more naturally than if (!$user_authenticated) in many cases. But there are clear rules about when unless helps readability:
Use unless when:
- The condition is a simple boolean or single test
- The English reads naturally: "unless the user is authenticated, redirect"
Avoid unless when:
- You need an
elsiforelsebranch -unless/elsereads backwards and confuses everyone - The condition is compound (
unless ($a && !$b)is a logic puzzle) - You are using double negatives (
unless (!$found)- just useif ($found))
# Good - reads naturally
die "File not found" unless -e $filename;
# Bad - unless with else is confusing
unless ($ready) {
wait_more();
} else {
proceed(); # Wait, so this runs when $ready is true?
}
# Better - just use if
if ($ready) {
proceed();
} else {
wait_more();
}
The Ternary Operator¶
The ternary operator (?:) is an expression-level if/else. It evaluates to one of two values based on a condition:
This is equivalent to:
The ternary operator shines in assignments, function arguments, and print statements - anywhere you need a value, not a block:
printf "Found %d %s\n", $count, ($count == 1) ? "item" : "items";
my $label = $is_admin ? "Administrator" : "User";
You can nest ternaries, but deep nesting becomes unreadable quickly:
# One level of nesting is sometimes acceptable
my $grade = ($score >= 90) ? "A"
: ($score >= 80) ? "B"
: ($score >= 70) ? "C"
: "F";
# More than two levels? Use if/elsif instead.
Tip
If a ternary expression does not fit on one line or requires nesting more than two levels deep, switch to if/elsif. The few saved lines are not worth the readability cost.
Statement Modifiers¶
Perl supports postfix conditionals - you can put if, unless, while, until, for, or foreach after a single statement:
print "Found it!\n" if $found;
warn "Disk full\n" unless $space_available;
print $_ while <STDIN>;
$total += $_ for @values;
Statement modifiers work only with a single statement on the left side. You cannot put a block before a modifier:
# This is a syntax error
{
print "hello\n";
print "world\n";
} if $greet;
# Do this instead
if ($greet) {
print "hello\n";
print "world\n";
}
The common modifiers and when to use them:
| Modifier | Meaning | Example |
|---|---|---|
if |
Execute when true | return $cached if exists $cache{$key}; |
unless |
Execute when false | die "Required" unless defined $value; |
while |
Loop while true | print <$fh> while defined($_ = <$fh>); |
until |
Loop until true | sleep 1 until -e $lockfile; |
for/foreach |
Loop over list | print "$_\n" for @names; |
Statement modifiers read like English and reduce visual clutter for simple operations. The convention is: use the modifier form when the action is more important than the condition, and the whole thing fits on one line.
# Modifier form - emphasizes the action
next if $line =~ /^#/;
# Block form - emphasizes the condition
if ($line =~ /^#/) {
$comment_count++;
push @comments, $line;
next;
}
Perl's Truth Rules¶
Truth in Perl is simple once you learn the four false values. Understanding truthiness is fundamental to every conditional, loop, and short-circuit operator in the language.
A value is false if it is:
undef- no value at all""- the empty string0- the number zero"0"- the string containing just the character zero
Everything else is true. This includes "0E0", " " (a space), "00", 0.0 (which stringifies to "0"), empty arrays in scalar context (they evaluate to 0), and references (always true).
# All false
if (undef) { ... } # false
if ("") { ... } # false
if (0) { ... } # false
if ("0") { ... } # false
# All true (some surprise people)
if ("0E0") { ... } # true - non-empty string that isn't "0"
if (" ") { ... } # true - contains a space
if ("00") { ... } # true - two characters, not the string "0"
if (0.0) { ... } # false - 0.0 is numeric 0
if (\0) { ... } # true - it's a reference
if ([]) { ... } # true - it's a reference to an empty array
The \"0E0\" Trick
The DBI module returns "0E0" for queries that succeed but affect zero rows. This evaluates to true in boolean context (it is a non-empty string that is not "0") but to 0 in numeric context (it is zero in scientific notation). This lets you distinguish "zero rows affected" (true, "0E0") from "query failed" (false, undef).
The following diagram shows how Perl evaluates truthiness:
flowchart TD
A[Value to evaluate] --> B{Is it undef?}
B -->|Yes| FALSE[FALSE]
B -->|No| C{Is it a string?}
C -->|Yes| D{Is it empty ''?}
D -->|Yes| FALSE
D -->|No| E{Is it '0'?}
E -->|Yes| FALSE
E -->|No| TRUE[TRUE]
C -->|No| F{Is it 0?}
F -->|Yes| FALSE
F -->|No| TRUE
In practice, Perl internally converts between strings and numbers freely, so the evaluation is more nuanced. A value like 0.0 is numeric zero, which is false. A value like "00" is the string "00", not "0", so it is true. The rules above cover every case you will encounter in real code.
while and until Loops¶
The while loop repeats a block as long as the condition is true:
The until loop is the opposite - it repeats until the condition becomes true:
my $response;
until (defined $response && $response eq "yes") {
print "Continue? (yes/no): ";
$response = <STDIN>;
chomp $response;
}
do-while and do-until¶
If you need the body to execute at least once before checking the condition, use do:
my $input;
do {
print "Enter a number (1-10): ";
$input = <STDIN>;
chomp $input;
} while ($input < 1 || $input > 10);
do-while Is Not a True Loop
A do { ... } while block is not a real loop in Perl's eyes. The next, last, and redo loop control statements do not work inside it. If you need loop control, use a regular while loop with the exit condition at the end.
Infinite Loops¶
while (1) creates an infinite loop. You break out of it with last:
while (1) {
print "Command: ";
my $cmd = <STDIN>;
chomp $cmd;
last if $cmd eq "quit";
process_command($cmd);
}
Reading Input with while¶
One of Perl's most common patterns is reading input line by line:
Perl has special magic here: the while (<STDIN>) construct automatically checks for defined rather than truth, so it correctly handles lines containing just "0". Writing while (defined(my $line = <STDIN>)) is equivalent, but the short form is idiomatic.
The same pattern works for files:
open my $fh, '<', 'data.txt' or die "Cannot open: $!";
while (my $line = <$fh>) {
chomp $line;
# process $line
}
close $fh;
for and foreach Loops¶
Perl has two loop styles: C-style for and list-iteration foreach. In practice, Perl treats for and foreach as interchangeable keywords - you can use either for both styles. Most Perl programmers use for exclusively.
C-style for¶
The three-part header works exactly like C: initialize, test, increment. The loop variable $i is lexically scoped to the loop when declared with my.
List Iteration with for/foreach¶
The far more common style iterates over a list:
If you omit the loop variable, Perl uses $_:
Aliasing, Not Copying
The loop variable is an alias for the current element, not a copy. Modifying the loop variable modifies the original array:
This is powerful but can cause bugs if you forget about it. Use a named variable (for my $item (@array)) to make the aliasing visible and intentional.
The Range Operator¶
The range operator (..) generates a list of consecutive values:
for my $n (1..10) {
print "$n\n";
}
# Also works with characters
for my $letter ('a'..'z') {
print "$letter ";
}
Reverse Iteration¶
To iterate in reverse:
Iterating with Index¶
When you need both the index and the value:
$#items gives the last index of the array.
Loop Control¶
Three statements control loop execution from inside the body: next, last, and redo.
| Statement | Effect | Equivalent in C/Python |
|---|---|---|
next |
Skip to the next iteration | continue / continue |
last |
Exit the loop entirely | break / break |
redo |
Restart the current iteration (no re-check) | No equivalent |
for my $n (1..20) {
next if $n % 2 == 0; # skip even numbers
last if $n > 15; # stop after 15
print "$n\n"; # prints 1, 3, 5, 7, 9, 11, 13, 15
}
redo¶
redo restarts the current iteration without re-evaluating the loop condition or advancing the iterator. It is useful for retry logic:
for my $server (@servers) {
my $response = ping($server);
unless ($response) {
warn "Retrying $server...\n";
sleep 1;
redo; # try the same $server again
}
print "$server is up\n";
}
Infinite redo
Without a counter or other exit condition, redo can create an infinite loop. Always include a way to eventually stop retrying:
Labeled Loops¶
When you have nested loops, next and last affect the innermost loop by default. Labels let you target an outer loop:
OUTER: for my $i (1..10) {
for my $j (1..10) {
next OUTER if $j == 5; # skip to next $i
last OUTER if $i == 3; # exit both loops
print "$i.$j ";
}
}
Labels are uppercase by convention. They go before the loop keyword followed by a colon. You can use any label name, but OUTER, LINE, ROW, and FILE are common choices that describe what the loop iterates over.
Short-Circuit Operators¶
Perl's logical operators do not just return true or false - they return the value that determined the result. This makes them powerful control flow tools beyond simple boolean logic.
|| and && as Value Selectors¶
The || (logical or) operator evaluates the left side. If true, it returns that value. If false, it evaluates and returns the right side:
my $name = $user_input || "Anonymous";
# If $user_input is truthy, $name gets that value
# If $user_input is false (empty string, undef, 0), $name gets "Anonymous"
The && (logical and) operator evaluates the left side. If false, it returns that value. If true, it evaluates and returns the right side:
The Defined-Or Operator: //¶
The || operator has a problem: it treats 0 and "" as false, which is often not what you want for defaults. The // (defined-or) operator checks for defined instead of truth:
my $port = $config{port} // 8080;
# Uses 8080 only if $config{port} is undef
# If $config{port} is 0, it keeps 0 (unlike ||)
This is critical for numeric defaults where 0 is a legitimate value:
my $count = $args{count} || 10; # Bug: count of 0 becomes 10
my $count = $args{count} // 10; # Correct: only undef becomes 10
Assignment Shortcuts¶
Both || and // have assignment forms:
$x //= "default" is the standard pattern for setting defaults. It assigns "default" only if $x is undef, leaving 0, "", and other false-but-defined values alone.
Error Handling with or¶
The or operator (low-precedence version of ||) is Perl's idiom for error handling:
open my $fh, '<', $filename or die "Cannot open $filename: $!";
chdir $directory or die "Cannot chdir to $directory: $!";
mkdir $path or warn "Cannot create $path: $!";
This works because open returns a truthy value on success and false (undef) on failure. When open succeeds, or short-circuits and die never executes. When open fails, or evaluates the right side and die terminates the program with an error message.
or vs || for Error Handling
Use or (not ||) with die/warn because of precedence. open $fh, '<', $file || die ... is parsed as open $fh, '<', ($file || die ...), which passes the result of $file || die as the filename. The or operator's low precedence ensures the open call completes first.
given/when (Experimental)¶
Perl 5.10 introduced given/when as a switch/case mechanism:
# Historical syntax - DO NOT USE in new code
use feature 'switch';
given ($status) {
when ("active") { process_active() }
when ("pending") { process_pending() }
when ("inactive") { process_inactive() }
default { handle_unknown() }
}
Deprecated and Unreliable
given/when has been experimental since its introduction and produces deprecation warnings in Perl 5.38+. The smartmatch operator (~~) that powers it has complex, surprising behavior that even experienced Perl developers find confusing. The feature is expected to be removed in a future Perl release.
Use if/elsif chains instead:
if ($status eq "active") {
process_active();
} elsif ($status eq "pending") {
process_pending();
} elsif ($status eq "inactive") {
process_inactive();
} else {
handle_unknown();
}
For dispatch tables (mapping values to actions), use a hash of code references:
Putting It All Together¶
Control flow in Perl is about choosing the right tool for each situation:
if/elsif/elsefor multi-way branchingunlessfor simple negated conditions- Ternary for inline value selection
- Statement modifiers for concise single-statement conditions
while/untilfor condition-driven loopsfor/foreachfor iterating over listsnext/last/redofor fine-grained loop control- Labels for controlling nested loops
||////&&for short-circuit logic and defaultsor/andfor statement-level flow control and error handling
The key is readability. Perl gives you many ways to express the same logic. Pick the one that makes intent clearest to someone reading your code six months from now - including yourself.
Further Reading¶
- perlsyn - Perl Syntax - official documentation for all Perl control structures
- perlop - Perl Operators - complete operator reference including precedence table
- Learning Perl, Chapter 10: More Control Structures - the "Llama Book" covers control flow in depth
- Perl Best Practices, Chapter 6: Control Structures - Damian Conway's style recommendations
- Modern Perl, Chapter 3 - control flow in modern Perl style
Previous: Arrays, Hashes, and Lists | Next: Regular Expressions | Back to Index