user1289 user1289 - 1 month ago 3x
Perl Question

Perl replacement operator doesn't work under Windows when patterns contain slashes

I want to replace a string with a path:

my $somedir = "D:/somedir/someotherdir";
system("perl -pi.bak -e \"s{STRING_TO_BE_REPLACED}{$somedir}\" $file");

but under Windows it replaces string with random symbols instead of slashes.
What's the problem?


Update   Replaced the map version with recommendation to either change the array in place and be aware of what is going on, or to copy to new array. Fixed the explanation of the in-place change. Added code and comments.

I think it's got something to do with a syntax detail needed on Windows, but can't test now.

However, as you are in a Perl script, why go out with system and run another Perl interpreter? It is far more complex since it involves a syscall or a shell, and starts another program (Perl). It is inefficient. Also, it is far harder to get it right -- you need to deal with syntax details, quoting and escaping, for system, your system's command interpreter, and the other instance of Perl.

The code below reads the whole file into an array first, which is fine for small enough files. In general it is better to process a file line by line, and how to do what you need in that way is discussed in detail in a perlfaq5 page. See the comment at the end, with the link.

use warnings 'all';
use strict;

# your code ...

open my $fh, '<', $file  or die "Can't open $file: $!";
my @lines = <$fh>;

# Change @lines in-place. See the comment
s/STRING_TO_BE_REPLACED/$somedir/ for @lines;  

open $fh, '>', $file  or die "Can't open $file for write: $!";
print $fh @lines;
close $fh;

When we open the $fh the second time it is closed and re-opened, so there is no need for an explicit close. When an existing file is opened for writing (>) it is clobbered, so this replaces it.

It's more to write but it is better.

Comment on the in-place change to @lines   This uses the fact that when iterating over an array if we change the index variable, here $_, the change is made in the original element. The index variable is like an alias for the array element. It says in perlsyn

If any element of LIST is an lvalue, you can modify it by modifying VAR inside the loop. Conversely, if any element of LIST is NOT an lvalue, any attempt to modify that element will fail. In other words, the foreach loop index variable is an implicit alias for each item in the list that you're looping over.

This has the benefit of not copying data and not touching elements that don't change so it is more efficient, potentially a lot more. However, it relies on a subtle property and thus it may be tricky and error prone, so I do not recommend it as a general practice.

To copy the array, with modifications, to a new one

my @lines_new;
foreach my $line (@lines) { 
    $line =~ s{STRING_TO_BE_REPLACED}{$somedir};
    push @lines_new, $line;

This also changes @lines. If it need be kept intact do (my $new_line = $line) =~ s/.../. Then write @lines_new to $file. Somewhere in between these two is

@lines = map { s{STRING_TO_BE_REPLACED}{$somedir}; $_ } @lines;

what was posted originally. However, since the map changes elements of @lines and copies data to build the output list, while the whole statement also overwrites the array, on reflection I think it makes more sense to do either the in-place change or to copy to a new array.

In principle it is better to not read the whole file at once but rather to process line by line, unless the file is small enough. In that case open the file for reading and new one for writing, and after you copy (with changes) the file over, move the new one to rewrite $file. See the topic in perlfaq5

To move the file use move from the core module File::Copy. The new file is temporary, to be used to overwrite $file, so it can be named using the core module File::Temp to avoid accidents.