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

Hello everyone,

Hope you are all well. I'm relatively new to perl and I've been reading a not very useful book for a few weeks now. I have just finished learning "writing and reading files".

At the end of this chapter there are exercises and one of them is to insert a line in the middle of a file but the script I wrote does nothing.

The file I have looks like this:

Jonathan Peter Jason Nick Stela Samantha

And I want to insert the name Simon below Jason and above Nick without overwriting any of the existing names.

Jonathan Peter Jason Simon Nick Stela Samantha

The script I wrote is below but it doesn't print anything in the file and it doesn't give me any warnings or errors.

my $file = '/home/yanni/scripts/testfiles/list_names'; open my $NAMES, '+>>', $file || die "What the hell? Can't open file $! +\n"; while (<$NAMES>) { if ($.== 3) {print "\nSimon\n";} } close ($NAMES);

First, I open the file for reading and appending (+>>), then with the while command I read the file line by line (at least this is what it says in the book). Inside the while loop I "state" that when you read line 3 print the name Simon.

What am I doing wrong?

Any suggestions please?
Thanks a lot

Replies are listed 'Best First'.
Re: Inserting a line in the middle of a text
by cdarke (Prior) on Jan 20, 2009 at 12:02 UTC
    Where to start. First, what you are trying to do is invalid. Unless you have fixed length records, it is not possible to insert a record into the middle of a text file. That is the case regardless of the language. The best you can do is to read a file then write out to a different one, inserting the new record in the correct place in the new file.

    Opening a file for append and read will position the current file position to end-of-file, so you will not read anything.

    The open statement requires parentheses around the argument list when you use the high precendent || operator. Or use the low precedence or.

    print on its own writes to STDOUT, not to a file.

    By convention, it is not a good idea to use UPPERCASE for your own variable names, like $NAMES.

    This may get you what you want:
    #!/usr/bin/perl use strict; use warnings; my $infile = '/home/yanni/scripts/testfiles/list_names'; my $outfile = "$infile.tmp"; open (my $in, '<', $infile ) || die "Can't open $infile $!\n"; open (my $out, '>', $outfile) || die "Can't open $outfile $!\n"; while (<$in>) { print $out $_; if ($.== 3) {print $out "Simon\n";} } close ($in); close ($out); rename ($outfile, $infile) || die "Unable to rename: $!";
    Update: On second thought, the record length has nothing to do with it if you want to insert (I was thinking of an overwrite). You would still have to read the records in first somehow.
      Where to start. First, what you are trying to do is invalid. Unless you have fixed length records, it is not possible to insert a record into the middle of a text file. That is the case regardless of the language.
      You are right that it's not language dependent. It's file system dependent. Most Unix and Windows filesystems where files are just streams of bytes - and there it's not possible to insert records without copying sections of the file. But there are filesystems where files are lists of records - files look a bit like row-based database tables. IIRC, VMS has the ability to use such filesystems.

      But the above is just some pedantery, chances the OP is using such a file system are tiny.

      Thank you very much cdarke for explaining my mistakes.

      Your script works perfectly.

        You start the thread by saying:

        I've been reading a not very useful book for a few weeks now.

        and it strikes me that the number of problems in the OP script might be an indicator of just how "not very useful" that book really is. Have you checked out the Reviews section here at the Monastery? Is your book already reviewed there? (If not, perhaps you could take your revenge on the "not very useful" author by contributing a review of the book.)

        Oh, and maybe you can find a better book... (maybe our Book Reviews nook will help you find one). Personally, my own favorite reading has always been the documentation that comes with perl -- starting perhaps with perl.

Re: Inserting a line in the middle of a text
by swampyankee (Parson) on Jan 20, 2009 at 12:20 UTC

    Well, the first question is "How would you do this in any arbitrary computer language?" The second is "How do you expect to insert a record into a file without writing into it?"

    There are numerous ways of doing this in Perl: localize $/, and slurp the file in as one chunk, and read the file into an array, and read the file a line at a time, and either write the modified file as a temp file and use rename to overwrite the old file or use the -i option to edit it in place. You could also use Tie::File, although that seems to some of the aesthetics of using a sledgehammer to kill a gnat.


    Information about American English usage here and here. Floating point issues? Please read this before posting. — emc

Re: Inserting a line in the middle of a text
by scorpio17 (Canon) on Jan 20, 2009 at 15:28 UTC

    Here's an in-place edit example:

    my $file = "mydata.txt"; # read data from file open my $fh, '+<', $file or die "can't open file $file : $!\n"; my @data = <$fh>; chomp @data; # modify data (add item) @data = sort (@data, 'Simon'); # rewind back to beginning seek($fh, 0, 0); for my $d (@data) { # write data back to file (skips blank lines) print $fh "$d\n" if $d; } # if new file is shorter than original, throw away extra truncate($fh, tell($fh)); close $fh;

    Note that opening the file as '+<' means 'update mode'. The basic idea is read the file, modify the contents, then rewind back to the beginning with seek, write the new data back to the file - overwriting the original contents. If the new file is larger, you could stop here, but if it's shorter, you'll need to throw away the "extra" using truncate (so it's best to always use it, just in case). You might want to apply file locking here, depending on the application. For example, if you were using this in a webapp to add new users to an email list, you could potentially have multiple processes trying to update the file at the same time. Just add a flock($fh,2) after the open statement.

    Also note that this assumes that the file is not so big that it can't be read into memory all at once.

    You might also want to read up on perl's -i command line switch (in place edit). It actually copies the input to a temp file, reads from the temp file, and writes to the original file name - so the end results is that the output overwrites the original input.

      That seems like a contortion to avoid opening an input filehandle and a separate output filehandle.

      use strict; use warnings; my $filename = "inputfile.txt"; my $outputfile = $filename . "out"; my( $infh, $outfh ); open( $infh, '<', $filename ) or die $!; open( $outfh, '>', $outputfile ) or die $!; while( my $line = <$infh> ) { if( $line =~ /\bWilliam\b/i ) { print $outfh "Nick\n"; } print $line; } close $infh or die $!; close $outfh or die $!; rename $outputfile, $filename or die $!;

      This is pretty close to what the -i commandline switch does for Perl one liners, with the exception of the "-i.bak" behavior, which could easily be implemented here too.

      This whole thing could be written as a one liner, by the way:

      perl -pi.bak -e "/\bWilliam\b/i && print qq/Nick\n/;" filename.txt

      Obviously I forgot the names you're searching on, but the principle works.


      Dave

        There are some advantages to working in-place over creating a temporary file:

        • File permission and ownership is maintained.
        • Using a temporary file requires more permissions.
        • Using a temporary file may complicate the locking system required.
        • Working in-place can be faster in some situations.

        This is pretty close to what the -i commandline switch does for Perl one liners

        -i is closer to

        open(my $fh_in, '<', $qfn); unlink($qfn); open(my $fh_out, '>', $qfn); ...

        for now.