I agree with hippo that this seems to be moving into XY Problem territory. What about /a/b/c/\$USER? Or /a/b/c/\\$USER? Or malformed inputs like /a/b/c/${USER? And what other possible inputs haven't you told us about?
Enough rope to shoot yourself in the foot:
use warnings;
use strict;
sub interpolate {
local $_ = shift;
my $vars = shift;
my @out;
pos=undef;
while (1) {
if ( m{\G ( [^\\\$]+ ) }xmsgc ) { push @out, $1 }
elsif ( m{\G \$ (?| (\w+) | \{(\w+)\} ) }xmsgc )
{ push @out, $vars->{$1} }
elsif ( m{\G \\ (.) }xmsgc ) {
$1 eq "\\" or $1 eq "\$"
or die "unexpected backslash escape";
push @out, $1;
}
else { last }
}
die "parse of '$_' failed at pos ".(pos//0)
if !defined pos || pos!=length;
return join '', @out;
}
use Test::More;
sub exception (&) { eval { shift->(); 1 } ? undef : ($@ || die) }
my %env = ( USER => "foobar", quz => "\$baz \\\\ \\\$" );
is interpolate("USER", \%env), "USER";
like exception { interpolate("\\USER"), \%env },
qr/\bunexpected backslash\b/;
is interpolate("\\\\USER", \%env), "\\USER";
is interpolate("\$USER", \%env), "foobar";
is interpolate("\${USER}", \%env), "foobar";
is interpolate("\\\$USER", \%env), "\$USER";
is interpolate("\\\\\$USER", \%env), "\\foobar";
is interpolate("\\\\\\\$USER", \%env), "\\\$USER";
is interpolate("\\\\\\\\\$USER", \%env), "\\\\foobar";
is interpolate("\\\\\\\\\${USER}", \%env), "\\\\foobar";
is interpolate("\$quz", \%env), "\$baz \\\\ \\\$";
like exception { interpolate("USER\$"), \%env },
qr/\bparse of 'USER\$' failed\b/;
like exception { interpolate("\${USER"), \%env },
qr/\bparse of '\$\{USER' failed\b/;
done_testing;
I wouldn't necessarily recommend doing much more (e.g. String::Interpolate), as otherwise you may end up opening a security hole.
Update: I realized that the code I posted originally (below) would also unescape backslashes and $s in the values of variables interpolated into the string, so I rewrote the parser using m/\G/gc regexes above. It should now also be more roboust in the face of invalid inputs. The only minor downside is that now integrating String::Unescape isn't quite as easy.
Original code:
use warnings;
use strict;
use Test::More;
sub exception (&) { eval { shift->(); 1 } ? undef : ($@ || die) }
my %_interp_map = ( '\\'=>'\\', '$'=>'$' ); # or use String::Unescape
sub interpolate {
my $str = shift;
$str =~ s{ (?<!\\) ((?:\\\\)*) \$ (?| (\w+) | \{(\w+)\} ) }
{ $1.$ENV{$2} }xeg;
$str =~ s{ \\ (.) }
{ $_interp_map{$1} || die "unexpected backslash escape" }xeg;
return $str;
}
local $ENV{USER} = "foobar";
is interpolate("USER"), "USER";
like exception { interpolate("\\USER") }, qr/unexpected backslash/i;
is interpolate("\\\\USER"), "\\USER";
is interpolate("\$USER"), "foobar";
is interpolate("\${USER}"), "foobar";
is interpolate("\\\$USER"), "\$USER";
is interpolate("\\\\\$USER"), "\\foobar";
is interpolate("\\\\\\\$USER"), "\\\$USER";
is interpolate("\\\\\\\\\$USER"), "\\\\foobar";
is interpolate("\\\\\\\\\${USER}"), "\\\\foobar";
done_testing;