PHP: Tail tackling large files

Published:

Needed a function that could get me the last N lines of a log file. Wanted it to be efficient and not dependent on anything other than my code.

Found some versions, but they were either a bit messy or depended on unstable arithmetic (where filesize is greater than PHP_INT_MAX). So, I decided to take on the challenge and try to write one myself. Nice little exercise 🙂

define('TAIL_NL', "\n");

/**
 * Tail in PHP, capable of eating big files.
 *
 * @author  Torleif Berger
 * @link    http://www.geekality.net/?p=1654
 */
function tail($filename, $lines = 10, $buffer = 4096)
{
    // Open the file
    if(is_resource($file) && (get_resource_type($file) == 'file'
    || get_resource_type($file) == 'stream'))
        $f = $file;
    elseif(is_string($file))
        $f = fopen($file, 'rb');
    else
        throw new Exception('$file must be either a resource (file or stream) or a filename.');

    // Jump to last character
    fseek($f, -1, SEEK_END);

    // Prepare to collect output
    $output = '';
    $chunk = '';

    // Start reading it and adjust line number if necessary
    // (Otherwise the result would be wrong if file doesn't end with a blank line)
    if(fread($f, 1) != TAIL_NL) $lines -= 1;

    // While we would like more
    while(ftell($f) > 0 && $lines >= 0)
    {
        // Figure out how far back we should jump
        $seek = min(ftell($f), $buffer);

        // Do the jump (backwards, relative to where we are)
        fseek($f, -$seek, SEEK_CUR);

        // Read a chunk and prepend it to our output
        $output = ($chunk = fread($f, $seek)).$output;

        // Jump back to where we started reading
        fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);

        // Decrease our line counter
        $lines -= substr_count($chunk, TAIL_NL);
    }

    // While we have too many lines
    // (Because of buffer size we might have read too many)
    while($lines++ < 0)
    {
        // Find first newline and remove all text before that
        $output = substr($output, strpos($output, TAIL_NL) + 1);
    }

    // Close file and return
    fclose($f);
    return $output;
}