Here's some example code on how to do this with a few tests. I'd invite adding some more tests and any bug reports/security issues as I've not really thought about this from a security perspective yet. It's a bit slower in that it matches twice, but it completely avoids any eval.
use strict;
use warnings;
sub substitute
{
my ($string, $from, $to) = @_;
$from = qr/$from/ unless ref $from and ref $from eq 'Regexp';
my @a = $string =~ $from;
$to =~ s/\$(\d+)/$a[$1-1]/g; # was $to =~ s/\$(\d+)/\Q$a[$1-1]/g;
$string =~ s/$from/$to/;
$string;
}
my @tests = (
[ "this is some test", "(is) s(o)me", '$1 n$2t a' ],
[ "this is some test", "is some", 'is not a' ],
);
for my $t (@tests)
{
print "[$t->[0]]...";
print "[",substitute(@$t), "]\n";
}
prints out:
[this is some test]...[this is not a test]
[this is some test]...[this is not a test]
which is what I expected. But, as you can see, it's not a very extensive test, so feel free to try a few more.
Update: It turns out that the \Q in the $to replacement wasn't needed.