perl.gg / regex

<!-- category: regex -->

The /e Modifier: Code Execution in Substitution

2026-04-11

The replacement side of s/// is normally a string. Put /e on the end, and it becomes Perl code.
my $text = "I have 3 cats and 12 dogs"; $text =~ s~(\d+)~$1 * 2~ge; say $text; # I have 6 cats and 24 dogs
Every number in the string just got doubled. Not by matching and rebuilding. By running actual Perl code on each match. The replacement expression $1 * 2 is not a string containing a dollar sign, a one, a space, and an asterisk. It is an arithmetic expression that Perl evaluates and uses as the replacement text.

This is s///e. The /e modifier tells Perl: "Do not treat the replacement as a double-quoted string. Treat it as code. Evaluate it. Use the result."

Part 1: BASIC SYNTAX

Without /e, the replacement is a string:
$str =~ s~foo~bar~; # replace "foo" with string "bar" $str =~ s~(\w+)~[$1]~; # replace word with "[$1]" (interpolated)
With /e, the replacement is code:
$str =~ s~(\d+)~$1 + 1~e; # replace number with (number + 1) $str =~ s~(\w+)~uc($1)~e; # replace word with uppercase version $str =~ s~(\d+)~sprintf("%04d", $1)~e; # zero-pad numbers
The difference is subtle but profound. Without /e, $1 + 1 is the literal string "3 + 1" (with variable interpolation of $1). With /e, it is the arithmetic expression 3 + 1, which evaluates to 4.
MODIFIER REPLACEMENT IF $1 = "3" RESULT -------- ---------------- ----------- ------ (none) $1 + 1 "3 + 1" 3 + 1 /e $1 + 1 eval(3 + 1) 4

Part 2: CALLING FUNCTIONS

Since the replacement is code, you can call any function:
use POSIX 'ceil'; my $text = "Prices: 3.7 and 12.2 and 8.9"; # round up all floating-point numbers $text =~ s~(\d+\.\d+)~ceil($1)~ge; say $text; # Prices: 4 and 13 and 9
Built-in functions work too:
# uppercase every word $text =~ s~(\w+)~uc($1)~ge; # reverse every word $text =~ s~(\w+)~scalar reverse($1)~ge; # get string length of every word $text =~ s~(\w+)~length($1)~ge;
Any expression that returns a scalar works. The result gets stringified and dropped into the string as the replacement.

Part 3: HASH LOOKUPS FOR TRANSLATION TABLES

This is one of the most practical uses. You have a mapping table and you want to translate matching patterns:
my %emoticons = ( ':)' => 'smile', ':(' => 'frown', ':D' => 'grin', ';)' => 'wink', ':P' => 'tongue', ); my $text = "Hey :) how are you :( I am :D fine ;)"; # build a regex from the keys (escape special chars) my $pattern = join '|', map { quotemeta } keys %emoticons; $text =~ s~($pattern)~$emoticons{$1}~ge; say $text; # Hey smile how are you frown I am grin fine wink
Without /e, the replacement $emoticons{$1} would be interpolated as a string. With /e, it is a hash lookup that runs for each match. Different match, different replacement.

Status code translation:

my %codes = ( 200 => 'OK', 301 => 'Moved', 404 => 'Not Found', 500 => 'Server Error', ); my $log = "status=200 status=404 status=500"; $log =~ s~status=(\d+)~status=$codes{$1} // "Unknown"~ge; say $log; # status=OK status=Not Found status=Server Error
The // "Unknown" fallback handles codes not in the hash. That is a full Perl expression running inside the substitution.

Part 4: CONDITIONAL REPLACEMENT

Because the replacement is code, you can use conditionals:
my $text = "scores: 45 78 92 33 88 61"; # highlight scores above 75 $text =~ s~(\d+)~ $1 >= 75 ? "[$1]" : $1 ~ge; say $text; # scores: 45 [78] [92] 33 [88] 61
The ternary operator works perfectly in /e replacements. So do if blocks wrapped in do{}:
$text =~ s~(\d+)~ do { if ($1 >= 90) { "A" } elsif ($1 >= 80) { "B" } elsif ($1 >= 70) { "C" } else { "F" } } ~ge;
That turns numeric scores into letter grades. Each match runs the full if/elsif/else block and uses the returned string.

Part 5: URL ENCODING AND DECODING

Classic use case. URL-encode special characters:
my $str = "hello world & goodbye <friends>"; # URL encode non-alphanumeric characters $str =~ s~([^A-Za-z0-9\-_.~])~sprintf("%%%02X", ord($1))~ge; say $str; # hello%20world%20%26%20goodbye%20%3Cfriends%3E
And the reverse, URL decode:
my $encoded = "hello%20world%20%26%20goodbye"; $encoded =~ s~%([0-9A-Fa-f]{2})~chr(hex($1))~ge; say $encoded; # hello world & goodbye
hex($1) converts the hex string to a number. chr() converts the number to a character. The /e modifier runs this conversion for every %XX sequence in the string.

Part 6: TEMPLATE VARIABLE EXPANSION

Expand ${variable} placeholders in a template string using a hash of values:
my %vars = ( name => 'Dave', role => 'sysadmin', server => 'prod-web-01', uptime => '47 days', ); my $template = 'Hello ${name}, you are the ${role} of ${server} ' . '(uptime: ${uptime}).'; $template =~ s~\$\{(\w+)\}~$vars{$1} // "[MISSING:$1]"~ge; say $template; # Hello Dave, you are the sysadmin of prod-web-01 (uptime: 47 days).
This is a baby template engine in one line. The // "[MISSING:$1]" fallback catches undefined variables instead of silently inserting empty strings.

You can make it more powerful. Environment variable expansion:

my $config = 'home=${HOME} user=${USER} shell=${SHELL}'; $config =~ s~\$\{(\w+)\}~$ENV{$1} // ''~ge; say $config; # home=/Users/dave user=dave shell=/bin/zsh

Part 7: MULTI-LINE REPLACEMENT WITH DO{}

When one expression is not enough, wrap your replacement in a do{} block:
my $csv = "Alice,95\nBob,87\nCarol,42\nDave,73"; $csv =~ s~^(\w+),(\d+)$~ do { my $grade = $2 >= 90 ? 'A' : $2 >= 80 ? 'B' : $2 >= 70 ? 'C' : $2 >= 60 ? 'D' : 'F'; "$1,$2,$grade" } ~gme; say $csv;
Output:
Alice,95,A Bob,87,B Carol,42,F Dave,73,C
The do{} block can contain any amount of Perl code. Declare variables, call functions, run loops. The last expression in the block becomes the replacement string.

Note the /m modifier alongside /e and /g. The /m makes ^ and $ match at line boundaries within the string, so each CSV line gets processed independently.

Part 8: CHAINING TRANSFORMS

You can chain multiple s///e calls to build transformation pipelines:
my $text = "the quick brown fox jumps over 3 lazy dogs at 12pm"; # step 1: capitalize first letter of each word $text =~ s~\b(\w)~uc($1)~ge; # step 2: double all numbers $text =~ s~(\d+)~$1 * 2~ge; # step 3: wrap long words in brackets $text =~ s~(\w{5,})~"[$1]"~ge; say $text; # The [Quick] [Brown] Fox [Jumps] Over 6 Lazy Dogs At 24pm
Each substitution transforms the string, and the next one works on the result. Simple, readable, and each step does one thing.

You can also use the /r modifier (non-destructive) to chain without mutating:

my $result = $text =~ s~\b(\w)~uc($1)~ger =~ s~(\d+)~$1 * 2~ger =~ s~(\w{5,})~"[$1]"~ger;
The /r flag returns the modified string instead of modifying in place. Each =~ in the chain works on the return value of the previous one. The original $text is untouched.

Part 9: THE /EE DOUBLE EVAL

If /e evaluates the replacement as code, what does /ee do? It evaluates it twice.

First evaluation: treat the replacement as code and get a string. Second evaluation: treat THAT string as code and evaluate it.

my $template = 'The answer is $answer'; my $answer = 42; # /e would give us the literal string '$answer' # because single quotes don't interpolate (my $result = $template) =~ s~(\$\w+)~$1~ee; say $result; # The answer is 42
Wait, what happened? First /e evaluates $1, which gives us the string $answer. Second /e evaluates $answer, which gives us 42.

This is powerful and dangerous. You are evaluating arbitrary strings as Perl code. If those strings come from user input, you have a code injection vulnerability. Never use /ee on untrusted data.

MODIFIER REPLACEMENT "expr" EVALUATIONS RESULT -------- ------------------ -------------------- ------ (none) $hash{$1} string interpolation $hash{foo} /e $hash{$1} code execution value of $hash{foo} /ee $hash{$1} code -> code eval of the value
Legitimate uses for /ee are rare. Config file expansion where you control the template. Code generation. Macro systems. If you find yourself reaching for /ee, stop and ask whether a hash lookup with /e would solve the problem more safely.

Part 10: PRACTICAL PATTERNS

A collection of /e patterns you can use today:
# zero-pad all numbers to 4 digits $str =~ s~(\d+)~sprintf("%04d", $1)~ge; # convert Celsius to Fahrenheit inline $str =~ s~(\d+)C~int($1 * 9/5 + 32) . "F"~ge; # obfuscate email addresses $str =~ s~(\w+)\@(\w+)~"$1\@" . ("x" x length($2))~ge; # increment version numbers $str =~ s~v(\d+)\.(\d+)~"v$1." . ($2 + 1)~ge; # replace Unix timestamps with human-readable dates use POSIX 'strftime'; $str =~ s~(\d{10})~strftime("%Y-%m-%d %H:%M", localtime($1))~ge; # wrap URLs in HTML links $str =~ s~(https?://\S+)~<a href="$1">$1</a>~ge; # pluralize: "1 cat" stays, "3 cat" becomes "3 cats" $str =~ s~(\d+)\s+(\w+)~$1 == 1 ? "$1 $2" : "$1 ${2}s"~ge;
Each one is a complete text transformation in a single line. The regex finds the targets, and the Perl code in the replacement decides what to do with them.
.--. |o_o | "Your replacement side |:_/ | is Turing-complete." // \ \ (| | ) /'\_ _/`\ \___)=(___/
The /e modifier turns s/// from a text replacement tool into a text transformation engine. The pattern side finds what you are looking for. The replacement side runs any Perl code you want on what it found.

Most regex flavors in other languages do not have this. They have callback functions for substitution, which is the same idea but with more ceremony. Perl lets you write the code right there in the regex, inline, no callback registration needed.

It is one of those features that sounds dangerous until you use it. Then it sounds indispensable. The first time you double every number in a log file with s~(\d+)~$1*2~ge, you will wonder how you ever lived without it.

Just stay away from /ee unless you know exactly what you are doing. One e is power. Two is a loaded weapon with the safety off.

perl.gg