Perl lookaround
last modified October 20, 2026
Lookaround assertions are zero-width regex features that test the text before or after a position without making it part of the match. There are four of them: positive and negative lookahead, and positive and negative lookbehind. They are useful for context-dependent matches, for inserting text between characters, and for combining several conditions in one pattern.
Positive lookahead
A lookaround assertion checks the text around the current position without
consuming it. The positive lookahead (?=...) succeeds when the
given pattern follows, but the following text does not become part of the match.
#!/usr/bin/perl use strict; use warnings; use v5.34.0; my $text = 'apple pie, apple tart, pineapple'; my @before_pie = $text =~ /(\w+)(?=\s+pie)/g; say "word before pie: @before_pie"; my $count = () = $text =~ /apple(?= tart)/g; say "apple followed by ' tart': $count"; (my $marked = $text) =~ s/apple(?= tart)/APPLE/g; say $marked; my $price = '100 USD, 200 EUR, 300 USD'; my @usd = $price =~ /(\d+)(?=\s+USD)/g; say "USD amounts: @usd";
The first match returns the word before pie without the
pie itself. The pattern apple(?= tart) counts and
marks only the apple that is followed by a tart, and leaves the one in
pineapple and the one before the pie. The amounts followed by
USD are returned without the currency, which is why
200 is missing.
$ perl ahead.pl word before pie: apple apple followed by ' tart': 1 apple pie, APPLE tart, pineapple USD amounts: 100 300
Negative lookahead
The negative lookahead (?!...) succeeds when the pattern does not
follow. Placed at the start of a pattern, it excludes whole lines.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @files = qw(a.txt b.bak c.txt d.tmp e.pl);
my @not_bak = grep { /^(?!.*\.(?:bak|tmp)$)/ } @files;
say "not backup or temp: @not_bak";
my $text = 'cat catalog concat cat.';
my @standalone = $text =~ /\b(cat)(?!\w)/g;
say "standalone cat: ", scalar @standalone;
my @q = 'q qu quit qi' =~ /\bq(?!u)\w*/g;
say "q not followed by u: @q";
my @lines = ('INFO ok', 'ERROR disk', 'INFO done', 'WARN slow');
say "not INFO: ", join ', ', grep { /^(?!INFO)/ } @lines;
The pattern ^(?!.*\.(?:bak|tmp)$) accepts a file name only when the
rest of it does not end with an unwanted extension. The (?!\w)
after cat makes sure that the word ends there, so
catalog and concat are ignored. In the third example
the words that start with q but not with qu are found.
The last example filters the lines that do not begin with INFO.
$ perl negative.pl not backup or temp: a.txt c.txt e.pl standalone cat: 2 q not followed by u: q qi not INFO: ERROR disk, WARN slow
Lookbehind
Lookbehind assertions look at the text before the position. The positive form is
(?<=...) and the negative form is (?<!...). The
pattern inside must have a fixed length, except for limited variable-length
forms in recent Perl versions.
#!/usr/bin/perl use strict; use warnings; use v5.34.0; my $text = 'price: $30, cost: $45, fee: 12'; my @dollars = $text =~ /(?<=\$)(\d+)/g; say "after dollar sign: @dollars"; my @bare = $text =~ /(?<![\$\d])\b(\d+)\b/g; say "without dollar sign: @bare"; my $url = 'http://a.com https://b.com ftp://c.com'; my @hosts = $url =~ /(?<=:\/\/)([\w.]+)/g; say "hosts: @hosts"; my $str = 'foobar foobaz barfoo'; my @not_foo = $str =~ /(?<!foo)bar/g; say "bar not after foo: ", scalar @not_foo;
The first match returns the digits that follow a dollar sign without the sign.
The second one finds numbers that are not preceded by a dollar sign or another
digit; the digit in the class keeps it from matching in the middle of a number.
The hosts are extracted after the :// separator. The last example
counts only the bar that does not follow foo.
$ perl behind.pl after dollar sign: 30 45 without dollar sign: 12 hosts: a.com b.com c.com bar not after foo: 1
Inserting separators
Lookarounds match positions, not characters, so a substitution with a zero-width match inserts text without removing anything. This is the classic way to add thousands separators and to split words.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
for my $n (1, 12, 1234, 1234567, 1234567890) {
(my $s = $n) =~ s/(?<=\d)(?=(?:\d{3})+$)/,/g;
say $s;
}
my $decimal = '9876543.2109';
$decimal =~ s/(\d+)/$1 =~ s{(?<=\d)(?=(?:\d{3})+$)}{,}gr/e;
say $decimal;
my $camel = 'parseHttpResponseCode';
my @parts = split /(?=[A-Z])/, $camel;
say "@parts";
my $snake = $camel =~ s/(?<=[a-z])(?=[A-Z])/_/gr;
say lc $snake;
The pattern finds every position that is preceded by a digit and followed by one
or more groups of exactly three digits up to the end, and inserts a comma there.
For the decimal value, the substitution is applied only to the integer part,
using /e with a nested substitution. The splitting of
camelCase works the same way: the lookahead (?=[A-Z])
matches the position before each capital, and split breaks the
string there.
$ perl commas.pl 1 12 1,234 1,234,567 1,234,567,890 9,876,543.2109 parse Http Response Code parse_http_response_code
Combining lookaheads: password check
Several lookaheads at the same position act as a logical AND: all of them must succeed, and since none consumes text, they can check independent conditions.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $strong = qr/
^
(?=.*[a-z]) # a lowercase letter
(?=.*[A-Z]) # an uppercase letter
(?=.*\d) # a digit
(?=.*[^\w\s]) # a symbol
\S{8,} # at least 8 non-space characters
$
/x;
for my $pw ('password', 'Passw0rd', 'P@ssw0rd!', 'Sh0rt!', 'NoDigits!!', 'has space1A!') {
printf "%-14s %s\n", $pw, $pw =~ $strong ? 'strong' : 'weak';
}
Each lookahead at the beginning requires one kind of character somewhere in the
string: a lowercase letter, an uppercase letter, a digit and a symbol. After
they all pass, \S{8,} checks the length and the absence of
whitespace. The password with a space fails, and so do the ones missing a
character class. The /x flag keeps the rules readable.
$ perl password.pl password weak Passw0rd weak P@ssw0rd! strong Sh0rt! weak NoDigits!! weak has space1A! weak
Zero-width behaviour and \K
Because lookarounds do not consume text, matches can overlap and a pattern can
match an empty string. The escape \K offers an alternative to
lookbehind: it keeps the text matched so far out of the final match.
#!/usr/bin/perl use strict; use warnings; use v5.34.0; my $s = 'aaaa'; my @plain = $s =~ /aa/g; say "plain aa matches: ", scalar @plain; my @peek = $s =~ /a(?=a)/g; say "overlapping a(?=a): ", scalar @peek; $s =~ /b*/g; say "zero-width match at: ", pos($s); my $text = 'price 10 USD'; $text =~ /(?<=price )(\d+)(?= USD)/; say "match: '$&', before: '$`', after: '$''"; (my $k = 'foo=bar') =~ s/foo=\Kbar/baz/; say "with \\K: $k"; (my $l = 'foo=bar') =~ s/(?<=foo=)bar/baz/; say "with lookbehind: $l";
The plain aa pattern finds two matches in four letters, while
a(?=a) finds three, because the following letter is not consumed.
The empty pattern b* matches at position 0. The special variables
show that $& contains only the part between the assertions. The
last two substitutions give the same result: \K discards
foo= from the match, and the lookbehind never includes it.
\K is the way to go when the prefix has a variable length.
$ perl zero.pl plain aa matches: 2 overlapping a(?=a): 3 zero-width match at: 0 match: '10', before: 'price ', after: ' USD' with \K: foo=baz with lookbehind: foo=baz
Best Practices
- Remember they are zero-width: the text inside a lookaround is not part of
$&and is not consumed. - Keep lookbehinds fixed-length: use
\Kwhen the prefix varies. - Use lookahead for AND conditions: several
(?=...)at the start combine independent rules. - Avoid cleverness: if a capture group solves the problem, it is often easier to read.
- Add comments with /x: lookaround patterns are dense, so explain each assertion.
In this article we have covered the four lookaround assertions, inserting
separators with zero-width matches, combining lookaheads, and the
\K escape.
Source
Author
List all Perl tutorials.