From: Uri Guttman Date: Tue, 5 Aug 2008 01:37:10 +0000 (-0400) Subject: Initial X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=HEAD;p=urisagit%2FFile-ReadBackwards.git Initial --- 0308c4d5000854142a309beb6a60d421cf2a6bd8 diff --git a/Changes b/Changes new file mode 100644 index 0000000..c173ad3 --- /dev/null +++ b/Changes @@ -0,0 +1,89 @@ +Revision history for Perl extension File::ReadBackwards. + +0.90 Mon Feb 28 21:37:29 2000 + - original version; created by h2xs 1.19 + +0.91 fixed test suite + +0.92 edited documentation + +0.93 Tue Mar 7 23:31:40 EST 2000 + removed use of qr// so it works under MacPerl + added README + edited documentation + +0.94 Wed Mar 8 00:12:28 EST 2000 + removed use of for modifier so it works under MacPerl + +0.95 Fri Apr 26 13:48:40 EDT 2002 + readline returns undef on a sysseek error so it fails cleanly on pipes + - from Carl Edwards + + + added eof() method + - from Antti S Lankila + + added flag to new() that marks the record separator as a regular + expression. that used to be the default behavior and now the + default is that is it a plain string like $/. + - from Robin Houston + + added close() method + - from kdamundson@mmm.com + + updated pod to reflect the changes + +0.96 Sun May 26 00:28:49 EDT 2002 + + fixed bug in close method and added test for close + - from Robin Houston + +0.97 Sun May 26 00:28:49 EDT 2002 + + D'oh! call CORE::close inside close(). robin had it correct + in his patch and i didn't use that. + +0.98 Wed Aug 21 22:51:41 EDT 2002 + + fixed bug with a partial buffer of '0'. + - from Joe Schaefer + +0.99 Tue Dec 3 00:50:23 EST 2002 + + fixed bug where readline returns data after a close + - from Khamdy + +1.00 Mon Aug 18 02:04:24 EDT 2003 + + fixed doc bug for the tied interface. the module name needs quotes + - from Madeleine Price + + added support for the tell method (including tests) + - Slaven Rezic + +1.01 Tue Oct 7 01:31:40 EDT 2003 (not released to cpan) + + fixed bug in test script in close_test. the write_file fails on + winblows since the file is still open from the main loop. now the + file is closed explicitly and tests added to cover that. + - Peter J. Acklam + +1.02 Fri Nov 21 01:53:42 EST 2003 + + fixed test problems with cr/lf files. + modified module to better handle them and when the rec_sep is set + all tests now work on unix and windows + +1.03 Mon Jan 24 17:57:54 EST 2005 + + added get_handle method and tests for it. + - Mark-Jason Dominus + +1.04 Thu May 5 01:10:44 EDT 2005 + + added getline method and tests for it + - Slaven Rezic + added support and test for large files (>2GB) + - Slaven Rezic + + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..1fb76f9 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,8 @@ +Changes +MANIFEST +Makefile.PL +ReadBackwards.pm +README +t/bw.t +t/large_file.t +META.yml Module meta-data (added by MakeMaker) diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..a63c6f6 --- /dev/null +++ b/META.yml @@ -0,0 +1,10 @@ +# http://module-build.sourceforge.net/META-spec.html +#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX# +name: File-ReadBackwards +version: 1.04 +version_from: ReadBackwards.pm +installdirs: site +requires: + +distribution_type: module +generated_by: ExtUtils::MakeMaker version 6.17 diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..e27c279 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,7 @@ +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'NAME' => 'File::ReadBackwards', + 'VERSION_FROM' => 'ReadBackwards.pm', # finds $VERSION +); diff --git a/README b/README new file mode 100644 index 0000000..d779973 --- /dev/null +++ b/README @@ -0,0 +1,55 @@ + + File::ReadBackwards.pm + +This module reads a file backwards line by line. It is simple to use, +memory efficient and fast. It supports both an object and a tied handle +interface. + +It is intended for processing log and other similar text files which +typically have their newest entries appended to them. By default files +are assumed to be plain text and have a line ending appropriate to the +OS. But you can set the input record separator string on a per file +basis. + +PREREQUISITES + +There are no prerequisite modules. + +INSTALLATION + +Installation is done as with most Perl modules by running these +commands: + + perl Makefile.PL + make + make test + make install + +DOCUMENTATION + +Documentation is in the module file in pod form. It will be installed in +the normal documentation directories on your system. An HTML version of +the documentation is at: + + http://www.sysarch.com/perl/modules/File-ReadBackwards.html + +SUPPORT + +If you have any questions, bug reports or feedback, email it to + + uri@sysarch.com + +AVAILABILITY + +The latest version of File::ReadBackwards.pm will always be available in +this directory: + + http://www.sysarch.com/perl/modules + + +COPYRIGHT + +(C) 2000 Uri Guttman. All rights reserved. + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. diff --git a/ReadBackwards.pm b/ReadBackwards.pm new file mode 100644 index 0000000..ee5aac8 --- /dev/null +++ b/ReadBackwards.pm @@ -0,0 +1,395 @@ +# File::ReadBackwards.pm + +# Copyright (C) 2003 by Uri Guttman. All rights reserved. +# mail bugs, comments and feedback to uri@stemsystems.com + +package File::ReadBackwards ; + +use strict ; + +use vars qw( $VERSION ) ; + +$VERSION = '1.04' ; + +use Symbol ; +use Fcntl qw( :seek O_RDONLY ) ; +use Carp ; + +my $max_read_size = 1 << 13 ; + +my $default_rec_sep ; + +BEGIN { + +# set the default record separator according to this OS +# this needs testing and expansion. + +# look for CR/LF types +# then look for CR types +# else it's a LF type + + if ( $^O =~ /win32/i || $^O =~ /vms/i ) { + + $default_rec_sep = "\015\012" ; + } + elsif ( $^O =~ /mac/i ) { + + $default_rec_sep = "\015" ; + } + else { + $default_rec_sep = "\012" ; + } + +# the tied interface is exactly the same as the object one, so all we +# need to do is to alias the subs with typeglobs + + *TIEHANDLE = \&new ; + *READLINE = \&readline ; + *EOF = \&eof ; + *CLOSE = \&close ; + *TELL = \&tell ; + +# added getline alias for compatibility with IO::Handle + + *getline = \&readline ; +} + + +# constructor for File::ReadBackwards + +sub new { + + my( $class, $filename, $rec_sep, $sep_is_regex ) = @_ ; + +# check that we have a filename + + defined( $filename ) || return ; + +# see if this file uses the default of a cr/lf separator +# those files will get cr/lf converted to \n + + $rec_sep ||= $default_rec_sep ; + my $is_crlf = $rec_sep eq "\015\012" ; + +# get a handle and open the file + + my $handle = gensym ; + sysopen( $handle, $filename, O_RDONLY ) || return ; + binmode $handle ; + +# seek to the end of the file and get its size + + my $seek_pos = sysseek( $handle, 0, SEEK_END ) or return ; + +# get the size of the first block to read, +# either a trailing partial one (the % size) or full sized one (max read size) + + my $read_size = $seek_pos % $max_read_size || $max_read_size ; + +# create the object + + my $self = bless { + 'file_name' => $filename, + 'handle' => $handle, + 'read_size' => $read_size, + 'seek_pos' => $seek_pos, + 'lines' => [], + 'is_crlf' => $is_crlf, + 'rec_sep' => $rec_sep, + 'sep_is_regex' => $sep_is_regex, + + }, $class ; + + return( $self ) ; +} + +# read the previous record from the file +# +sub readline { + + my( $self, $line_ref ) = @_ ; + + my $read_buf ; + +# get the buffer of lines + + my $lines_ref = $self->{'lines'} ; + + return unless $lines_ref ; + + while( 1 ) { + +# see if there is more than 1 line in the buffer + + if ( @{$lines_ref} > 1 ) { + +# we have a complete line so return it +# and convert those damned cr/lf lines to \n + + $lines_ref->[-1] =~ s/\015\012/\n/ + if $self->{'is_crlf'} ; + + return( pop @{$lines_ref} ) ; + } + +# we don't have a complete, so have to read blocks until we do + + my $seek_pos = $self->{'seek_pos'} ; + +# see if we are at the beginning of the file + + if ( $seek_pos == 0 ) { + +# the last read never made more lines, so return the last line in the buffer +# if no lines left then undef will be returned +# and convert those damned cr/lf lines to \n + + $lines_ref->[-1] =~ s/\015\012/\n/ + if @{$lines_ref} && $self->{'is_crlf'} ; + + return( pop @{$lines_ref} ) ; + } + +# we have to read more text so get the handle and the current read size + + my $handle = $self->{'handle'} ; + my $read_size = $self->{'read_size'} ; + +# after the first read, always read the maximum size + + $self->{'read_size'} = $max_read_size ; + +# seek to the beginning of this block and save the new seek position + + $seek_pos -= $read_size ; + sysseek( $handle, $seek_pos, SEEK_SET ) ; + $self->{'seek_pos'} = $seek_pos ; + +# read in the next (previous) block of text + + my $read_cnt = sysread( $handle, $read_buf, $read_size ) ; + +# prepend the read buffer to the leftover (possibly partial) line + + my $text = $read_buf ; + $text .= shift @{$lines_ref} if @{$lines_ref} ; + +# split the buffer into a list of lines +# this may want to be $/ but reading files backwards assumes plain text and +# newline separators + + @{$lines_ref} = ( $self->{'sep_is_regex'} ) ? + $text =~ /(.*?$self->{'rec_sep'}|.+)/gs : + $text =~ /(.*?\Q$self->{'rec_sep'}\E|.+)/gs ; + +#print "Lines \n=>", join( "<=\n=>", @{$lines_ref} ), "<=\n" ; + + } +} + +sub eof { + + my ( $self ) = @_ ; + + my $seek_pos = $self->{'seek_pos'} ; + my $lines_count = @{ $self->{'lines'} } ; + return( $seek_pos == 0 && $lines_count == 0 ) ; +} + +sub tell { + my ( $self ) = @_ ; + + my $seek_pos = $self->{'seek_pos'} ; + $seek_pos + length(join "", @{ $self->{'lines'} }); +} + +sub get_handle { + my ( $self ) = @_ ; + + my $handle = $self->{handle} ; + seek( $handle, $self->tell, SEEK_SET ) ; + return $handle ; +} + +sub close { + + my ( $self ) = @_ ; + + my $handle = delete( $self->{'handle'} ) ; + delete( $self->{'lines'} ) ; + + CORE::close( $handle ) ; +} + +__END__ + + +=head1 NAME + +File::ReadBackwards.pm -- Read a file backwards by lines. + + +=head1 SYNOPSIS + + use File::ReadBackwards ; + + # Object interface + + $bw = File::ReadBackwards->new( 'log_file' ) or + die "can't read 'log_file' $!" ; + + while( defined( $log_line = $bw->readline ) ) { + print $log_line ; + } + + # ... or the alternative way of reading + + until ( $bw->eof ) { + print $bw->readline ; + } + + # Tied Handle Interface + + tie *BW, 'File::ReadBackwards', 'log_file' or + die "can't read 'log_file' $!" ; + + while( ) { + print ; + } + +=head1 DESCRIPTION + + +This module reads a file backwards line by line. It is simple to use, +memory efficient and fast. It supports both an object and a tied handle +interface. + +It is intended for processing log and other similar text files which +typically have their newest entries appended to them. By default files +are assumed to be plain text and have a line ending appropriate to the +OS. But you can set the input record separator string on a per file +basis. + + +=head1 OBJECT INTERFACE + +These are the methods in C' object interface: + + +=head2 new( $file, [$rec_sep], [$sep_is_regex] ) + +C takes as arguments a filename, an optional record separator and +an optional flag that marks the record separator as a regular +expression. It either returns the object on a successful open or undef +upon failure. $! is set to the error code if any. + +=head2 readline + +C takes no arguments and it returns the previous line in the +file or undef when there are no more lines in the file. If the file is +a non-seekable file (e.g. a pipe), then undef is returned. + +=head2 getline + +C is an alias for the readline method. It is here for +compatibilty with the IO::* classes which has a getline method. + +=head2 eof + +C takes no arguments and it returns true when readline() has +iterated through the whole file. + +=head2 close + +C takes no arguments and it closes the handle + +=head2 tell + +C takes no arguments and it returns the current filehandle position. +This value may be used to seek() back to this position using a normal +file handle. + +=head2 get_handle + +C takes no arguments and it returns the internal Perl +filehandle used by the File::ReadBackwards object. This handle may be +used to read the file forward. Its seek position will be set to the +position that is returned by the tell() method. Note that +interleaving forward and reverse reads may produce unpredictable +results. The only use supported at present is to read a file backward +to a certain point, then use 'handle' to extract the handle, and read +forward from that point. + +=head1 TIED HANDLE INTERFACE + +=head2 tie( *HANDLE, 'File::ReadBackwards', $file, [$rec_sep], [$sep_is_regex] ) + + +The TIEHANDLE, READLINE, EOF, CLOSE and TELL methods are aliased to +the new, readline, eof, close and tell methods respectively so refer +to them for their arguments and API. Once you have tied a handle to +File::ReadBackwards the only I/O operation permissible is <> which +will read the previous line. You can call eof() and close() on the +tied handle as well. All other tied handle operations will generate an +unknown method error. Do not seek, write or perform any other +unsupported operations on the tied handle. + +=head1 LINE AND RECORD ENDINGS + + +Since this module needs to use low level I/O for efficiency, it can't +portably seek and do block I/O without managing line ending conversions. +This module supports the default record separators of normal line ending +strings used by the OS. You can also set the separator on a per file +basis. + +The record separator is a regular expression by default, which differs +from the behavior of $/. + +Only if the record separator is B specified and it defaults to +CR/LF (e.g, VMS, redmondware) will it will be converted to a single +newline. Unix and MacOS files systems use only a single character for +line endings and the lines are left unchanged. This means that for +native text files, you should be able to process their lines backwards +without any problems with line endings. If you specify a record +separator, no conversions will be done and you will get the records as +if you read them in binary mode. + +=head1 DESIGN + +It works by reading a large (8kb) block of data from the end of the +file. It then splits them on the record separator and stores a list of +records in the object. Each call to readline returns the top record of +the list and if the list is empty it refills it by reading the previous +block from the file and splitting it. When the beginning of the file is +reached and there are no more lines, undef is returned. All boundary +conditions are handled correctly i.e. if there is a trailing partial +line (no newline) it will be the first line returned and lines larger +than the read buffer size are handled properly. + + +=head1 NOTES + + +There is no support for list context in either the object or tied +interfaces. If you want to slurp all of the lines into an array in +backwards order (and you don't care about memory usage) just do: + + @back_lines = reverse . + +This module is only intended to read one line at a time from the end of +a file to the beginning. + +=head1 AUTHOR + + +Uri Guttman, uri@stemsystems.com + +=head1 COPYRIGHT + + +Copyright (C) 2003 by Uri Guttman. All rights reserved. This program is +free software; you can redistribute it and/or modify it under the same +terms as Perl itself. + +=cut diff --git a/t/bw.t b/t/bw.t new file mode 100755 index 0000000..07796bb --- /dev/null +++ b/t/bw.t @@ -0,0 +1,286 @@ +#!/usr/local/bin/perl -ws + +use strict ; +use Test::More ; +use Fcntl qw( :seek ) ; +use File::ReadBackwards ; +use Carp ; + +use vars qw( $opt_v ) ; + +my $file = 'bw.data' ; + +my $is_crlf = ( $^O =~ /win32/i || $^O =~ /vms/i ) ; + +print "nl\n" ; +my @nl_data = init_data( "\n" ) ; +plan( tests => 10 * @nl_data + 1 ) ; +test_read_backwards( \@nl_data ) ; + +print "crlf\n" ; +my @crlf_data = init_data( "\015\012" ) ; +test_read_backwards( \@crlf_data, "\015\012" ) ; + +test_close() ; +unlink $file ; + +exit ; + +sub init_data { + + my ( $rec_sep ) = @_ ; + + return map { ( my $data = $_ ) =~ s/RS/$rec_sep/g ; $data } + '', + 'RS', + 'RSRS', + 'RSRSRS', + "\015", + "\015RSRS", + 'abcd', + "abcdefghijRS", + "abcdefghijRS" x 512, + 'a' x (8 * 1024), + 'a' x (8 * 1024) . '0', + '0' x (8 * 1024) . '0', + 'a' x (32 * 1024), + join( 'RS', '00' .. '99', '' ), + join( 'RS', '00' .. '99' ), + join( 'RS', '0000' .. '9999', '' ), + join( 'RS', '0000' .. '9999' ), + ; +} + +sub test_read_backwards { + + my( $data_list_ref, $rec_sep ) = @_ ; + + foreach my $data ( @$data_list_ref ) { + +# write the test data to a file in text or bin_mode + + if ( defined $rec_sep ) { + + write_bin_file( $file, $data ) ; + +# print "cnt: ${\scalar @rev_file_lines}\n" ; + + } + else { + write_file( $file, $data ) ; + + } + + test_data( $rec_sep ) ; + + test_tell_handle( $rec_sep ) ; + } +} + +sub test_data { + + my( $rec_sep ) = @_ ; + +# slurp in the file and reverse the list of lines to get golden data + + my @rev_file_lines = reverse read_bin_file( $file ) ; + +# convert CR/LF to \n if needed - based on OS or we are testing CR/LF + + if ( $is_crlf || $rec_sep && $rec_sep eq "\015\012" ) { + s/\015\012\z/\n/ for @rev_file_lines ; + } + +# open the file with backwards and read in the lines + + my $bw = File::ReadBackwards->new( $file, $rec_sep ) or + die "can't open $file: $!" ; + + my( @bw_file_lines ) ; + while ( 1 ) { + + my $line = $bw->readline() ; + last unless defined( $line ) ; + push( @bw_file_lines, $line ) ; + + $line = $bw->getline() ; + last unless defined( $line ) ; + push( @bw_file_lines, $line ) ; + } + +# while ( defined( my $line = $bw->readline() ) ) { +# push( @bw_file_lines, $line) ; +# } + +# see if we close cleanly + + ok( $bw->close(), 'close' ) ; + +# compare the golden lines to the backwards lines + + if ( eq_array( \@rev_file_lines, \@bw_file_lines ) ) { + + ok( 1, 'read' ) ; + return ; + } + +# test failed so dump the different lines if verbose + + ok( 0, 'read' ) ; + + return unless $opt_v ; + + print "[$rev_file_lines[0]]\n" ; + print unpack( 'H*', $rev_file_lines[0] ), "\n" ; + print unpack( 'H*', $bw_file_lines[0] ), "\n" ; + +#print "REV ", unpack( 'H*', join '',@rev_file_lines ), "\n" ; +#print "BW ", unpack( 'H*', join '',@bw_file_lines ), "\n" ; + +} + +sub test_tell_handle { + + my( $rec_sep ) = @_ ; + +# open the file backwards again to test tell and get_handle methods + + my $bw = File::ReadBackwards->new( $file, $rec_sep ) or + die "can't open $file: $!" ; + +# read the last line in + + my $bw_line = $bw->readline() ; + +# get the current seek position + + my $pos = $bw->tell() ; + +#print "BW pos = $pos\n" ; + + if ( $bw->eof() ) { + + ok( 1, "skip tell - at eof" ) ; + ok( 1, "skip get_handle - at eof" ) ; + } + else { + +# save the current $/ so we can reassign it if it $rec_sep isn't set + + my $old_rec_sep = $/ ; + local $/ = $rec_sep || $old_rec_sep ; + +# open a new regular file and seek to this spot. + + open FH, $file or die "tell open $!" ; + seek FH, $pos, SEEK_SET or die "tell seek $!" ; + +# read in the next line and clean up the ending CR/LF + + my $fw_line = ; + $fw_line =~ s/\015\012\z/\n/ ; + +# print "BW [", unpack( 'H*', $bw_line ), +# "] TELL [", unpack( 'H*', $fw_line), "]\n" if $bw_line ne $fw_line ; + +# compare the backwards and forwards lines + + is ( $bw_line, $fw_line, "tell check" ) ; + +# get the handle and seek to the current spot + + my $fh = $bw->get_handle() ; + +# read in the next line and clean up the ending CR/LF + + my $fh_line = <$fh> ; + $fh_line =~ s/\015\012\z/\n/ ; + +# print "BW [", unpack( 'H*', $bw_line ), +# "] HANDLE [", unpack( 'H*', $fh_line), "]\n" if $bw_line ne $fh_line ; + +# compare the backwards and forwards lines + + is ( $bw_line, $fh_line, "get_handle" ) ; + } + + ok( $bw->close(), 'close2' ) ; + +} + +sub test_close { + + write_file( $file, <new( $file ) or + die "can't open $file: $!" ; + + my $line = $bw->readline() ; + + $bw->close() ; + + if ( $bw->readline() ) { + + ok( 0, 'close' ) ; + return ; + } + + ok( 1, 'close' ) ; +} + +sub read_file { + + my( $file_name ) = shift ; + + local( *FH ) ; + + open( FH, $file_name ) || carp "can't open $file_name $!" ; + + local( $/ ) unless wantarray ; + + +} + +# utility sub to write a file. takes a file name and a list of strings + +sub write_file { + + my( $file_name ) = shift ; + + local( *FH ) ; + + open( FH, ">$file_name" ) || carp "can't create $file_name $!" ; + + print FH @_ ; +} + +sub read_bin_file { + + my( $file_name ) = shift ; + + local( *FH ) ; + open( FH, $file_name ) || carp "can't open $file_name $!" ; + binmode( FH ) ; + + local( $/ ) = shift if @_ ; + + local( $/ ) unless wantarray ; + + +} + +# utility sub to write a file. takes a file name and a list of strings + +sub write_bin_file { + + my( $file_name ) = shift ; + + local( *FH ) ; + open( FH, ">$file_name" ) || carp "can't create $file_name $!" ; + binmode( FH ) ; + + print FH @_ ; +} diff --git a/t/large_file.t b/t/large_file.t new file mode 100644 index 0000000..b367c9a --- /dev/null +++ b/t/large_file.t @@ -0,0 +1,107 @@ +#!/usr/local/bin/perl -ws + +use strict ; + +use Carp ; +use Config ; +use Fcntl qw( :seek ) ; +use Test::More ; + +use File::ReadBackwards ; + +# NOTE: much of this code was taken from the core perl test script +# ops/lfs.t. it was modified to test File::ReadBackwards and large files + +my %has_no_sparse_files = map { $_ => 1 } + qw( MSWin32 NetWare VMS unicos ) ; + +my $test_file = 'bw.data' ; + +my @test_lines = ( + "3rd from last line\n", + "2nd from last\n", + "last line\n", +) ; + +my $test_text = join '', @test_lines ; + + +sub skip_all_tests { + + my( $skip_text ) = @_ ; + +# unlink $test_file ; + plan skip_all => $skip_text ; +} + +if( $Config{lseeksize} < 8 ) { + skip_all_tests( "no 64-bit file offsets\n" ) ; +} + +unless( $Config{uselargefiles} ) { + skip_all_tests( "no large file support\n" ) ; +} + +if ( $has_no_sparse_files{ $^O } ) { + skip_all_tests( "no sparse files in $^O\n" ) ; +} + +# run the long seek code below in a subprocess in case it exits with a +# signal + +my $rc = system $^X, '-e', <<"EOF"; +open(BIG, ">$test_file"); +seek(BIG, 5_000_000_000, 0); +print BIG "$test_text" ; +exit 0; +EOF + +if( $rc ) { + + my $error = 'signal ' . ($rc & 0x7f) ; + skip_all_tests( "seeking past 2GB failed: $error" ) ; +} + +open(BIG, ">$test_file"); + +unless( seek(BIG, 5_000_000_000, 0) ) { + skip_all_tests( "seeking past 2GB failed: $!" ) ; +} + + +# Either the print or (more likely, thanks to buffering) the close will +# fail if there are are filesize limitations (process or fs). + +my $print = print BIG $test_text ; +my $close = close BIG; + +unless ($print && $close) { + + print "# print failed: $!\n" unless $print; + print "# close failed: $!\n" unless $close; + + if( $! =~/too large/i ) { + skip_all_tests( 'writing past 2GB failed: process limits?' ) ; + } + + if( $! =~ /quota/i ) { + skip_all_tests( 'filesystem quota limits?' ) ; + } + + skip_all_tests( "large file error: $!" ) ; +} + +plan tests => 2 ; + +my $bw = File::ReadBackwards->new( $test_file ) or + die "can't open $test_file: $!" ; + +my $line = $bw->readline() ; +is( $line, $test_lines[-1], 'last line' ) ; + +$line = $bw->readline() ; +is( $line, $test_lines[-2], 'next to last line' ) ; + +unlink $test_file ; + +exit ;