http://www.perlmonks.org?node_id=887026

Anonyrnous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Occasionally, I get the opportunity to impress collegues with an elegant one-liner in our beloved language. This time, though, the advocacy didn't work out too well so far... ;)

The task is to go through a number of config files and fix up a certain entry if it exists, or else (if the entry doesn't exist) append it to one of the files (last file processed is fine, but so is any other).

It's important the entry does not exist in multiple files as a result of the fixing, and it's preferable to leave it where it was, in case it was found. It can be assumed the entry exists exactly only one or zero times in the original input files. Also, files should be edited in place.

Somewhat simplified:

Case 1 — entry "opt=foo" already exists, fix it with s/opt=.*/opt=bar/:

input: desired output: file1: file1: line1 line1 opt=foo opt=bar line3 line3 file2: file2: line1 line1 line2 line2 line3 line3

Case 2 — entry doesn't exist, append "opt=bar" to last file:

input: desired output: file1: file1: line1 line1 line2 line2 line3 line3 file2: file2: line1 line1 line2 line2 line3 line3 opt=bar

The first idea was to use an END block to conditionally output the last line

$ perl -i -pe '$f||=s/opt=.*/opt=bar/; END { print "opt=bar\n" if !$f +}' file1 file2

This doesn't work, however, because the print goes to stdout, instead of the file being edited in-place. (print ARGV ... doesn't work either; $! says "Bad file descriptor", presumably because ARGV is already closed at that point.)

So, next idea was to use eof to be able to move the test into the the loop implied by -p.  However, while this in principle works fine with eof (which is true at the end of every file), it doesn't work with eof(), which should be true only once after all files have been processed — which is what would really be needed to avoid multiple insertions of the entry in question.

Test script:

#!/bin/bash #echo -e "line1\nopt=foo\nline3" >file1 # case 1 echo -e "line1\nline2\nline3" >file1 # case 2 echo -e "line1\nline2\nline3" >file2 echo --- cat before --- cat file1 file2 echo --- perl output to stdout --- perl -i -pe '$f||=s/opt=.*/opt=bar/; $_.="opt=bar\n" if eof() && !$f' +file1 file2 echo --- cat after --- cat file1 file2

Output:

--- cat before --- line1 line2 line3 line1 line2 line3 --- perl output to stdout --- line3 opt=bar --- cat after --- line1 line2 line3 line1 line2

Desired output:

--- cat before --- line1 line2 line3 line1 line2 line3 --- perl output to stdout --- --- cat after --- line1 line2 line3 line1 line2 line3 opt=bar

Interestingly, not even case 1 (where the eof() wouldn't really be needed) works. Here, the last line ("line3") is also being written to stdout instead of to the file, in case eof() is being tested...

Can anyone explain what's going on here?  Is this behavior documented somewhere?  (Of course, there are many more or less clumsy ways to solve this another way, but I'm primarily interested in why this approach doesn't do what I'd expected.)

Update: Solution: in cases like these, replace eof() with !@ARGV && eof, which doesn't have the undesirable side effect...