Perl from the ground up

Patterns: matching, replacing and splitting text

The part of Perl that made its name: searching inside a string, pulling pieces out of it with captures, rewriting it, and cutting it into fields.

Part 9 of 12

Advanced20 min readBack to Courses

A pattern between slashes

A pattern is written between slashes, and the =~ operator asks whether a string matches it: if ($line =~ /warning/) { ... }. The pattern can match anywhere in the string, so ^ and $ anchor it to the start and the end when that matters.

Most characters stand for themselves. A few do not: \d is a digit, \w is a word character, \s is any kind of space, and a dot is any one character. + means one or more of what came before and ? means optional, so \d+ is "a run of digits" — which is most of what everyday matching needs.

Capturing the pieces

Parentheses in a pattern do two jobs: they group, and they remember. What each pair matched lands in $1, $2 and so on, in order, and stays there until the next successful match.

Those numbered pieces are the whole reason people reached for Perl when a log needed reading. A pattern like /^(\w+):(\d+):(.+)$/ leaves the name, the number and the rest waiting in $1, $2 and $3. Anchor the pattern, so a line that does not fit is skipped rather than half-parsed.

Replacing, splitting, joining

s/old/new/ substitutes the first match and s/old/new/g changes every one, rewriting the string itself: $text =~ s/\s+/ /g; collapses runs of whitespace into single spaces. split cuts a string into a list on a pattern, and join glues a list back into one string — together they are how text becomes data and data becomes text.

split ' ', $text is a special case worth knowing. With a quoted single space, Perl splits on any run of whitespace and ignores space at the start, which makes it the shortest way to turn a sentence into words.

How the engine tries a pattern

The regex engine starts at the left of the string and tries to match there. When a part of the pattern can match more than one way, it takes the longest option first and backtracks to a shorter one if the rest of the pattern fails. That search is why a pattern can look right and still capture more text than you expected.

Anchors remove the search: ^ pins the attempt to the start and $ to the end, so a line that does not fit fails as a whole. \b marks a word boundary, and a character class such as [aeiou] matches one character from a set. Put those pieces together and the pattern describes the shape of the line, not just a word somewhere inside it.

Substitution edits the target

$text =~ s/old/new/g changes $text itself and answers the number of replacements. Without =~ the target is $_, which is handy in a loop and easy to miss when reading. The g makes the engine continue after each replacement; without it, only the first match changes.

A substitution is not a copy-and-edit operation, so the original is gone once it runs. If you need both versions, copy the string first. Captures have the same in-place feel: $1, $2 and the rest are overwritten by the next successful match, so copy them into names immediately if a later pattern might run.

Flags that change the question

A flag after the closing slash changes the match: /warning/i ignores case, and /x lets a long pattern be spread over several lines with comments, because whitespace stops counting. A /g match in a loop keeps asking for further matches, which is how every number in a line is collected.

!~ is the negation: if ($line !~ /ok/) reads as "does not match", which is clearer than wrapping a match in not. Flags go after the second slash and nowhere else.

A line of fields, a collapsed sentence, a colour list

use strict;
use warnings;

my $line = 'Ada:36:London';
if ($line =~ /^(\w+):(\d+):(.+)$/) {
  print "name $1, age $2, city $3\n";
}

my $text = 'Perl   is   a   text   language';
$text =~ s/\s+/ /g;
print "$text\n";
my @words = split ' ', $text;
print "words: ", scalar @words, "\n";

my $csv = 'red,green,blue';
my @colours = split /,/, $csv;
print join(' | ', @colours), "\n";

# Anchors decide which lines a branch is taken for.
print "starts with Ada\n" if $line =~ /^Ada/;
print "has a number\n" if $line =~ /\d+/;

Two lines of fields, a sentence and a list

Run it and check that both lines were parsed and the collapsed sentence reads normally. Then add a third line to the list, and one more capture of your own to the pattern.

Output
Click "Run" to execute your code

A pattern ending in a plus sign is greedy: it takes as much as it can while still letting the rest of the pattern match. If a capture comes back holding more than you expected, add a question mark after it or make the pattern more specific.

Three pattern mistakes are quiet. =~ binds a match; == compares numbers, so $line == /warn/ compares $line with a match against $_, not with the pattern. A dot matches any character, so a literal dot needs \.. And $1 is overwritten by the next successful match, so copy the captures you need before running another pattern.

Try these next

  • Parse three lines of name:score with one anchored pattern and print a table of the captures.
  • Collapse runs of spaces in a sentence, then split it into words and count them.
  • Split a CSV line with a trailing comma, once with the default limit and once with a limit of -1.

Read a log and count the warnings

Run it and check the counts. Then add a line that does not match the pattern and confirm it is skipped rather than half-read.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Where does the first set of parentheses in a pattern put what it matched?