How can I use a variable in the replacement side of the Perl substitution operator?
I would like to do the following:
$find = "start (.*) end";
$replace = "foo \1 bar";
$var = "start middle end";
$var =~ s/$find/$replace/;
I would expect $var to contain "foo middle bar", but it does not work. Neither does:
$replace = 'foo \1 bar';
Somehow I am missing something regarding the escaping.
Solution 1:
On the replacement side, you must use $1, not \1.
And you can only do what you want by making replace an evalable expression that gives the result you want and telling s/// to eval it with the /ee modifier like so:
$find="start (.*) end";
$replace='"foo $1 bar"';
$var = "start middle end";
$var =~ s/$find/$replace/ee;
print "var: $var\n";
To see why the "" and double /e are needed, see the effect of the double eval here:
$ perl
$foo = "middle";
$replace='"foo $foo bar"';
print eval('$replace'), "\n";
print eval(eval('$replace')), "\n";
__END__
"foo $foo bar"
foo middle bar
(Though as ikegami notes, a single /e or the first /e of a double e isn't really an eval()
; rather, it tells the compiler that the substitution is code to compile, not a string. Nonetheless, eval(eval(...))
still demonstrates why you need to do what you need to do to get /ee to work as desired.)
Solution 2:
Deparse tells us this is what is being executed:
$find = 'start (.*) end';
$replace = "foo \cA bar";
$var = 'start middle end';
$var =~ s/$find/$replace/;
However,
/$find/foo \1 bar/
Is interpreted as :
$var =~ s/$find/foo $1 bar/;
Unfortunately it appears there is no easy way to do this.
You can do it with a string eval, but thats dangerous.
The most sane solution that works for me was this:
$find = "start (.*) end";
$replace = 'foo \1 bar';
$var = "start middle end";
sub repl {
my $find = shift;
my $replace = shift;
my $var = shift;
# Capture first
my @items = ( $var =~ $find );
$var =~ s/$find/$replace/;
for( reverse 0 .. $#items ){
my $n = $_ + 1;
# Many More Rules can go here, ie: \g matchers and \{ }
$var =~ s/\\$n/${items[$_]}/g ;
$var =~ s/\$$n/${items[$_]}/g ;
}
return $var;
}
print repl $find, $replace, $var;
A rebuttal against the ee technique:
As I said in my answer, I avoid evals for a reason.
$find="start (.*) end";
$replace='do{ print "I am a dirty little hacker" while 1; "foo $1 bar" }';
$var = "start middle end";
$var =~ s/$find/$replace/ee;
print "var: $var\n";
this code does exactly what you think it does.
If your substitution string is in a web application, you just opened the door to arbitrary code execution.
Good Job.
Also, it WON'T work with taints turned on for this very reason.
$find="start (.*) end";
$replace='"' . $ARGV[0] . '"';
$var = "start middle end";
$var =~ s/$find/$replace/ee;
print "var: $var\n"
$ perl /tmp/re.pl 'foo $1 bar'
var: foo middle bar
$ perl -T /tmp/re.pl 'foo $1 bar'
Insecure dependency in eval while running with -T switch at /tmp/re.pl line 10.
However, the more careful technique is sane, safe, secure, and doesn't fail taint. ( Be assured tho, the string it emits is still tainted, so you don't lose any security. )
Solution 3:
As others have suggested, you could use the following:
my $find = 'start (.*) end';
my $replace = 'foo $1 bar'; # 'foo \1 bar' is an error.
my $var = "start middle end";
$var =~ s/$find/$replace/ee;
The above is short for the following:
my $find = 'start (.*) end';
my $replace = 'foo $1 bar';
my $var = "start middle end";
$var =~ s/$find/ eval($replace) /e;
I prefer the second to the first since it doesn't hide the fact that eval(EXPR)
is used. However, both of the above silence errors, so the following would be better:
my $find = 'start (.*) end';
my $replace = 'foo $1 bar';
my $var = "start middle end";
$var =~ s/$find/ my $r = eval($replace); die $@ if $@; $r /e;
But as you can see, all of the above allow for the execution of arbitrary Perl code. The following would be far safer:
use String::Substitution qw( sub_modify );
my $find = 'start (.*) end';
my $replace = 'foo $1 bar';
my $var = "start middle end";
sub_modify($var, $find, $replace);