#!/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()
Comments