ZetCode

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.

ahead.pl
#!/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.

negative.pl
#!/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.

behind.pl
#!/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.

commas.pl
#!/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.

password.pl
#!/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.

zero.pl
#!/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

In this article we have covered the four lookaround assertions, inserting separators with zero-width matches, combining lookaheads, and the \K escape.

Source

perlre: Lookaround Assertions

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.