Replacing with Named Captures and Precompiled Regular Expressions in Perl

Viewed 110

I'm trying to compile a set of substitution regexes but I can't figure out how to delay interpolation of the capture variables in the replacement scalar I'm setting aside; here's a simple contrived example:

use strict;
use warnings;

my $from = "quick";
my $to = "zippy";

my $find = qr/${from} (?<a>(fox|dog))/;
my $repl = "$to $+{a}"; # Use of uninitialized value in concatenation (.) or string

my $s0 = "The quick fox...\n";
$s0 =~ s/${find}/${repl}/;
print($s0);

This doesn't work because repl is interpolated immediately and elicits "Use of uninitialized value in concatenation (.) or string"

If I use non-interpolating '' quotes it doesn't interpolate in the actual substitution so I get "The zippy $+{a}..."

Is there a trick to setting aside a replacement scalar that contains capture references?

3 Answers

You are getting the warning because you are using $+{a} before performing the match. qr// doesn't perform any matching; it's simply compiles the pattern. It's s/// that performs the match.

You presumably meant to use

my $repl = "$to \$+{a}";

But that simply outputs

The zippy \$+{a}...

You could use the following:

my $find = qr/quick (?<a>fox|dog)/;

my $s0 = "The quick fox...\n";
$s0 =~ s/$find/zippy $+{a}/;
print($s0);

But that hard codes the replacement expression. If you want this code to be dynamic, then what you are building is a template system.

I don't know of any template system with your specific desired syntax.

If you're ok with using the positional variables ($1) instead of named ones ($+{a}), you can use String::Substitution.

use String::Substitution qw( sub_modify );

my $find = qr/quick (?<a>fox|dog)/;    # Or simply qr/\Q$from\E (fox|dog)/
my $repl = "zippy \$1";

my $s0 = "The quick fox...\n";
sub_modify($s0, $find, $repl);
print($s0);

The qr// only compiles a pattern. It does not perform a match, so it does not set anything in %+. Hence, the uninitialized warnings.

However, you can do that in the substitution so you don't need to prepare the replacement ahead of time:

s/$find/$to $+{a}/;

However, if you don't know what you want your replacement to be, you can eval code in the replacement side of the substitution that will then be the replacement. Here's a simple addition:

s/$find/ 2 + 2 /e;

You'd get the sum as the replacement:

The 4 jumped over the lazy dog

But here's the rub: That's code and it can do whatever code can do. How you construct that is very important and should never use unsanitized user input.

If you didn't know the string you wanted to put in there, you can construct it beforehand and store it in the variable you use in the replacement side. However, you are making Perl code to eval, so it needs to be a valid Perl string. The double quotes are part of the eval that you will eval later:

my $replacement = '"$to $+{a}"';
s/$find/$replacement/;

Like that, you get the literal string value from $replacement:

The "$to $+{a}" jumped over the lazy dog

Adding the /e means that we evaluate the replacement side as code:

s/$find/$replacement/e;

But, that code is $replacement, and ends up giving us the same result because it's just its string value:

The "$to $+{a}" jumped over the lazy dog

Now here's the fun part. We can eval again! Add another /e and the substitution will eval the first time, then take that result and eval it again:

$s0 =~ s/${find}/$replacement/ee;

The first round of the eval gets the literal text value of $replacement, which is "$to $+{a}" (including the double quotes). The second round takes "$to $+{a}" and evals that, filling in the variables with the values in the current lexical scope. The %+ is populated by the substitution already. Now you have your result:

The zippy fox jumped over the lazy dog

However, this isn't a trick you should pull out lightly. There might be a better way to attack your problem. You do this sort of thing when you bend anything else to your will.

You also have to be very careful that you do what you intend in the string that you construct. You are creating new Perl code. If you are using any sort of outside data that you didn't supply, someone can trick your program into running code that you didn't intend.

There are three good ways to do dynamic regex substitution at runtime:

  1. String interpolation of variables s///
  2. Callback for code execution s///e
  3. Embedded code constructs in the regex.

See the examples below.

Normally a callback form, either via a function or Embedded regex code is used when logic is required to construct a replacement.

Otherwise, use a simple string interpolation on the replacement side.

use strict;
use warnings;

my $s0 = "";
my ($from, $to) = ("quick", "zippy") ;

sub getRepl {
    my ($grp1, $grp2) = @_;
    if ( $grp1 eq $from ) {
       return "<$to $grp2>" }
    else {
       return "< $2>"
    }
}

my $find = qr/(\Q${from}\E) (fox|dog)/;

# ======================================
# Substitution via string interpolation
$s0 = "The quick dog...\n";
$s0 =~ s/$find/[$to $2]/;
print $s0;

# ======================================
# Substitution via callback (eval)
$s0 = "The quick dog...\n";
$s0 =~ s/$find/ getRepl($1,$2) /e;
print $s0;


# ==================================================
# Substitution via regex embedded code constructs
my $repl = "";
my $RxCodeEmbed = qr/(\Q${from}\E)(?{$repl = '(' . $to}) (fox|dog)(?{$repl .= ' ' . $^N . ')'})/;

$s0 = "The quick dog...\n";
$s0 =~ s/$RxCodeEmbed/$repl/;
print $s0;

Outputs

The [zippy dog]...
The <zippy dog>...
The (zippy dog)...
Related