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

Hello
I'm having trouble coming up with code to replace one unique string in a multi-line text file. The goal: I have a text file of orders separated by lines. When I run &ReplaceStatus I want the status of the order $order in this text file to change from "Pending" to "Delivered". Here's what I have so far:

sub ReplaceStatus {

$ORDERS="../../../orders.txt";

$order = $in{'order'};
$new_order_status = "Delivered";

open (FILE, "<$ORDERS");
open (WRBK, ">>$ORDERS");

while (<FILE>) {
chomp;
my @vals = split "\t";
$status = $vals19;

if ($vals[0] == "$order") {
$vals19 = $new_order_status;
print WRBK $vals19;
last;

}

close(WRBK);

}
}

The file its calling is a tab delimited text where each line is an individual order. The last tab is the "status" string which I need to change. Right now all this code is doing is adding a new line at the bottom of the text file with the string "Delivered"

Thanks in advance!
  • Comment on replacing string in multi-line text file

Replies are listed 'Best First'.
Re: replacing string in multi-line text file
by davido (Cardinal) on Mar 25, 2011 at 08:08 UTC

    One of the problems you will run into is that there is no good way to insert a change into a text file without clobbering something. You can append. You can seek and write. But your records aren't fixed length, so just about any change you make will alter the number of bytes in a given record. This makes in-place editing tricky.

    You have a few options. Some of them scale better than others. One option would be to read the file line by line, and simultaneously write a new file line by line, along with the changes you intend to make. After the files are closed, you can delete the original and rename the newly created file. That's a very common way of working with flat files. But it doesn't scale well. The bigger your file gets, the more work has to be done just to enact one change.

    Another solution is to make each record in the file uniform length, and each field uniform length. That would allow you to treat the file more like a database. You could literally make a change at any point within the file, and as long as that change doesn't change the field size you're ok. But the problem with that is that you're basically implementing a database from scratch. And if you're going to do that, just go all the way and use something like SQLite.

    There are a few other alternatives. You could tie your file to an array with Tie::File. That module allows for in-place editing where the dirty work of rewriting the file upon each edit is hidden behind a simple interface. This method suffers from scalability problems just like the first method, but ends up being quite simple to implement.

    Here are examples of the line-by-line with a temp file approach, and of the Tie::File approach:

    use Modern::Perl; open my $infh, '<', 'orders.txt'; open my $outfh, '>', 'orders.txt.tmp'; while( <$infh> ) { chomp; my @vals = split "\t"; my $status = $vals[19]; if( $vals[0] = 'trigger text' ) { $vals[19] = 'Delivered'; } print $outfh join("\t",@vals), "\n"; } close $infh; close $outfh; rename "orders.txt", "orders.txt.bak"; rename "orders.txt.tmp", "orders.txt";

    Now for the Tie::File method:

    use strict; use warnings; use Modern::Perl use Tie::File; tie my @lines, 'Tie::File', 'orders.txt' or die $!; foreach my $line ( @lines ) { chomp $line; my @vals = split "\t", $line; if( $vals[0] == 'trigger text' ) { $vals[19] = 'Delivered'; } $line = join( "\t", @vals ) . "\n"; } untie @lines;

    Either of these methods should work fine (though you would have to adapt them to your specific situation). It's up to you which you consider easier. But if your file looks like it's going to grow bigger and bigger, you may seriously consider converting over to a database such as SQLite.


    Dave

      Thank you for replying.

      My webhost is erroring out on this line:
      use Modern::Perl;

      Ideas?

        For starters, use strict, use warnings, change your file opens to the canonical "or die" style, and convert 'say' to 'print' with a backslash-n at the end. With respect to the example I provided, making those changes will enable you to remove the 'use Modern::Perl;' line, with approximately the same intended outcome.


        Dave

        Lol, resolve the mysterious error
Re: replacing string in multi-line text file
by Nikhil Jain (Monk) on Mar 25, 2011 at 07:46 UTC

    1. First of all use three argument open with exception handling like

    open(my $fh, '<', "input.txt") or die $!;

    2. secondly, your code is adding a new line at the bottom of the text file because you are opening a file in append mode ie., >>

    use '+<' is almost always preferred for read/write updates.

    For more detail see http://perldoc.perl.org/functions/open.html

Re: replacing string in multi-line text file
by apl (Monsignor) on Mar 25, 2011 at 10:49 UTC
    From a purely religious point of view, you don't want to update the source file. Rather, you want to create a new file that reflects the changes to the original file. Upon successful completion, you can then rename the original file to something else, and the new file to the original name.

    That way, you can always fall back to the source input file and avoid disaster.