Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?

Re-runnably editing a file in place

by eyepopslikeamosquito (Archbishop)
on May 22, 2003 at 09:06 UTC ( [id://260011] : perlquestion . print w/replies, xml ) Need Help??

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

I want to edit a file "in place", preserving the file's permissions, yet be safely re-runnable should an interruption occur at any time. Though the script does not need to be multi-user safe, I am vaguely interested in how that might be achieved (I guess with file locking). I know about Perl's -i switch, but this switch first renames the file, so if the user CTRL-C's (or power is lost) after the rename but before the new file is written, the script is not re-runnable because the original file has been renamed (or may not be complete). Is that right? Anyway, here is my attempt at solving this problem.

# Slurp file $fname into array @lines. open(my $fh, $fname) or die "open '$fname': $!"; my @lines = <$fh>; close($fh); # Mangle @lines as appropriate ... if ($file_contents_changed) { my $bak = $fname . $$ . '.tmp'; open(my $fh, '>'.$bak) or die "create $bak: $!"; print $fh @lines or die "writing $bak: $!"; close($fh) or die "close $bak: $!"; defined(my $mode = (stat($fname))[2]) or die "stat $fname: $!"; -w _ or (chmod($mode|0200, $fname) or die "chmod $fname: $!"); rename($bak, $fname) or die "rename $bak $fname: $!"; chmod($mode, $fname) or die "chmod $fname: $!"; }

This has at least one flaw. In the case of a read-only file, if you are interrupted after chmod($mode|0200) but before chmod($mode), the file $fname will have the wrong permissions on re-run. Improvements welcome.

References Added Later

Replies are listed 'Best First'.
Re: Re-runnably editing a file in place
by broquaint (Abbot) on May 22, 2003 at 09:47 UTC
    Sounds like you want transactions for your files, and rather conveniently, this is just what File::Transaction provides e.g
    use File::Transaction; my $ft = File::Transaction->new; $ft->linewise_rewrite($fname); ## do stuff if($file_contents_changed) { $ft->commit } else { $ft->revert }
    See. the File::Transaction docs for more info.


•Re: Re-runnably editing a file in place
by merlyn (Sage) on May 22, 2003 at 10:53 UTC

      The script is to run on Windows and various Unix flavours. I suppose I should test $^O to eliminate the unnecessary step on the Unices.

      Update. Thanks to merlyn, I have improved my first attempt. On Unix, don't chmod a read-only file because renaming to a read-only file works fine there -- permissions on *directory* (not file) control rename (and delete) on Unix. Permissions now changed on temporary file, not original; this makes the rename the last (atomic) action, making the code 100% rerunnable on Unix, I think. On Windows, there is an extremely low chance of trouble: the rename is right after the chmod(...0200) so the worst that can happen is a read-only file may become writable.

      if ($file_contents_changed) { my $bak = $fname . $$ . '.tmp'; -e $bak and (unlink($bak) or die "unlink $bak: $!"); open(my $fh, '>'.$bak) or die "create $bak: $!"; print $fh @lines or die "writing $bak: $!"; close($fh) or die "close $bak: $!"; defined(my $mode = (stat($fname))[2]) or die "stat $fname: $!"; chmod($mode, $bak) or die "chmod $bak: $!"; $^O eq 'MSWin32' && ! -w _ and (chmod($mode|0200, $fname) or die "chmod $fname: $!"); rename($bak, $fname) or die "rename $bak $fname: $!"; }
        Oh, so that is a Windows restriction? You're kidding. You can't rename a file you can't read?

        Every time I hear about another way that Windows makes it hard to be a programmer, I thank the maker that I can say no to contracts that have Windows in the specification.

        -- Randal L. Schwartz, Perl hacker
        Be sure to read my standard disclaimer if this is a reply.

Re: Re-runnably editing a file in place
by Bilbo (Pilgrim) on May 22, 2003 at 09:54 UTC

    I think that the most reliable sequence would be:

    1. Edit the file, reading from the original, writing to the temporary file
    2. Set the permissions on the temporary file to those of the original file
    3. Rename the temporary file to the original filename.

    Unless there is an interuption during the final renaming of the file your file should be in a sensible state whatever happens.

    Update: Sorry, that's what you're doing already, with the necessary step of making the file readable to move it. I'm not very awake at the moment. Just ignore me.

    I'm not sure if there would be any advantage in using the move function in File::Copy rather than rename. Is it more portable or reliable?