Search code examples
perlinplace-editing

Read entire file then print when editing inplace?


Most examples of inplace editing are one-liners that iterate through a file or files, reading and printing one line at a time.

I can't find any examples of reading an entire file into an array, modifying the array as needed, and then printing the array while using the ^I switch to do an inplace edit. When I try to read the entire file from the diamond operator, edit the contents and print the entire contents, I find that the print goes to STDOUT instead of ARGVOUT and that ARGVOUT is closed. I can open the same file for output and then print to it, but I'm not sure I understand why that is necessary. Here is an example:

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

my $filename = 'test.txt';

push @ARGV, $filename;

$^I = ".bk";

my @file = <>; #Read all records into array
chomp @file;
push @file, qw(add a few more lines);

print join "\n", @file; #This prints to STDOUT, and ARGVOUT is closed. Why?

Running the above makes a backup of the test.txt file as expected, but leaves the edited test.txt empty, printing the edited contents to STDOUT instead.


Solution

  • See perlrun.

    When the -i switch has been invoked, perl starts the program using ARGVOUT as the default file handle instead of STDOUT. If there are multiple input files, then every time the <> or <ARGV> or readline(ARGV) operation finishes with one of the input files, it closes ARGVOUT and reopens it to write to the next output file name.

    Once all the input from <> is exhausted (when there are no more files to process), perl closes ARGVOUT and restores STDOUT as the default file handle again. Or as perlrun says

    #!/usr/bin/perl -pi.orig
    s/foo/bar/;
    

    is equivalent to

    #!/usr/bin/perl
    $extension = '.orig';
    LINE: while (<>) {
        if ($ARGV ne $oldargv) {
            if ($extension !~ /\*/) {
                $backup = $ARGV . $extension;
            }
            else {
                ($backup = $extension) =~ s/\*/$ARGV/g;
            }
            rename($ARGV, $backup);
            open(ARGVOUT, ">$ARGV");
            select(ARGVOUT);
            $oldargv = $ARGV;
        }
        s/foo/bar/;
    }
    continue {
        print;  # this prints to original filename
    }
    select(STDOUT);
    

    Once you say my @file = <> and consume all the input, Perl closes the filehandle to the backup files and starts directing output to STDOUT again.


    The workaround, I think, is to call <> in scalar context and check eof(ARGV) after each line. When eof(ARGV)=1, you have read the last line in that file and you get one chance to print before you call <> again:

    my @file = ();
    while (<>) {
        push @file, $_;
        if (eof(ARGV)) {
            # done reading current file
            @processed_file = &do_something_with(@file);
            # last chance to print before ARGVOUT gets reset
            print @processed_file;
            @file = ();
        }
    }