Perl in-place editing produces garbage

155 views Asked by At

I am having trouble with in-place file editing, having browsed the web for a couple of hours without results.

I really don't want to use the general temporary file scheme, i.e. writing everything to a new file and replace the old one. I need modification timestamps to reflect actual changes, permissions and ownership to remain unchanged etc.

If I understand correctly, using $I^ is just a short-hand for the temp-file scheeme - or am I wrong?

The "+<" mode should open the file for both reading and writing.

My test code so far:

#!/usr/bin/perl
use strict;
use warnings;

open(FILE, "+<", "testingfile") or die "$!";

while (<FILE>) {
    print;
    s/world/WORLD/;
    print FILE $_;
    print;
}

The "testingfile" has three lines, and I just want to replace "world" with "WORLD" for now:

hello
world
foo

Result

When I run the Perl script, garbage is produced and the terminal is left hanging until interrupted (Ctrl+C):

hello
hello
foo
foo
o
o
llo
llo
ÈÈ'>jËNgs}>¾ØKeh%P8*   *        +       +      p+      ÑÑÀ+    +       p+      p+      ¨° #!/u8in/puse ct;
ÈÈ'>jËNgs}>¾ØKeh%P8*   *        +       +      p+      ÑÑÀ+    +       p+      p+      ¨° #!/u8in/puse ct;

The "testingfile" now contains:

hello
world
foo
hello
hello
foo

I'm running an old Perl on a SunOS (Solaris) production system:

This is perl, v5.8.4 built for i86pc-solaris-64int
4

There are 4 answers

1
wolfhammer On BEST ANSWER
#!/usr/bin/perl
use strict;
use warnings;

# getlines
open(FILE, "<", "testingfile") or die "$!";
my @lines = <FILE>;
my $line = "";
close(FILE);

# open file again but this time for writing
open(FILE, ">", "testingfile") or die "$!";

# write adjusted text
foreach $line (@lines) {
    $line =~s/world/WORLD/;
    print FILE "$line";
    print "$line";
}
0
Borodin On

The most straightforward way is to use Tie::File, which allows you to edit a text file by simply modifying an array. It does have the reputation of being slow, but you will know if it is too slow only by trying it yourself

Your example code would become just this

#!/usr/bin/perl
use strict;
use warnings;

use Tie::File;

tie my @file, 'Tie::File', 'testingfile' or die $!;

s/world/WORLD/ for @file;

untie @file;
0
brian d foy On

The in-place editing doesn't do what you are doing. It renames the original file then opens a new file with the original name. It reads from the renamed file and writes to the original filename. See perlrun for the explanation of -I.

1
mob On

You need to learn about the seek command to move around in your file. Your filehandle FILE has a single cursor. After you read from FILE, its cursor is pointing at the end of the data you just read. Then you write on FILE and you are not overwriting the data you just read, but the data your were just about to read.

Here is your file. When you first open it, the cursor is at the beginning of the file.

 h e l l o \n w o r l d \n f o o \n EOF
^

Next you read a line of input with the <FILE> operation. That loads the text "hello\n" into the variable $_ and moves FILE's cursor:

 h e l l o \n w o r l d \n f o o \n EOF
             ^

Next, your substitution fails and doesn't alter $_, and you print the contents of $_ to FILE. The writing begins at the cursor and you get

 h e l l o \n h e l l o \n f o o \n EOF
                          ^

The next time you read, you get foo\n in $_, move the cursor to the end of the file, and then rewrite $_ at the end of the file.

 h e l l o \n h e l l o \n f o o \n f o o \n EOF
                                            ^

Use the seek command to move the cursor around. Maybe something like

open(FILE, "+<", "testingfile") or die "$!";

while (<FILE>) {
    print;
    if (s/world/WORLD/) {
        seek FILE, -length($_), 1;   # move back by length of $_
        print FILE $_;
    }
    print;
}

As @Borodin points out, this gets a lot more complicated if you want to lengthen or shorten $_ as you move through the file.