Last active
July 22, 2025 15:17
-
-
Save gbhorwood/865c3e85fbff170ce659963d7ff72f6c to your computer and use it in GitHub Desktop.
Pure php implementation of tail -f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Tailf | |
| * A pure PHP implementation of the linux command tail -f | |
| * | |
| * 20250722 gbh | |
| */ | |
| // duration for usleep | |
| define('ONE_TENTH_SECOND', 100000); | |
| // number of lines to output on tail | |
| define('TAIL_LINE_COUNT', 10); | |
| /** | |
| * Entry point | |
| */ | |
| tailf(filePath($argv)); | |
| /** | |
| * Gets file path from cli arguments, tests for validity | |
| * and returns it. | |
| * Dies with message on any fail. | |
| * | |
| * @param array<string> $argv | |
| * @return string | |
| */ | |
| function filePath($argv): string { | |
| $file = $argv[1] ?? null; | |
| if(!$file) { | |
| die("Must pass a file as argument"); | |
| } | |
| if(!file_exists($file)) { | |
| die("No file at '$file'"); | |
| } | |
| if(!is_readable($file)) { | |
| die("Cannot read file at '$file'"); | |
| } | |
| return $file; | |
| } | |
| /** | |
| * Opens a file, returns the pointer resource | |
| * | |
| * @param string $file Path to file | |
| * @return mixed The file pointer resource | |
| */ | |
| function open(string $file): mixed { | |
| $fp = fopen($file, "r"); | |
| return $fp; | |
| } | |
| /** | |
| * Updates a file pointer $fp to be n bytes from the end of the file | |
| * so that outputting to eof prints the last $lineCount lines. | |
| * | |
| * @param mixed $fp The file pointer resource | |
| * @param int $lineCount How many lines back from the end of file | |
| * @return mixed The file pointer resource | |
| */ | |
| function windToTailStart(mixed $fp, int $lineCount): mixed { | |
| fseek($fp, 0, SEEK_END); | |
| $position = -1; | |
| $lineCounter = 0; | |
| do { | |
| if(ftell($fp) == 0) { | |
| return $fp; | |
| } | |
| fseek($fp, $position--, SEEK_END); | |
| if(fgetc($fp) == PHP_EOL) { | |
| $lineCounter++; | |
| } | |
| } | |
| while($lineCounter < $lineCount); | |
| return $fp; | |
| } | |
| /** | |
| * Outputs all lines of file from the position of the | |
| * file pointer $fp to the end of file. | |
| * | |
| * @param mixed $fp The file pointer resource | |
| * @return void | |
| */ | |
| function output(mixed $fp) { | |
| while(!feof($fp)) { | |
| print fgets($fp); | |
| } | |
| } | |
| /** | |
| * Outputs the last $lineCount lines from file $file. | |
| * | |
| * @param string $file Path to file | |
| * @param int $lineCount | |
| * @return void | |
| */ | |
| function tail(string $file, int $lineCount) { | |
| $fp = open($file); | |
| $fp = windToTailStart($fp, $lineCount); | |
| output($fp); | |
| fclose($fp); | |
| } | |
| /** | |
| * Outputs the last $lineCount lines from file $file | |
| * then waits for the file to update and outputs the | |
| * new lines. ie. `tail -f` | |
| * | |
| * @param string $file Path to file | |
| * @param int $lineCount | |
| * @return void | |
| */ | |
| function tailf(string $file, int $lineCount = TAIL_LINE_COUNT) { | |
| // output tail content | |
| tail($file, $lineCount); | |
| // open file at the end | |
| $fp = open($file); | |
| fseek($fp, 0, SEEK_END); | |
| // wait for new file content and output | |
| while(true) { /** @phpstan-ignore while.alwaysTrue */ | |
| $tell = ftell($fp); | |
| usleep(ONE_TENTH_SECOND); | |
| fseek($fp, 0, SEEK_END); | |
| if($tell != ftell($fp)) { | |
| fseek($fp, $tell, SEEK_SET); | |
| output($fp); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment