-2

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?

user1289
  • 1,251
  • 2
  • 13
  • 25
  • 1
    This code won't compile. There's a `"` missing in the first line. Compile your code before you paste it here (yes, I sometimes forget to do that, too). – reinierpost Sep 18 '16 at 09:44
  • 5
    Put yourself in our shoes. You see code that can't possibly be the code the question is about. How many more differences will there turn out to be? How can you be sure the problem isn't somewhere else entirely? If you'd spent a lot of time answering questions on SO you'd realize how important this is. – reinierpost Sep 18 '16 at 10:15
  • I'm done :) see you later ;) – user1289 Sep 23 '16 at 19:10
  • @user1289 Cleaned up mine as well, thanks :) – zdim Sep 23 '16 at 19:13

1 Answers1

2

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 and inefficient since it involves a syscall or a shell, and starts another program. 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, the other instance of Perl, and the regex.

The code below reads the whole file into an array first, which is fine if files aren't huge. 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 an explicit 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

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

zdim
  • 64,580
  • 5
  • 52
  • 81
  • Thanks, I'll try with this one. Yes for sure it's better than calling system, but I remember I couldn't make work such function on windows, probably I did something wrong. – user1289 Sep 18 '16 at 09:43
  • @user1289 Alright. This _has to_ work. It does only the very basic things -- opens a file, runs a simple regex, writes a file. If anything is failing it has to be a simple error -- rewrite the code to more basic version, step at a time - explicit loop with two arrays instead of `map`, two files and `File::Copy::move` etc, to catch the error. Running the `system`, in particular on Windows, is far trickier. Let me know if there is a problem. – zdim Sep 18 '16 at 09:47
  • 4
    You're in a Perl program, you have a piece of Perl code you want to execute. If you just make it a line of your program, Perl will interpret it and check it for errors ([make sure it does!](http://stackoverflow.com/questions/8023959/why-use-strict-and-warnings)). Instead, you construct a call to `perl` and feed that to your command line interpreter (`cmd`? `bash`? something else?). Suddenly you have 3 interpreters working on that code, all doing their variable interpolations, quote interpretation, escaping, etcetera. That is much harder to get right. – reinierpost Sep 18 '16 at 09:51
  • @user1289 Great :) Let me know if anything pops up or if more or better explanation(s) would be useful. – zdim Sep 18 '16 at 22:34
  • @user1289 I've added to the post considerably, it may be a good idea to read through it. (The original `map` method is correct.) – zdim Sep 19 '16 at 02:49