119ea8026Sopenharmony_ci#!/usr/bin/env python3 219ea8026Sopenharmony_ci# 319ea8026Sopenharmony_ci# Efficiently displays the last n lines of a file/pipe. 419ea8026Sopenharmony_ci# 519ea8026Sopenharmony_ci# Example: 619ea8026Sopenharmony_ci# ./scripts/tailpipe.py trace -n5 719ea8026Sopenharmony_ci# 819ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors. 919ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause 1019ea8026Sopenharmony_ci# 1119ea8026Sopenharmony_ci 1219ea8026Sopenharmony_ciimport collections as co 1319ea8026Sopenharmony_ciimport io 1419ea8026Sopenharmony_ciimport os 1519ea8026Sopenharmony_ciimport select 1619ea8026Sopenharmony_ciimport shutil 1719ea8026Sopenharmony_ciimport sys 1819ea8026Sopenharmony_ciimport threading as th 1919ea8026Sopenharmony_ciimport time 2019ea8026Sopenharmony_ci 2119ea8026Sopenharmony_ci 2219ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1): 2319ea8026Sopenharmony_ci # allow '-' for stdin/stdout 2419ea8026Sopenharmony_ci if path == '-': 2519ea8026Sopenharmony_ci if mode == 'r': 2619ea8026Sopenharmony_ci return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) 2719ea8026Sopenharmony_ci else: 2819ea8026Sopenharmony_ci return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) 2919ea8026Sopenharmony_ci else: 3019ea8026Sopenharmony_ci return open(path, mode, buffering) 3119ea8026Sopenharmony_ci 3219ea8026Sopenharmony_ciclass LinesIO: 3319ea8026Sopenharmony_ci def __init__(self, maxlen=None): 3419ea8026Sopenharmony_ci self.maxlen = maxlen 3519ea8026Sopenharmony_ci self.lines = co.deque(maxlen=maxlen) 3619ea8026Sopenharmony_ci self.tail = io.StringIO() 3719ea8026Sopenharmony_ci 3819ea8026Sopenharmony_ci # trigger automatic sizing 3919ea8026Sopenharmony_ci if maxlen == 0: 4019ea8026Sopenharmony_ci self.resize(0) 4119ea8026Sopenharmony_ci 4219ea8026Sopenharmony_ci def write(self, s): 4319ea8026Sopenharmony_ci # note using split here ensures the trailing string has no newline 4419ea8026Sopenharmony_ci lines = s.split('\n') 4519ea8026Sopenharmony_ci 4619ea8026Sopenharmony_ci if len(lines) > 1 and self.tail.getvalue(): 4719ea8026Sopenharmony_ci self.tail.write(lines[0]) 4819ea8026Sopenharmony_ci lines[0] = self.tail.getvalue() 4919ea8026Sopenharmony_ci self.tail = io.StringIO() 5019ea8026Sopenharmony_ci 5119ea8026Sopenharmony_ci self.lines.extend(lines[:-1]) 5219ea8026Sopenharmony_ci 5319ea8026Sopenharmony_ci if lines[-1]: 5419ea8026Sopenharmony_ci self.tail.write(lines[-1]) 5519ea8026Sopenharmony_ci 5619ea8026Sopenharmony_ci def resize(self, maxlen): 5719ea8026Sopenharmony_ci self.maxlen = maxlen 5819ea8026Sopenharmony_ci if maxlen == 0: 5919ea8026Sopenharmony_ci maxlen = shutil.get_terminal_size((80, 5))[1] 6019ea8026Sopenharmony_ci if maxlen != self.lines.maxlen: 6119ea8026Sopenharmony_ci self.lines = co.deque(self.lines, maxlen=maxlen) 6219ea8026Sopenharmony_ci 6319ea8026Sopenharmony_ci canvas_lines = 1 6419ea8026Sopenharmony_ci def draw(self): 6519ea8026Sopenharmony_ci # did terminal size change? 6619ea8026Sopenharmony_ci if self.maxlen == 0: 6719ea8026Sopenharmony_ci self.resize(0) 6819ea8026Sopenharmony_ci 6919ea8026Sopenharmony_ci # first thing first, give ourself a canvas 7019ea8026Sopenharmony_ci while LinesIO.canvas_lines < len(self.lines): 7119ea8026Sopenharmony_ci sys.stdout.write('\n') 7219ea8026Sopenharmony_ci LinesIO.canvas_lines += 1 7319ea8026Sopenharmony_ci 7419ea8026Sopenharmony_ci # clear the bottom of the canvas if we shrink 7519ea8026Sopenharmony_ci shrink = LinesIO.canvas_lines - len(self.lines) 7619ea8026Sopenharmony_ci if shrink > 0: 7719ea8026Sopenharmony_ci for i in range(shrink): 7819ea8026Sopenharmony_ci sys.stdout.write('\r') 7919ea8026Sopenharmony_ci if shrink-1-i > 0: 8019ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % (shrink-1-i)) 8119ea8026Sopenharmony_ci sys.stdout.write('\x1b[K') 8219ea8026Sopenharmony_ci if shrink-1-i > 0: 8319ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dB' % (shrink-1-i)) 8419ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % shrink) 8519ea8026Sopenharmony_ci LinesIO.canvas_lines = len(self.lines) 8619ea8026Sopenharmony_ci 8719ea8026Sopenharmony_ci for i, line in enumerate(self.lines): 8819ea8026Sopenharmony_ci # move cursor, clear line, disable/reenable line wrapping 8919ea8026Sopenharmony_ci sys.stdout.write('\r') 9019ea8026Sopenharmony_ci if len(self.lines)-1-i > 0: 9119ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) 9219ea8026Sopenharmony_ci sys.stdout.write('\x1b[K') 9319ea8026Sopenharmony_ci sys.stdout.write('\x1b[?7l') 9419ea8026Sopenharmony_ci sys.stdout.write(line) 9519ea8026Sopenharmony_ci sys.stdout.write('\x1b[?7h') 9619ea8026Sopenharmony_ci if len(self.lines)-1-i > 0: 9719ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) 9819ea8026Sopenharmony_ci sys.stdout.flush() 9919ea8026Sopenharmony_ci 10019ea8026Sopenharmony_ci 10119ea8026Sopenharmony_cidef main(path='-', *, lines=5, cat=False, sleep=None, keep_open=False): 10219ea8026Sopenharmony_ci if cat: 10319ea8026Sopenharmony_ci ring = sys.stdout 10419ea8026Sopenharmony_ci else: 10519ea8026Sopenharmony_ci ring = LinesIO(lines) 10619ea8026Sopenharmony_ci 10719ea8026Sopenharmony_ci # if sleep print in background thread to avoid getting stuck in a read call 10819ea8026Sopenharmony_ci event = th.Event() 10919ea8026Sopenharmony_ci lock = th.Lock() 11019ea8026Sopenharmony_ci if not cat: 11119ea8026Sopenharmony_ci done = False 11219ea8026Sopenharmony_ci def background(): 11319ea8026Sopenharmony_ci while not done: 11419ea8026Sopenharmony_ci event.wait() 11519ea8026Sopenharmony_ci event.clear() 11619ea8026Sopenharmony_ci with lock: 11719ea8026Sopenharmony_ci ring.draw() 11819ea8026Sopenharmony_ci time.sleep(sleep or 0.01) 11919ea8026Sopenharmony_ci th.Thread(target=background, daemon=True).start() 12019ea8026Sopenharmony_ci 12119ea8026Sopenharmony_ci try: 12219ea8026Sopenharmony_ci while True: 12319ea8026Sopenharmony_ci with openio(path) as f: 12419ea8026Sopenharmony_ci for line in f: 12519ea8026Sopenharmony_ci with lock: 12619ea8026Sopenharmony_ci ring.write(line) 12719ea8026Sopenharmony_ci event.set() 12819ea8026Sopenharmony_ci 12919ea8026Sopenharmony_ci if not keep_open: 13019ea8026Sopenharmony_ci break 13119ea8026Sopenharmony_ci # don't just flood open calls 13219ea8026Sopenharmony_ci time.sleep(sleep or 0.1) 13319ea8026Sopenharmony_ci except FileNotFoundError as e: 13419ea8026Sopenharmony_ci print("error: file not found %r" % path) 13519ea8026Sopenharmony_ci sys.exit(-1) 13619ea8026Sopenharmony_ci except KeyboardInterrupt: 13719ea8026Sopenharmony_ci pass 13819ea8026Sopenharmony_ci 13919ea8026Sopenharmony_ci if not cat: 14019ea8026Sopenharmony_ci done = True 14119ea8026Sopenharmony_ci lock.acquire() # avoids https://bugs.python.org/issue42717 14219ea8026Sopenharmony_ci sys.stdout.write('\n') 14319ea8026Sopenharmony_ci 14419ea8026Sopenharmony_ci 14519ea8026Sopenharmony_ciif __name__ == "__main__": 14619ea8026Sopenharmony_ci import sys 14719ea8026Sopenharmony_ci import argparse 14819ea8026Sopenharmony_ci parser = argparse.ArgumentParser( 14919ea8026Sopenharmony_ci description="Efficiently displays the last n lines of a file/pipe.", 15019ea8026Sopenharmony_ci allow_abbrev=False) 15119ea8026Sopenharmony_ci parser.add_argument( 15219ea8026Sopenharmony_ci 'path', 15319ea8026Sopenharmony_ci nargs='?', 15419ea8026Sopenharmony_ci help="Path to read from.") 15519ea8026Sopenharmony_ci parser.add_argument( 15619ea8026Sopenharmony_ci '-n', '--lines', 15719ea8026Sopenharmony_ci nargs='?', 15819ea8026Sopenharmony_ci type=lambda x: int(x, 0), 15919ea8026Sopenharmony_ci const=0, 16019ea8026Sopenharmony_ci help="Show this many lines of history. 0 uses the terminal height. " 16119ea8026Sopenharmony_ci "Defaults to 5.") 16219ea8026Sopenharmony_ci parser.add_argument( 16319ea8026Sopenharmony_ci '-z', '--cat', 16419ea8026Sopenharmony_ci action='store_true', 16519ea8026Sopenharmony_ci help="Pipe directly to stdout.") 16619ea8026Sopenharmony_ci parser.add_argument( 16719ea8026Sopenharmony_ci '-s', '--sleep', 16819ea8026Sopenharmony_ci type=float, 16919ea8026Sopenharmony_ci help="Seconds to sleep between reads. Defaults to 0.01.") 17019ea8026Sopenharmony_ci parser.add_argument( 17119ea8026Sopenharmony_ci '-k', '--keep-open', 17219ea8026Sopenharmony_ci action='store_true', 17319ea8026Sopenharmony_ci help="Reopen the pipe on EOF, useful when multiple " 17419ea8026Sopenharmony_ci "processes are writing.") 17519ea8026Sopenharmony_ci sys.exit(main(**{k: v 17619ea8026Sopenharmony_ci for k, v in vars(parser.parse_intermixed_args()).items() 17719ea8026Sopenharmony_ci if v is not None})) 178