It seems you want to load language-agnostic search and replace patterns from a configuration file, and then apply them via a Perl script.
If that is your goal, then using eval is not appropriate since Perl has syntax that you do not want to support, as you found out.
It is not reasonable to try to work around those Perl-specific parts by trying to escape them, since that can get rather complex. For example, you considered escaping occurrences of @ as they can introduce an array name, but what if that character already is backslash-escaped? Handling this properly would require an almost complete re-implementation of Perl's string literal syntax, which doesn't sound like fun.
What I would do is to define a replacement string syntax of our own, so that we're completely independent from Perl's syntax.
For example, we might define our replacement string syntax to be entirely verbatim, except that we support certain backslash-escapes. Let's say that the syntax '\' DIGIT such as \1 replaces a capture, and that the usual backlash escapes are supported (\b \t \n \v \f \r \" \' \\ \x0A), which is the common subset of JavaScript string literals, Python 3 string literals, and Perl escapes, minus octal escapes. Note that these languages do not agree on a syntax for Unicode characters.
We can implement an interpreter for this string replacement language as follows: we parse the replacement string into an array of opcodes, alternating a literal string with the number of a capture. For example, the replacement pattern abc\1def would be parsed into ['abc', 1, 'def']:
sub parse_replacement_pattern {
my ($pattern) = @_;
my @ops = (''); # init with empty string
# use m//gc style parsing which lets us anchor patterns at the current "pos"
pos($pattern) = 0;
while (pos $pattern < length $pattern) {
if ($pattern =~ /\G([^\\]+)/gc) {
$ops[-1] .= $1;
}
elsif ($pattern =~ /\G\\n/gc) {
$ops[-1] .= "\n";
}
... # and so on for the basic escapes
elsif ($pattern =~ /\G\\x([0-9a-fA-F]{2})/gc) {
$ops[-1] .= chr $1;
}
elsif ($pattern =~ /\G\\([1-9])/gc) {
push @ops, $1, ''; # add replacement opcode + empty string
}
else {
die "invalid syntax";
}
}
return \@ops;
}
We can apply such a replacement pattern by looping through the operations, appending the literal string or the capture contents as appropriate.
sub apply_replacement_pattern {
my ($ops) = @_;
my $output = '';
my $is_capture = 0;
for my $op (@$ops) {
if ($is_capture) {
# we know that $op must be the number of a capture buffer
$output .= ${^CAPTURE}[$op - 1]; # like eval "\$$op"
}
else {
# we know that $op must be a literal string
$output .= $op;
}
$is_capture = !$is_capture;
}
return $output;
}
We can now use these functions in your test case:
my $text_str ='public class ZipUtilTest extends TestCase {}';
my $find = '^(public class \\w+) extends TestCase \\{';
my $replace = '@RunWith(JUnit4.class)\\n\\1 {';
my $replace_ops = parse_replacement_pattern($replace);
$text_str =~ s{$find}{apply_replacement_pattern($replace_ops)}mge;
say $text_str;
This produces the expected output
@RunWith(JUnit4.class)
public class ZipUtilTest {}
TL;DR\@RunWith(JUnit4.class)\\n\\1\n, but not variables? Are those replacement strings trusted?