Before you begin
Install a supported Perl 5 interpreter and check perl -v. Save examples as .pl plain-text files. Run perl filename.pl. The first lessons use core Perl only. Keep use strict and use warnings at the top of each script.
Working toward: Build a tested log-analysis tool that handles Unicode, bad input, and large files.
Read each explanation, run the example in your own lab, and attempt the exercise before opening its answer. Published lessons are ready to study; unfinished roadmap topics remain planned.
Validation: Lessons 1–4 retain their prior expected-output checks. Lesson 5 and its solution were executed on Linux using Perl 5.38.2 and Test::More 1.302194, including deliberate failing tests. Current Perl 5.44 documentation was reviewed; 5.44 and other operating systems were not execution-tested.
1. Scalars and interpolation
A scalar stores one value and its name begins with $. my declares a lexical variable. Double-quoted strings interpolate variables; a semicolon ends a statement. strict catches several classes of accidental code, including undeclared variables.
use strict;
use warnings;
my $days = 7;
my $daily = 25;
my $total = $days * $daily;
print "Food budget: $total EUR\n";What to expect
Food budget: 175 EUR
Your turn
Add 18 euros for a taxi and print 193.
Show answer and reasoning
my $taxi = 18;
$total += $taxi;
print "$total\n";Watch for: Use eq to compare text and == to compare numbers. Numeric comparison can coerce text in surprising ways.
Link to this lesson2. Arrays and hashes
An array is ordered and uses @ when referring to the whole collection. A hash uses % and associates keys with values. A single element is a scalar, so it uses $. Sort hash keys when stable report order matters.
use strict;
use warnings;
my @cities = ("Munich", "Bucharest");
my %visits;
for my $city (@cities) {
$visits{$city}++;
}
for my $city (sort keys %visits) {
print "$city: $visits{$city}\n";
}What to expect
Bucharest: 1, followed by Munich: 1.
Your turn
Add Munich a second time and predict the count.
Show answer and reasoning
my @cities = ("Munich", "Bucharest", "Munich");
# Munich: 2Watch for: Hash iteration order is not a presentation order. An array index begins at zero.
Link to this lesson3. Match a line deliberately
A regular expression describes a text pattern. Anchors ^ and $ constrain the match to the line boundaries. Parentheses capture the varying parts. Here a word and an integer are separated by whitespace.
use strict;
use warnings;
my $line = "taxi 18";
if ($line =~ /^(\w+)\s+(\d+)$/) {
my ($item, $euros) = ($1, $2);
print "$item costs $euros EUR\n";
}What to expect
taxi costs 18 EUR
Your turn
Try "taxi eighteen" and explain why it does not match. Extend the pattern to accept a decimal amount with exactly two decimal places.
Show answer and reasoning
The integer pattern requires digits. A decimal form is /^(\w+)\s+(\d+\.\d{2})$/. This recognizes text; monetary arithmetic still needs an explicit rounding or integer-cents policy.Watch for: A dot normally matches almost any character. Escape it to mean a literal decimal point. A regex is not a CSV parser.
Link to this lesson4. Stream a UTF-8 file
Three-argument open separates filename from access mode. The encoding layer decodes bytes. Reading one line at a time avoids loading a large file into memory. chomp removes the input record separator.
use strict;
use warnings;
open my $fh, "<:encoding(UTF-8)", "places.txt"
or die "Cannot open places.txt: $!";
my $count = 0;
while (my $line = <$fh>) {
chomp $line;
$count++ if length $line;
}
close $fh or die "Cannot close: $!";
print "$count nonempty lines\n";Run it
Create places.txt with Munich, a blank line, and Bucharest on separate lines.What to expect
2 nonempty lines
Your turn
Make whitespace-only lines count as empty too.
Show answer and reasoning
Replace length $line with $line =~ /\S/. A line containing spaces has a length but no non-whitespace character.Watch for: Avoid two-argument open for untrusted filenames. Decode input explicitly; terminal output encoding is a separate concern.
Link to this lesson5. Give a text rule a name—and prove its behavior
Before this lesson: Complete scalars, collections and patterns (lessons 1–3). Use Perl 5 with Test::More in an empty lab folder; check perl -v and perl -MTest::More -e 1. Outcome: extract one text-cleaning rule into a function and write tests that detect a broken rule. This lesson uses traditional argument unpacking and no experimental features.
A subroutine names reusable behavior. Before writing it, define a contract: our clean_label function takes one defined scalar containing text, removes surrounding whitespace, preserves internal spaces and letter case, and returns the cleaned value without altering the caller's variable. A blank result is allowed. This is whitespace cleanup—not spelling correction, case folding or Unicode normalization.
For a subroutine without signatures, @_ holds the incoming arguments. scalar @_ gives their count. my ($label) = @_ copies the one value into a lexical variable, so replacing characters in $label does not edit the original variable. defined distinguishes undef (no value) from an empty string. die rejects an unsupported call; return hands the result back rather than printing it. Separating the rule from output makes the same rule usable in a report or test.
The substitution operator s/pattern/replacement/ changes matching text. \\A and \\z are absolute string boundaries; \\s includes whitespace such as spaces, tabs and newlines. Two substitutions remove leading and trailing whitespace only. This follows the earlier pattern lesson but deliberately does not collapse spaces between words.
Save the program below as labels.t. The .t suffix is a testing convention; it is still Perl code. Test::More's is compares actual and expected values, ok checks a true condition, and like checks a regex against text. Each test has a name describing the rule it protects. eval BLOCK catches die in this controlled test, and $@ contains the error immediately afterward. The final 1 makes a successful eval true even if the called function legitimately returns an empty string. done_testing declares how many checks ran. These tests are executable examples of the contract; passing them is evidence for these cases, not a proof about every possible input.
use strict;
use warnings;
use Test::More;
sub clean_label {
die "clean_label expects one defined scalar\n"
unless @_ == 1 && defined $_[0] && !ref $_[0];
my ($label) = @_;
$label =~ s/\A\s+//;
$label =~ s/\s+\z//;
return $label;
}
my $raw = " Field Notes\t";
is(clean_label($raw), "Field Notes", "trim edges, preserve interior");
is($raw, " Field Notes\t", "caller value is unchanged");
is(clean_label(""), "", "empty text is valid");
is(clean_label(" \t\n"), "", "whitespace-only text becomes empty");
is(clean_label("0"), "0", "zero is text, not missing");
my $ok = eval { clean_label(undef); 1 };
my $error = $@;
ok(!$ok, "undefined input is rejected");
like($error, qr/expects one defined scalar/, "failure explains contract");
done_testing();Run it
perl labels.t
prove -v labels.t
# prove is the test harness normally included with Perl's test tools.
# If unavailable, perl labels.t still executes the checks.
# Record a failure status immediately with echo $? in Bash.What to expect
Seven named 'ok' checks and a plan of 1..7; prove reports success. The original $raw still contains its edge whitespace. To see a useful failure, temporarily remove the trailing-whitespace substitution: the first and whitespace-only tests should report 'not ok', and the process should exit nonzero. Restore the line and rerun; do not change expected values just to make the suite green.
Your turn
Extend the test file (before done_testing) to check an already-clean label, a missing argument, and two arguments. Check both rejection and its message for each invalid call. Predict the new number of tests. Next, deliberately remove the leading-whitespace substitution and show which existing test detects it. Restore the implementation and rerun the suite.
Show answer and reasoning
# Insert before done_testing():
is(clean_label("Munich"), "Munich", "clean label is unchanged");
my $missing_ok = eval { clean_label(); 1 };
my $missing_error = $@;
ok(!$missing_ok, "missing argument is rejected");
like($missing_error, qr/expects one defined scalar/, "missing-argument reason");
my $extra_ok = eval { clean_label("one", "two"); 1 };
my $extra_error = $@;
ok(!$extra_ok, "extra argument is rejected");
like($extra_error, qr/expects one defined scalar/, "extra-argument reason");
# Twelve tests: seven original plus one success and two pairs.
# Removing the leading substitution breaks the first check:
# the actual string still begins with spaces. The empty and
# zero checks alone would not detect that bug.
# Exit status and test diagnostics matter, not just seeing output.
# Ready to move on: write a test for a newline at each edge,
# predicting the cleaned value and checking caller preservation.Watch for: A truthiness check rejects the valid string '0'; defined is the correct missing-value question here. Assigning through $_[0] can mutate the caller, so copy first. ref is included to reject references, a topic for the next unit. Keep eval here as a BLOCK, not a string of code, and capture $@ before another eval changes it. This ASCII fixture does not validate every Unicode whitespace/encoding scenario. Current Perl also supports signatures; experimental named-parameter signatures are not required here.
Lesson references
- Perl subroutines: argument aliasing, copying, return and signatures (checked 14 September 2026) ↗
- Test::More: is, ok, like and done_testing ↗
- Perl eval: block exceptions and $@ ↗
- Perl regular expressions: anchors and character classes ↗
Path to advanced
In-progress stages identify the lessons already published. All other listed topics remain planned. Each addition needs teaching, a reproducible lab, failure cases, and a checkpoint before the capstone.
Reliable text tools
Published: subroutines with Test::More checks (lesson 5). Planned: references, command-line options, reusable modules, and deeper error reporting.
IN PROGRESS · PUBLISHED LESSONS ABOVEData and modules
CPAN tooling, CSV parsers, JSON, DBI placeholders, and Unicode boundaries.
PLANNEDMaintainable applications
Modules, object design, profiling, dependencies, and deployment.
PLANNEDCapstone
Create a streaming report tool with fixture-based tests and documented failure behavior.
PLANNED
References
Original AUWEN lessons, with upstream documentation for further study and version checks.
All learning paths and update notes →