Wexxor
Wexxor

Reputation: 1909

Need Perl inplace editing of files not on command line

I have a program that has a number of filenames configured internally. The program edits a bunch of configuration files associated with a database account, and then changes the database password for the database account.

The list of configuration files is associated with the name of the database account via an internal list. When I process these files, I have the following loop in my program:

BEGIN { $^I = '.oldPW'; }  # Enable in-place editing
...
foreach (@{$Services{$request}{'files'}})
{
    my $filename = $Services{$request}{'configDir'} . '/' . $_;
    print "Processing ${filename}\n";
    open CONFIGFILE, '+<', $filename or warn $!;
    while (<CONFIGFILE>)
    {
        s/$oldPass/$newPass/;
        print;
    }
    close CONFIGFILE;
}

The problem is, this writes the modified output to STDOUT, not CONFIGFILE. How do I get this to actually edit inplace? Move the $^I inside the loop? Print CONFIGFILE? I'm stumped.

>

Update: I found what I was looking for on PerlMonks. You can use a local ARGV inside the loop to do inplace editing in the normal Perl way. The above loop now looks like:

foreach (@{$Services{$request}{'files'}})
{
    my $filename = $Services{$request}{'configDir'} . '/' . $_;
    print "Processing ${filename}\n";
    {
        local @ARGV = ( $filename);
        while (<>)
        {
            s/$oldPass/$newPass/;
            print;
        }
    }
}

If it weren't for tacking the configDir on the beginning, I could just toss the whole list into the local @ARGV, but this is efficient enough.

Thanks for the helpful suggestions on Tie::File. I'd probably go that way if doing this over. The configuration files I'm editing are never more than a few  KB in length, so a Tie wouldn't use too much memory.

Upvotes: 15

Views: 5418

Answers (3)

DavidO
DavidO

Reputation: 13942

Tie::File has already been mentioned, and is very simple. Avoiding the -i switch is probably a good idea for non-command-line scripts. If you're looking to avoid Tie::File, the standard solution is this:

  • Open a file for input
  • Open a temp file for output
  • Read a line from input file.
  • Modify the line in whatever way you like.
  • Write the new line out to your temp file.
  • Loop to next line, etc.
  • Close input and output files.
  • Rename input file to some backup name, such as appending .bak to filename.
  • Rename temporary output file to original input filename.

This is essentially what goes on behind the scenes with the -i.bak switch anyway, but with added flexibility.

Upvotes: 2

Grrrr
Grrrr

Reputation: 2536

The recent versions of File::Slurp provide convenient functions, edit_file and edit_file_lines. The inner part of your code would look:

use File::Slurp qw(edit_file);
edit_file { s/$oldPass/$newPass/g } $filename;

Upvotes: 16

mob
mob

Reputation: 118605

The $^I variable only operates on the sequence of filenames held in $ARGV using the empty <> construction. Maybe something like this would work:

BEGIN { $^I = '.oldPW'; }  # Enable in-place editing
...

local @ARGV = map {
    $Services{$request}{'configDir'} . '/' . $_ 
} @{$Services{$request}{'files'}};
while (<>) {
   s/$oldPass/$newPass/;

   # print? print ARGVOUT? I don't remember
   print ARGVOUT;
}

but if it's not a simple script and you need @ARGV and STDOUT for other purposes, you're probably better off using something like Tie::File for this task:

use Tie::File;
foreach (@{$Services{$request}{'files'}})
{
    my $filename = $Services{$request}{'configDir'} . '/' . $_;

    # make the backup yourself
    system("cp $filename $filename.oldPW");   # also consider File::Copy

    my @array;
    tie @array, 'Tie::File', $filename;

    # now edit @array
    s/$oldPass/$newPass/ for @array;

    # untie to trigger rewriting the file
    untie @array;
}

Upvotes: 11

Related Questions