<!-- category: regex -->
The /e Modifier: Code Execution in Substitution
The replacement side ofs/// is normally a string. Put /e on
the end, and it becomes Perl code.
Every number in the string just got doubled. Not by matching and rebuilding. By running actual Perl code on each match. The replacement expressionmy $text = "I have 3 cats and 12 dogs"; $text =~ s~(\d+)~$1 * 2~ge; say $text; # I have 6 cats and 24 dogs
$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:
With$str =~ s~foo~bar~; # replace "foo" with string "bar" $str =~ s~(\w+)~[$1]~; # replace word with "[$1]" (interpolated)
/e, the replacement is code:
The difference is subtle but profound. Without$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
/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:Built-in functions work too: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
Any expression that returns a scalar works. The result gets stringified and dropped into the string as the replacement.# 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;
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:Withoutmy %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
/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:
Themy %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
// "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:The ternary operator works perfectly inmy $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
/e replacements. So
do if blocks wrapped in do{}:
That turns numeric scores into letter grades. Each match runs the full$text =~ s~(\d+)~ do { if ($1 >= 90) { "A" } elsif ($1 >= 80) { "B" } elsif ($1 >= 70) { "C" } else { "F" } } ~ge;
if/elsif/else block and uses the returned string.
Part 5: URL ENCODING AND DECODING
Classic use case. URL-encode special characters:And the reverse, URL decode: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
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:
This is a baby template engine in one line. Themy %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).
// "[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 ado{} block:
Output: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;
TheAlice,95,A Bob,87,B Carol,42,F Dave,73,C
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 multiples///e calls to build transformation
pipelines:
Each substitution transforms the string, and the next one works on the result. Simple, readable, and each step does one thing.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
You can also use the /r modifier (non-destructive) to chain
without mutating:
Themy $result = $text =~ s~\b(\w)~uc($1)~ger =~ s~(\d+)~$1 * 2~ger =~ s~(\w{5,})~"[$1]"~ger;
/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.
Wait, what happened? Firstmy $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
/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.
Legitimate uses forMODIFIER 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
/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:
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.# 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;
The.--. |o_o | "Your replacement side |:_/ | is Turing-complete." // \ \ (| | ) /'\_ _/`\ \___)=(___/
/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