#!/usr/bin/python2 """Mimics UNIX tail, especially its behavior when dealing with large files The I/O operations have nearly the same pattern as tail """ import collections import sys import errno def tail_unseekable(filein, count): """Fallback for special files like pipes or terminals Arguments: filein -- file-like object opened for reading count -- number of lines to be written to stdout """ sys.stdout.writelines(collections.deque(filein, count)) def read_blocks(filein, positions, endpos): """Yields strings of 8192 bytes length from filein Arguments: filein -- seekable file opened for reading positions -- iterable of seek positions from which to read endpos -- a position beyond which shall not be read """ blocks = ((blockpos, min(8192, endpos - blockpos)) for blockpos in positions) for blockpos, blocklen in blocks: filein.seek(blockpos) yield filein.read(blocklen) def count_lines(filein, count, endpos): """Reads the given file backwards, stopping after counting count lnes Arguments: filein -- seekable file opened for reading. After returning, the file position will be right after the returned data block count -- number of lines to find endpos -- a position beyond which shall not be read Returns: (linecount, blockdata) linecount -- the number of lines actually counted. This can be higher than count or lower (when the file is too short) blockdata -- the last block read from file, included in linecount """ linecount = 0 blockdata = None blockpositions = reversed(xrange(0, endpos, 8192)) for blockdata in read_blocks(filein, blockpositions, endpos): blocklines = blockdata.count('\n') linecount += blocklines if linecount >= count: break return (linecount, blockdata) def print_partial(blockdata, actual_count, wanted_count): """Prints the tail of blockdata to stdout, skipping superfluous lines Arguments: blockdata -- a string containing lines actual_count -- number of lines in blockdata (and beyond) wanted_count -- number of lines that shall be written """ toomuch = actual_count - wanted_count startpos = 0 for _ in xrange(toomuch): startpos = blockdata.index('\n', startpos) + 1 sys.stdout.write(buffer(blockdata, startpos)) def print_to_pos(filein, endpos): """Prints filein to stdout from the current position to endpos""" blockpos = filein.tell() wholeblocks, remainder = divmod(endpos - blockpos, 8192) blocklens = [8192] * wholeblocks blocklens.append(remainder) blocks = (filein.read(blocklen) for blocklen in blocklens) for block in blocks: sys.stdout.write(block) # sys.stdout.writelines(blocks) # works, too, but buffers output def tail(filein, count): """Writes the last count lines from filein to stdout""" try: filein.seek(0, 2) except IOError as err: if err.errno == errno.ESPIPE: tail_unseekable(filein, count) return else: raise endpos = filein.tell() linecount, blockdata = count_lines(filein, count, endpos) print_partial(blockdata, linecount, count) print_to_pos(filein, endpos) def main(): filename = sys.argv[1] count = 100 with open(filename, 'r', 8192) as fd: tail(fd, count) return if __name__ == '__main__': main()