#!/usr/bin/env ruby
# mtros FILES... - sort -m, but reading files backwards
Process.setrlimit(:NOFILE, 2048)
# ==============================================================================
# elif.rb
#
# Created by James Edward Gray II on 2006-01-28.
# Copyright 2006 Gray Productions. All rights reserved.
# Distributed under the user's choice of the
# GPL[http://www.gnu.org/copyleft/gpl.html] (see COPYING for details)
# or the {Ruby software license}[http://www.ruby-lang.org/en/LICENSE.txt]
# by James Edward Gray II.
#
# A File-like object for reading lines from a disk file in reverse order. See
# Elif::new and Elif#gets for details. All other methods are just interface
# conveniences.
#
# Based on Perl's File::ReadBackwards module, by Uri Guttman.
#
class Elif
# The version of the installed library.
VERSION = "0.1.0".freeze
# The size of the reads we will use to add to the line buffer.
MAX_READ_SIZE = 1 << 10 # 1024
# Works just line File::foreach, save that the lines come in reverse order.
def self.foreach(name, sep_string = $/)
open(name) do |file|
while line = file.gets(sep_string)
yield line
end
end
end
# Works just line File::open.
def self.open(*args)
file = new(*args)
if block_given?
begin
yield file
ensure
file.close
end
else
file
end
end
#
# Works just line File::readlines, save that line Array will be in
# reverse order.
#
def self.readlines(name, sep_string = $/)
open(name) { |file| file.readlines(sep_string) }
end
#
# The first half of the Elif algorithm (to read file lines in reverse order).
# This creates a new Elif object, shifts the read pointer to the end of the
# file, and prepares a buffer to hold read lines until they can be returned.
# This method also sets the @read_size to the remainer of File#size
# and +MAX_READ_SIZE+ for the first read.
#
# Technically +args+ are delegated straight to File#new, but you must open the
# File object for reading for it to work with this algorithm.
#
def initialize(*args)
# Delegate to File::new and move to the end of the file.
@file = File.new(*args)
@file.seek(0, IO::SEEK_END)
# Record where we are.
@current_pos = @file.pos
# Get the size of the next of the first read, the dangling bit of the file.
@read_size = @file.pos % MAX_READ_SIZE
@read_size = MAX_READ_SIZE if @read_size.zero?
# A buffer to hold lines read, but not yet returned.
@line_buffer = Array.new
end
#
# The second half on the Elif algorthim (see Elif::new). This method returns
# the next line of the File, working from the end to the beginning in reverse
# line order.
#
# It works by moving the file pointer backwords +MAX_READ_SIZE+ at a time,
# storing seen lines in @line_buffer. Once the buffer contains at
# least two lines (ensuring we have seen on full line) or the file pointer
# reaches the head of the File, the last line from the buffer is returned.
# When the buffer is exhausted, this will throw +nil+ (from the empty Array).
#
def gets(sep_string = $/)
#
# If we have more than one line in the buffer or we have reached the
# beginning of the file, send the last line in the buffer to the caller.
# (This may be +nil+, if the buffer has been exhausted.)
#
return @line_buffer.pop if @line_buffer.size > 2 or @current_pos.zero?
#
# If we made it this far, we need to read more data to try and find the
# beginning of a line or the beginning of the file. Move the file pointer
# back a step, to give us new bytes to read.
#
@current_pos -= @read_size
@file.seek(@current_pos, IO::SEEK_SET)
#
# Read more bytes and prepend them to the first (likely partial) line in the
# buffer.
#
@line_buffer[0] = "#{@file.read(@read_size)}#{@line_buffer[0]}"
@read_size = MAX_READ_SIZE # Set a size for the next read.
#
# Divide the first line of the buffer based on +sep_string+ and #flatten!
# those new lines into the buffer.
#
@line_buffer[0] = @line_buffer[0].scan(/.*?#{Regexp.escape(sep_string)}|.+/)
@line_buffer.flatten!
# We have move data now, so try again to read a line...
gets(sep_string)
end
# Works just line File#each, save that the lines come in reverse order.
def each(sep_string = $/)
while line = gets(sep_string)
yield line
end
end
alias_method :each_line, :each # Works just like File#each_line.
include Enumerable # Support all the standard iterators.
# Works just line File#readline, save that the lines come in reverse order.
def readline(sep_string = $/)
gets(sep_string) || raise(EOFError, "end of file reached")
end
#
# Works just line File#readlines, save that line Array will be in
# reverse order.
#
def readlines(sep_string = $/)
lines = Array.new
while line = gets(sep_string)
lines << line
end
lines
end
# Works just line File#close.
def close
@file.close
end
end
# ==============================================================================
# end of elif.rb
files = ARGV
elifs = files.map { |f| Elif.open(f) }
lines = elifs.map { |p| p.gets }
loop {
m = nil
mi = nil
lines.each_with_index { |l, li|
if l && (m.nil? || m < l)
m, mi = l, li
end
}
break unless m
puts files[mi] + ": " + lines[mi]
lines[mi] = elifs[mi].gets
}