ZetCode

Perl regular expressions

last modified October 9, 2026

Regular expressions are patterns that describe text. In Perl, they are built into the language and are used with the match operator m//, the substitution operator s///, and the transliteration operator tr///. Perl patterns are character based, they can capture text, look around the current position, and be assembled from reusable pieces. In this tutorial, we cover Perl regular expressions with practical examples.

A pattern is applied to a string with the binding operator =~. The negated form !~ returns true when the pattern does not match. When the pattern is written directly with /.../, the m can be omitted.

Basic matching

The simplest use of a regular expression is to test whether a string contains a pattern.

basic.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "Perl is a powerful language";

if ($text =~ /powerful/) {
    say "The word 'powerful' was found";
}

if ($text !~ /python/) {
    say "The word 'python' was not found";
}

The =~ operator returns true when the pattern matches the string. The !~ operator is its negation.

$ perl basic.pl
The word 'powerful' was found
The word 'python' was not found

Character classes and escapes

A character class matches one character out of a set. The common shorthand classes are \d (a digit), \w (a word character), \s (whitespace), and their negated forms \D, \W, and \S. A class written in square brackets, such as [a-z0-9], matches any character from the listed ranges.

classes.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "Item A12 costs 4.50 USD";

if ($text =~ /([A-Z])(\d+)/) {
    say "Prefix $1, number $2";
}

say "Contains a digit" if $text =~ /\d/;
say "Contains no tab" if $text !~ /\t/;

my $pairs = "key=value; other=thing";
my @tokens = $pairs =~ /(\w+)=(\w+)/g;
say "Tokens: @tokens";

say "Lowercase word" if "abc123" =~ /^[a-z0-9]+$/;
say "Has a hyphen" if "abc-123" =~ /[^\w]/;

my $version = "perl-5.34.0";
my ($major, $minor) = $version =~ /(\d+)\.(\d+)/;
say "Major $major, minor $minor";

Inside a class, ^ in the first position negates the set, so [^\w] matches any character that is not a word character. A dot inside a class is a literal dot, unlike the . metacharacter. Ranges are inclusive, and several shorthands can be combined, for example [\d\s].

$ perl classes.pl
Prefix A, number 12
Contains a digit
Contains no tab
Tokens: key value other thing
Lowercase word
Has a hyphen
Major 5, minor 34

Anchors and word boundaries

Anchors match a position rather than a character. The ^ anchor matches the beginning of the string, $ matches the end, and \b matches a boundary between a word character and a non-word character. The stricter forms \A and \z always refer to the beginning and the end of the whole string.

anchors.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @log = ("ERROR: disk full", "WARN: low memory", "ERROR: timeout");

for my $line (@log) {
    say "Fatal: $line" if $line =~ /^ERROR:/;
}

say "Ends with .txt" if "report.txt" =~ /\.txt\z/;

my $sentence = "The quick brown fox";
my @words = $sentence =~ /\b\w+\b/g;
say "Words: @words";

if ("12345" =~ /\A\d+\z/) {
    say "The whole string is digits";
}

say "Not a single word char" if "---" !~ /\w/;

The $ anchor also matches just before a trailing newline, while \z matches only the very end of the string. Use \A and \z when validating input, so that a stray newline cannot slip through. Word boundaries let you match whole words and avoid partial matches inside longer words.

$ perl anchors.pl
Fatal: ERROR: disk full
Fatal: ERROR: timeout
Ends with .txt
Words: The quick brown fox
The whole string is digits
Not a single word char

Captures

Parentheses group part of a pattern and capture the text it matches. The captured values are available in the variables $1, $2, and so on.

captures.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "Today is 2026-10-08 in the calendar";

if ($text =~ /(\d{4})-(\d{2})-(\d{2})/) {
    say "Year: $1";
    say "Month: $2";
    say "Day: $3";
}

The pattern (\d{4})-(\d{2})-(\d{2}) has three groups. Each group is stored in its own numbered variable. Note that \d matches a digit and {4} means exactly four repetitions.

$ perl captures.pl
Year: 2026
Month: 10
Day: 08

Quantifiers and greediness

Quantifiers repeat the preceding item. * means zero or more, + means one or more, ? means zero or one, and {n,m} gives an explicit range. By default quantifiers are greedy, so they match as much text as possible. Adding ? makes them lazy, which means they match as little as possible.

quant.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "aaa ab aaaa b";

my @matches = $text =~ /a+/g;
say "One or more: @matches";

@matches = $text =~ /a{3}/g;
say "Exactly three: @matches";

@matches = $text =~ /\ba{2,3}\b/g;
say "Two or three: @matches";

my $html = "<b>bold</b> and <i>italic</i>";
my ($greedy) = $html =~ /<(.+)>/;
my ($lazy)   = $html =~ /<(.+?)>/;
say "Greedy: $greedy";
say "Lazy:   $lazy";

The greedy pattern <(.+)> swallows everything up to the last > in the string. The lazy version <(.+?)> stops at the first >, which is usually what you want when parsing markup. Quantifiers also accept a possessive form, written a++ or a*+, which never gives characters back.

$ perl quant.pl
One or more: aaa a aaaa
Exactly three: aaa aaa
Two or three: aaa
Greedy: b>bold</b> and <i>italic</i
Lazy:   b

Alternation and grouping

The vertical bar | matches either alternative, and parentheses group parts of a pattern. A group written as (?:...) groups without capturing, which keeps the numbered capture variables clean. The delimiters of a match can be changed to almost any character, which is handy when the pattern itself contains slashes.

alternation.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "I like cats and dogs";

if ($text =~ /cat|dog/) {
    say "Found an animal";
}

my @found = $text =~ /(cat|dog)s?/g;
say "Matches: @found";

my $date = "12/25/2026";
if ($date =~ m{(\d{1,2})/(\d{1,2})/(\d{4})}) {
    say "Month $1, day $2, year $3";
}

my $color = "color";
if ($color =~ /colou?r/) {
    say "Matches both spellings";
}

my $prefix = "pre-heated";
if ($prefix =~ /^(?:pre|post)-/) {
    say "Non-capturing group matched '$&'";
    say "No numbered group created" unless defined $1;
}

Alternation has the lowest precedence, so grouping is often required. The pattern /ab|cd/ matches ab or cd, whereas /a(b|c)d/ matches abd or acd. The m{...} form avoids escaping the slashes in the date pattern.

$ perl alternation.pl
Found an animal
Matches: cat dog
Month 12, day 25, year 2026
Matches both spellings
Non-capturing group matched 'pre-'
No numbered group created

Backreferences

A backreference matches the same text that an earlier group already matched. The escape \1 refers to group 1, \2 to group 2, and so on. This makes it possible to detect repeated words, matching quotes, or doubled strings.

backref.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "The the cat sat";

if ($text =~ /\b(\w+)\s+\1\b/i) {
    say "Repeated word: '$1'";
}

my $quoted = q{She said "hello" then 'bye'};
while ($quoted =~ /(["'])(.*?)\1/g) {
    say "Quoted text: $2";
}

my $line = "abcabc";
if ($line =~ /^(\w{3})\1$/) {
    say "'$line' is a doubled string";
}

The group (["']) captures whichever quote character appears first, and \1 then requires the same character to close the text. Without a backreference, the two quote styles could be mixed. In the replacement part of s///, the same values are written as $1, $2, and so on.

$ perl backref.pl
Repeated word: 'The'
Quoted text: hello
Quoted text: bye
'abcabc' is a doubled string

Lookaround assertions

Lookaround asserts what comes before or after the current position without consuming any characters. (?=...) is a positive lookahead, (?!...) a negative lookahead, (?<=...) a positive lookbehind, and (?<!...) a negative lookbehind.

look.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $prices = "10 USD, 20 EUR, 30 USD";

while ($prices =~ /(\d+)(?=\s*USD)/g) {
    say "USD amount: $1";
}

my @amounts = $prices =~ /(?<=\s)(\d+)/g;
say "Amounts not at the start: @amounts";

my $text = "foobar foobaz foobar";
my @foos = $text =~ /foo(?!bar)/g;
say "foo not followed by bar: @foos";

my $password = "secret";
if ($password !~ /(?=.*\d)/) {
    say "Password has no digit";
}

Because a lookahead does not consume text, (\d+)(?=\s*USD) captures the amount while the currency name stays available for the next match. A lookbehind must have a fixed width in older Perl versions; since Perl 5.30 variable-length lookbehind is also supported. Lookarounds are useful when the surrounding context should be checked but not captured.

$ perl look.pl
USD amount: 10
USD amount: 30
Amounts not at the start: 20 30
foo not followed by bar: foo
Password has no digit

Global matching in list context

The /g modifier finds all matches, not just the first one. In list context, the match returns all captured values (or all whole matches, if the pattern has no groups) as a flat list. Assigning that list to a hash turns key-value pairs into hash entries.

global.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $data = "x=10, y=20, z=30";

my @pairs = $data =~ /(\w)=(\d+)/g;
say "Flat list: @pairs";

my %values = $data =~ /(\w)=(\d+)/g;

for my $key (sort keys %values) {
    say "$key => $values{$key}";
}

The first match gives the flat list x, 10, y, 20, z, 30. The second assignment uses the same list as key-value pairs for the hash.

$ perl global.pl
Flat list: x 10 y 20 z 30
x => 10
y => 20
z => 30

Substitution with /g and /e

The s/// operator replaces matched text. The /g modifier replaces all occurrences, and /e treats the replacement as a Perl expression. The operator returns the number of substitutions made.

subst.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $prices = "apple 3, pear 5, plum 10";

(my $doubled = $prices) =~ s/(\d+)/$1 * 2/ge;
say $doubled;

my $name = "hello world";
(my $title = $name) =~ s/(\w+)/\u$1/g;
say $title;

my $word = "banana";
my $count = ($word =~ s/a/A/g);
say "$word, replaced $count";

Without /e, the replacement $1 * 2 would be inserted as literal text. With /e, it is evaluated as code for each match. The (my $doubled = $prices) construct copies the string first, so the original is not changed. The \u escape uppercases the next character.

$ perl subst.pl
apple 6, pear 10, plum 20
Hello World
bAnAnA, replaced 3

Non-destructive substitution with /r

Substitution normally changes the target string in place. The /r modifier returns a modified copy instead and leaves the original untouched. This removes the need for the (my $copy = $original) idiom.

copy.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "hello world";

my $upper = $text =~ s/(\w+)/\u$1/gr;
say "Original: $text";
say "Copy:     $upper";

my @words = ("one", "two");
my @shout = map { s/(\w+)/\U$1/r } @words;
say "Shouted: @shout";

The /r flag works with /g and /e as well, and it composes nicely with map, because each element can be transformed without a temporary variable. The alternation of flags, such as /gr, can be written in any order.

$ perl copy.pl
Original: hello world
Copy:     Hello World
Shouted: ONE TWO

Escaping metacharacters

Characters such as ., *, +, and ? have a special meaning in a pattern. To match them literally, escape them with a backslash. \Q and \E quote everything between them, and the quotemeta function returns a quoted copy of a string.

escape.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $path = "foo.a.bar";
my @parts = split /\./, $path;
say "Split on dot: @parts";

my $separator = "a.b";
my $pattern = qr/\Q$separator\E/;
say "Matches literal 'a.b'" if $path =~ $pattern;

my $regex = "c++";
my $quoted = quotemeta($regex);
say "Quoted: $quoted";
say "Literal match" if "learning c++" =~ /$quoted/;

if ("1+1=2" =~ /\Q1+1\E=2/) {
    say "Escaped literal metacharacters";
}

Quoting is essential when a variable from user input or a configuration file is inserted into a pattern, because an unescaped dot or plus would change the meaning of the regex. The \Q escape applies only until the end of the string or the next \E.

$ perl escape.pl
Split on dot: foo a bar
Matches literal 'a.b'
Quoted: c\+\+
Literal match
Escaped literal metacharacters

Counting with tr///

The tr/// operator (also written y///) replaces or counts characters. It works on single characters, not patterns. When the replacement list is empty, it only counts the matching characters and leaves the string unchanged.

trans.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $dna = "AGCTTAGCTA";

my $gc = ($dna =~ tr/GC//);
say "G and C count: $gc";

(my $rna = $dna) =~ tr/T/U/;
say "RNA: $rna";

my $text = "Perl regex tutorial";
my $vowels = ($text =~ tr/aeiouAEIOU//);
say "Vowels: $vowels";

The first tr/// counts the G and C characters in the DNA string. The second one replaces every T with U to convert DNA to RNA. The third one counts the vowels.

$ perl trans.pl
G and C count: 4
RNA: AGCUUAGCUA
Vowels: 7

The /i and /x modifiers

The /i modifier ignores case. The /x modifier allows whitespace and comments inside the pattern, which makes complex patterns easier to read.

modifiers.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "Perl PERL perl";

my @matches = $text =~ /perl/gi;
say "Case-insensitive matches: ", scalar @matches;

my $phone = qr/
    (\d{3})   # area code
    [-.\s]?   # optional separator
    (\d{4})   # number
/x;

if ("555-1234" =~ $phone) {
    say "Area $1, number $2";
}

The qr// operator compiles a pattern into a reusable object. With /x, the spaces and newlines in the pattern are ignored, and the text after # is a comment. To match a literal space, use \s or escape it.

$ perl modifiers.pl
Case-insensitive matches: 3
Area 555, number 1234

The /s and /m modifiers

By default, ^ and $ anchor to the start and end of the whole string, and . does not match a newline. The /m modifier makes ^ and $ match at line boundaries, and /s makes . match newlines as well.

multiline.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $log = "start\nERROR: disk full\nWARN: low memory\nERROR: timeout\nend";

my @errors = $log =~ /^ERROR.*$/mg;
say "Errors: @errors";

if ($log =~ /start.*end/s) {
    say "Dot matched across newlines";
}

my @levels = $log =~ /^(\w+):/mg;
say "Levels: @levels";

With /m, each line of the log is treated as its own string for anchoring purposes. With /s, the dot spans the whole record including the newlines. When both are needed, they can be combined as /ms, and the order of the letters does not matter.

$ perl multiline.pl
Errors: ERROR: disk full ERROR: timeout
Dot matched across newlines
Levels: ERROR WARN ERROR

Special match variables

After a successful match, Perl fills several variables. $& holds the whole match, $` the text before it, and $' the text after it. The numbered groups are in $1, $2, and so on. The arrays @- and @+ store the start and end offsets of each group.

special.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "The year is 2026, the month is 10";

if ($text =~ /(\d{4})/) {
    say "Whole match: $&";
    say "Before match: '$`'";
    say "After match:  '$''";
    say "Group 1: $1";
    say "Group 1 spans indexes $-[1] to $+[1]";
}

my $sentence = "cat bat rat";

while ($sentence =~ /(\w)at/g) {
    say "Group: $1, whole match: $&, ends at ", pos($sentence);
}

The offsets in @- and @+ are zero-based and describe the substring substr($text, $-[1], $+[1] - $-[1]). Because $&, $`, and $' can slow down the whole program, prefer capturing groups when only part of the match is needed.

$ perl special.pl
Whole match: 2026
Before match: 'The year is '
After match:  ', the month is 10'
Group 1: 2026
Group 1 spans indexes 12 to 16
Group: c, whole match: cat, ends at 3
Group: b, whole match: bat, ends at 7
Group: r, whole match: rat, ends at 11

Named captures

Named captures use the syntax (?<name>...). The matched text is available in the %+ hash, using the group name as the key. Named captures make code easier to read than numbered variables.

named.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $line = "2026-10-08 ERROR Disk full";

if ($line =~ /(?<date>\d{4}-\d{2}-\d{2})\s+(?<level>\w+)\s+(?<msg>.+)/) {
    say "Date: $+{date}";
    say "Level: $+{level}";
    say "Message: $+{msg}";
    say "Names: ", join(", ", sort keys %+);
}

The $+{date} expression reads the value of the named group date. The keys of %+ list all named groups that participated in the match.

$ perl named.pl
Date: 2026-10-08
Level: ERROR
Message: Disk full
Names: date, level, msg

Scalar context, pos, and global matching

In scalar context, /g returns one match at a time and remembers where it stopped. That position is stored per variable and can be read with pos. The loop below uses this behavior to walk through the words of the string. When the match fails, the position is reset.

pos.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = "one two three";

while ($text =~ /(\w+)/g) {
    say "Word '$1' ends at position ", pos($text);
}

say defined pos($text) ? "pos is set" : "pos is reset";

my $count = () = $text =~ /\w+/g;
say "Count in list context: $count";

The while loop calls the match in scalar context on each iteration. After the last word, the match fails and pos becomes undefined. The () = ... idiom forces list context and counts the matches.

$ perl pos.pl
Word 'one' ends at position 3
Word 'two' ends at position 7
Word 'three' ends at position 13
pos is reset
Count in list context: 3

Validating input

Regular expressions are often used to validate user input. For validation, anchor the pattern with \A and \z so that the whole string must match, and take advantage of alternation to describe the allowed forms.

validate.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

sub is_email {
    my ($addr) = @_;
    return $addr =~ /\A[\w.+-]+\@[\w-]+(\.[\w-]+)+\z/;
}

for my $addr ("user\@example.com", "bad\@", "a.b\@mail.co.uk") {
    say "$addr is ", is_email($addr) ? "valid" : "invalid";
}

my $ip = "192.168.0.1";
if ($ip =~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
    say "Octets: $1 $2 $3 $4";
}

my $time = "23:59:59";
if ($time =~ /\A([01]\d|2[0-3]):[0-5]\d:[0-5]\d\z/) {
    say "Valid time: $time";
}

The time pattern accepts hours from 00 to 23 and minutes and seconds from 00 to 59, without any arithmetic. Note that the email and IP patterns above are simplified, they check the shape of the input rather than every rule of the standards.

$ perl validate.pl
user@example.com is valid
bad@ is invalid
a.b@mail.co.uk is valid
Octets: 192 168 0 1
Valid time: 23:59:59

In this article we have covered Perl regular expressions, from simple matching and captures to character classes, anchors, quantifiers, alternation, backreferences, lookaround, modifiers, and substitution. Together with the match, substitution, and transliteration operators these features form the core of text processing in Perl.

Source

Perl regular expressions · Perl regular expression tutorial · Perl regular expressions quick start · Perl character classes · Perl backslash sequences

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Perl tutorials.