require 'epitools/minimal' # # Beginning of File reached! (Raised when reading a file backwards.) # class BOFError < Exception; end class File # # A streaming `reverse_each` implementation. (For large files, it's faster and uses less memory.) # def reverse_each(&block) return to_enum(:reverse_each) unless block_given? seek_end reverse_each_from_current_pos(&block) end # # Read the previous `length` bytes. After the read, `pos` will be at the beginning of the region that you just read. # Returns `nil` when the beginning of the file is reached. # # If the `block_aligned` argument is `true`, reads will always be aligned to file positions which are multiples of 512 bytes. # (This should increase performance slightly.) # def reverse_read(length, block_aligned=false) raise "length must be a multiple of 512" if block_aligned and length % 512 != 0 end_pos = pos return nil if end_pos == 0 if block_aligned misalignment = end_pos % length length += misalignment end if length >= end_pos # this read will take us to the beginning of the file seek(0) else seek(-length, IO::SEEK_CUR) end start_pos = pos data = read(end_pos - start_pos) seek(start_pos) data end # # Read each line of file backwards (from the current position.) # def reverse_each_from_current_pos return to_enum(:reverse_each_from_current_pos) unless block_given? # read the rest of the current line, in case we started in the middle of a line start_pos = pos fragment = readline rescue "" seek(start_pos) while data = reverse_read(4096) lines = data.each_line.to_a lines.last << fragment unless lines.last[-1] == "\n" fragment = lines.first lines[1..-1].reverse_each { |line| yield line } end yield fragment end # # Seek to `EOF` # def seek_end seek(0, IO::SEEK_END) end # # Seek to `BOF` # def seek_start seek(0) end # # Read the previous line (leaving `pos` at the beginning of the string that was read.) # def reverse_readline raise BOFError.new("beginning of file reached") if pos == 0 seek_backwards_to("\n", 512, -2) new_pos = pos data = readline seek(new_pos) data end # # Scan backwards in the file until `string` is found, and set the IO's +pos+ to the first character after the matched string. # def seek_backwards_to(string, blocksize=512, rindex_end=-1) loop do data = reverse_read(blocksize) if index = data.rindex(string, rindex_end) seek(index+string.size, IO::SEEK_CUR) break elsif pos == 0 break end end end end