Perl regular expressions: match, capture and replace safely
You will finish with a small Perl script that validates a record, extracts named fields, finds every match and performs a deliberate substitution. The examples target the locally installed Perl 5.38.2 and its perlre(1) documentation. Allow about 20 minutes if you are comfortable with shell commands and basic Perl; allow longer if regular expressions are new to you.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
You need a Linux shell and the perl executable. No elevated privileges are needed, and these examples only read their input and print results. Check the interpreter before relying on version-specific behaviour:
perl -v
perl -e 'print "$^V\n"'
On this machine the second command prints v5.38.2. The Perl regular expression syntax is broad, so this guide concentrates on ordinary matching, captures, modifiers and substitutions rather than embedded code blocks or engine internals.
Checkpoint 1: make a whole-value check
Start with a pattern that describes the complete value. The ^ and $ anchors make a difference: without them, a pattern can find a valid-looking fragment inside a bad value. The \A and \z anchors are stricter about the absolute start and end of the string, so they are a good default for validation.
my @values = ('backup-2026-09-26', 'backup-2026-9-26', 'prefix-backup-2026-09-26');
for my $value (@values) {
if ($value =~ /\Abackup-\d{4}-\d{2}-\d{2}\z/) {
print "valid: $value\n";
} else {
print "invalid: $value\n";
}
}
Expected output:
valid: backup-2026-09-26
invalid: backup-2026-9-26
invalid: prefix-backup-2026-09-26
This checks shape, not whether the date exists. For example, month 99 still has two digits. Keep that distinction visible in your code and add a date parser when calendar validity matters.
Checkpoint 2: capture fields by name
Parentheses capture text matched by their contents. Named captures make later code easier to audit than remembering that the third pair of parentheses is the year. Perl stores a named capture in %+ when it succeeds.
my $record = '[email protected] | 42';
if ($record =~ /\A(?<email>[\w.+-]+\@[\w.-]+)\s*\|\s*(?<count>\d+)\z/) {
print "email=$+{email}\n";
print "count=$+{count}\n";
}
Expected output:
[email protected]
count=42
The \s* parts accept optional whitespace around the separator. The pattern still rejects a missing email, a non-numeric count or trailing text. If you need the captured text in a replacement, use a named backreference such as ${^CAPTURE} only after checking the exact interpolation rules; for straightforward substitutions, the replacement examples below are less ambiguous.
Checkpoint 3: make a readable pattern
Long expressions become easier to review with the /x modifier. It permits whitespace and comments inside the pattern. A literal space must then be written as \ or matched with \s. Add /i when case should not matter, and keep that choice local rather than changing the meaning of every pattern.
my $line = 'Status: READY';
my $status = qr{
\A status: \s* (ready|paused|failed) \z
}ix;
if ($line =~ $status) {
print "state=$1\n";
}
Expected output:
state=READY
qr{...} builds a reusable regular expression. The braces are delimiters here, so the slash character does not need escaping. This is useful when the same check is applied in several places, but do not interpolate untrusted text into a pattern without a deliberate escaping step.
Checkpoint 4: collect repeated matches
The /g modifier finds successive matches. In list context, a match with one capture returns the captured values, which is often clearer than inspecting the special match variables.
my $text = 'IDs: job-17, job-204, job-9';
my @ids = ($text =~ /\bjob-(\d+)\b/g);
print join(',', @ids), "\n";
Expected output:
17,204,9
A common distraction trap is confusing /g with "match anywhere forever". A pattern that can match an empty string, such as \w??, needs special care in a loop. The regular expression documentation describes Perl's zero-length safeguards, but a clearer design is usually to require progress, for example by matching \w+ when an actual word is required.
Checkpoint 5: substitute only what you intend
The substitution operator is s{pattern}{replacement}modifiers. Use /g only when every occurrence should change. Capture groups in the replacement are available as $1, $2 and so on. Prefer the braced form for adjacent digits, because \1000 is easy to misread as a backreference.
my $message = 'ticket 17 and ticket 204';
$message =~ s{\bticket (\d+)\b}{case-$1}g;
print "$message\n";
Expected output:
case-17 and case-204
For a literal string supplied by a user, do not insert it into the pattern as raw regular expression syntax. Use quotemeta or \Q...\E when the text is meant to be literal. Also remember that the right side of a substitution is a double-quoted context. If you add the /e modifier, it becomes Perl code: treat that as a security-sensitive boundary and never evaluate untrusted replacement text.
Common failures and recovery
- If a check accepts a suffix or prefix unexpectedly, inspect its anchors first. Use
\Aand\zfor a complete string. - If
$1is empty or stale, check whether the match succeeded before using it. A failed match does not provide new captures. - If a pattern becomes unreadable, split it with
/x, name meaningful captures and test one condition at a time. - If a replacement changes too much, remove
/gand print a copy before assigning it back. These substitutions only change an in-memory scalar, so rerunning the script is the undo operation. - If a pattern is slow on long input, look for nested or overlapping quantifiers and alternatives that can backtrack heavily. Test with representative worst-case input before putting it on a request path.
There is no service restart or system file change in this guide. If you adapt the substitution to rewrite a file, write to a new temporary file first, inspect it, then replace the original only after a successful check. Keep a backup when the original is difficult to recreate.
Done means
- You checked the installed Perl version.
- Your validation pattern anchors the whole value when that is the requirement.
- You can explain each capture and use named captures for structured data.
- You use
/x,/iand/gintentionally, not by habit. - You tested a substitution's output and know how to avoid raw or executable untrusted input.