From b2a2cc9a19bb034e05dd673dc0a0a886e17b8e4b Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Sun, 16 Oct 2022 14:27:14 -0500 Subject: [PATCH] Added teepipe.py and watch.py --- scripts/bench.py | 1 + scripts/code.py | 1 + scripts/cov.py | 1 + scripts/data.py | 1 + scripts/perf.py | 1 + scripts/perfbd.py | 1 + scripts/plot.py | 73 +++++++++-- scripts/prettyasserts.py | 1 + scripts/stack.py | 1 + scripts/struct_.py | 1 + scripts/summary.py | 1 + scripts/tailpipe.py | 60 ++++++--- scripts/teepipe.py | 73 +++++++++++ scripts/test.py | 1 + scripts/tracebd.py | 74 +++++++---- scripts/watch.py | 265 +++++++++++++++++++++++++++++++++++++++ 16 files changed, 509 insertions(+), 47 deletions(-) create mode 100755 scripts/teepipe.py create mode 100755 scripts/watch.py diff --git a/scripts/bench.py b/scripts/bench.py index 178e488c..225e5231 100755 --- a/scripts/bench.py +++ b/scripts/bench.py @@ -36,6 +36,7 @@ PERF_SCRIPT = ['./scripts/perf.py'] def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/code.py b/scripts/code.py index 4054b7a5..b76011f7 100755 --- a/scripts/code.py +++ b/scripts/code.py @@ -125,6 +125,7 @@ class CodeResult(co.namedtuple('CodeResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/cov.py b/scripts/cov.py index e7ad0b3c..fcd2ecfb 100755 --- a/scripts/cov.py +++ b/scripts/cov.py @@ -200,6 +200,7 @@ class CovResult(co.namedtuple('CovResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/data.py b/scripts/data.py index 7b345081..c8800551 100755 --- a/scripts/data.py +++ b/scripts/data.py @@ -125,6 +125,7 @@ class DataResult(co.namedtuple('DataResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/perf.py b/scripts/perf.py index f265e5f2..373dda21 100755 --- a/scripts/perf.py +++ b/scripts/perf.py @@ -146,6 +146,7 @@ class PerfResult(co.namedtuple('PerfResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/perfbd.py b/scripts/perfbd.py index 179b69ef..43f43afc 100755 --- a/scripts/perfbd.py +++ b/scripts/perfbd.py @@ -132,6 +132,7 @@ class PerfBdResult(co.namedtuple('PerfBdResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/plot.py b/scripts/plot.py index db8191a2..24580930 100755 --- a/scripts/plot.py +++ b/scripts/plot.py @@ -18,6 +18,12 @@ import os import shutil import time +try: + import inotify_simple +except ModuleNotFoundError: + inotify_simple = None + + COLORS = [ '1;34', # bold blue '1;31', # bold red @@ -79,6 +85,7 @@ def si(x, w=4): return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) @@ -87,6 +94,31 @@ def openio(path, mode='r', buffering=-1): else: return open(path, mode, buffering) +def inotifywait(paths): + # wait for interesting events + inotify = inotify_simple.INotify() + flags = (inotify_simple.flags.ATTRIB + | inotify_simple.flags.CREATE + | inotify_simple.flags.DELETE + | inotify_simple.flags.DELETE_SELF + | inotify_simple.flags.MODIFY + | inotify_simple.flags.MOVED_FROM + | inotify_simple.flags.MOVED_TO + | inotify_simple.flags.MOVE_SELF) + + # recurse into directories + for path in paths: + if os.path.isdir(path): + for dir, _, files in os.walk(path): + inotify.add_watch(dir, flags) + for f in files: + inotify.add_watch(os.path.join(dir, f), flags) + else: + inotify.add_watch(path, flags) + + # wait for event + inotify.read() + class LinesIO: def __init__(self, maxlen=None): self.maxlen = maxlen @@ -118,28 +150,41 @@ class LinesIO: if maxlen != self.lines.maxlen: self.lines = co.deque(self.lines, maxlen=maxlen) - last_lines = 1 + canvas_lines = 1 def draw(self): # did terminal size change? if self.maxlen == 0: self.resize(0) # first thing first, give ourself a canvas - while LinesIO.last_lines < len(self.lines): + while LinesIO.canvas_lines < len(self.lines): sys.stdout.write('\n') - LinesIO.last_lines += 1 + LinesIO.canvas_lines += 1 - for j, line in enumerate(self.lines): + # clear the bottom of the canvas if we shrink + shrink = LinesIO.canvas_lines - len(self.lines) + if shrink > 0: + for i in range(shrink): + sys.stdout.write('\r') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dA' % (shrink-1-i)) + sys.stdout.write('\x1b[K') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dB' % (shrink-1-i)) + sys.stdout.write('\x1b[%dA' % shrink) + LinesIO.canvas_lines = len(self.lines) + + for i, line in enumerate(self.lines): # move cursor, clear line, disable/reenable line wrapping sys.stdout.write('\r') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) sys.stdout.write('\x1b[K') sys.stdout.write('\x1b[?7l') sys.stdout.write(line) sys.stdout.write('\x1b[?7h') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) sys.stdout.flush() @@ -697,8 +742,16 @@ def main(csv_paths, *, ring = LinesIO() draw(ring) ring.draw() - # don't just flood open calls - time.sleep(sleep or 0.1) + + # try to inotifywait + if inotify_simple is not None: + ptime = time.time() + inotifywait(csv_paths) + # sleep for a minimum amount of time, this helps issues + # around rapidly updating files + time.sleep(max(0, (sleep or 0.01) - (time.time()-ptime))) + else: + time.sleep(sleep or 0.1) except KeyboardInterrupt: pass diff --git a/scripts/prettyasserts.py b/scripts/prettyasserts.py index 8f2212b5..b10fdda1 100755 --- a/scripts/prettyasserts.py +++ b/scripts/prettyasserts.py @@ -43,6 +43,7 @@ LEXEMES = { def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/stack.py b/scripts/stack.py index 9e7d08d2..b455c824 100755 --- a/scripts/stack.py +++ b/scripts/stack.py @@ -119,6 +119,7 @@ class StackResult(co.namedtuple('StackResult', [ def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/struct_.py b/scripts/struct_.py index 540cbe5d..4dbc747f 100755 --- a/scripts/struct_.py +++ b/scripts/struct_.py @@ -119,6 +119,7 @@ class StructResult(co.namedtuple('StructResult', ['file', 'struct', 'size'])): def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/summary.py b/scripts/summary.py index dd37aefd..a0be3a8d 100755 --- a/scripts/summary.py +++ b/scripts/summary.py @@ -546,6 +546,7 @@ def table(Result, results, diff_results=None, *, def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/tailpipe.py b/scripts/tailpipe.py index 30eeb82d..802f74d4 100755 --- a/scripts/tailpipe.py +++ b/scripts/tailpipe.py @@ -12,12 +12,15 @@ import collections as co import io import os +import select import shutil import sys +import threading as th import time def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) @@ -57,48 +60,71 @@ class LinesIO: if maxlen != self.lines.maxlen: self.lines = co.deque(self.lines, maxlen=maxlen) - last_lines = 1 + canvas_lines = 1 def draw(self): # did terminal size change? if self.maxlen == 0: self.resize(0) # first thing first, give ourself a canvas - while LinesIO.last_lines < len(self.lines): + while LinesIO.canvas_lines < len(self.lines): sys.stdout.write('\n') - LinesIO.last_lines += 1 + LinesIO.canvas_lines += 1 - for j, line in enumerate(self.lines): + # clear the bottom of the canvas if we shrink + shrink = LinesIO.canvas_lines - len(self.lines) + if shrink > 0: + for i in range(shrink): + sys.stdout.write('\r') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dA' % (shrink-1-i)) + sys.stdout.write('\x1b[K') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dB' % (shrink-1-i)) + sys.stdout.write('\x1b[%dA' % shrink) + LinesIO.canvas_lines = len(self.lines) + + for i, line in enumerate(self.lines): # move cursor, clear line, disable/reenable line wrapping sys.stdout.write('\r') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) sys.stdout.write('\x1b[K') sys.stdout.write('\x1b[?7l') sys.stdout.write(line) sys.stdout.write('\x1b[?7h') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) sys.stdout.flush() -def main(path='-', *, lines=5, cat=False, sleep=0.01, keep_open=False): +def main(path='-', *, lines=5, cat=False, sleep=None, keep_open=False): if cat: ring = sys.stdout else: ring = LinesIO(lines) - ptime = time.time() + # if sleep print in background thread to avoid getting stuck in a read call + event = th.Event() + lock = th.Lock() + if not cat: + done = False + def background(): + while not done: + event.wait() + event.clear() + with lock: + ring.draw() + time.sleep(sleep or 0.01) + th.Thread(target=background, daemon=True).start() + try: while True: with openio(path) as f: for line in f: - ring.write(line) - - # need to redraw? - if not cat and time.time()-ptime >= sleep: - ring.draw() - ptime = time.time() + with lock: + ring.write(line) + event.set() if not keep_open: break @@ -111,6 +137,8 @@ def main(path='-', *, lines=5, cat=False, sleep=0.01, keep_open=False): pass if not cat: + done = True + lock.acquire() # avoids https://bugs.python.org/issue42717 sys.stdout.write('\n') diff --git a/scripts/teepipe.py b/scripts/teepipe.py new file mode 100755 index 00000000..ee32e44b --- /dev/null +++ b/scripts/teepipe.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# +# tee, but for pipes +# +# Example: +# ./scripts/tee.py in_pipe out_pipe1 out_pipe2 +# +# Copyright (c) 2022, The littlefs authors. +# SPDX-License-Identifier: BSD-3-Clause +# + +import os +import io +import time +import sys + + +def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout + if path == '-': + if mode == 'r': + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def main(in_path, out_paths, *, keep_open=False): + out_pipes = [openio(p, 'wb', 0) for p in out_paths] + try: + with openio(in_path, 'rb', 0) as f: + while True: + buf = f.read(io.DEFAULT_BUFFER_SIZE) + if not buf: + if not keep_open: + break + # don't just flood reads + time.sleep(0.1) + continue + + for p in out_pipes: + try: + p.write(buf) + except BrokenPipeError: + pass + except FileNotFoundError as e: + print("error: file not found %r" % in_path) + sys.exit(-1) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + import sys + import argparse + parser = argparse.ArgumentParser( + description="tee, but for pipes.", + allow_abbrev=False) + parser.add_argument( + 'in_path', + help="Path to read from.") + parser.add_argument( + 'out_paths', + nargs='+', + help="Path to write to.") + parser.add_argument( + '-k', '--keep-open', + action='store_true', + help="Reopen the pipe on EOF, useful when multiple " + "processes are writing.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/test.py b/scripts/test.py index 15be268c..5ceb4208 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -36,6 +36,7 @@ PERF_SCRIPT = ['./scripts/perf.py'] def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) diff --git a/scripts/tracebd.py b/scripts/tracebd.py index f3662599..ecf49a7c 100755 --- a/scripts/tracebd.py +++ b/scripts/tracebd.py @@ -17,10 +17,10 @@ import math as m import os import re import shutil +import threading as th import time - CHARS = 'rpe.' COLORS = ['42', '45', '44', ''] @@ -42,6 +42,7 @@ CHARS_BRAILLE = ( def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout if path == '-': if mode == 'r': return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) @@ -81,32 +82,44 @@ class LinesIO: if maxlen != self.lines.maxlen: self.lines = co.deque(self.lines, maxlen=maxlen) - last_lines = 1 + canvas_lines = 1 def draw(self): # did terminal size change? if self.maxlen == 0: self.resize(0) # first thing first, give ourself a canvas - while LinesIO.last_lines < len(self.lines): + while LinesIO.canvas_lines < len(self.lines): sys.stdout.write('\n') - LinesIO.last_lines += 1 + LinesIO.canvas_lines += 1 - for j, line in enumerate(self.lines): + # clear the bottom of the canvas if we shrink + shrink = LinesIO.canvas_lines - len(self.lines) + if shrink > 0: + for i in range(shrink): + sys.stdout.write('\r') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dA' % (shrink-1-i)) + sys.stdout.write('\x1b[K') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dB' % (shrink-1-i)) + sys.stdout.write('\x1b[%dA' % shrink) + LinesIO.canvas_lines = len(self.lines) + + for i, line in enumerate(self.lines): # move cursor, clear line, disable/reenable line wrapping sys.stdout.write('\r') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) sys.stdout.write('\x1b[K') sys.stdout.write('\x1b[?7l') sys.stdout.write(line) sys.stdout.write('\x1b[?7h') - if len(self.lines)-1-j > 0: - sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-j)) + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) sys.stdout.flush() - # space filling Hilbert-curve # # note we memoize the last curve since this is a bit expensive @@ -801,23 +814,39 @@ def main(path='-', *, else: ring = LinesIO(lines) - ptime = time.time() + # if sleep print in background thread to avoid getting stuck in a read call + event = th.Event() + lock = th.Lock() + if sleep: + done = False + def background(): + while not done: + event.wait() + event.clear() + with lock: + draw(ring) + if not cat: + ring.draw() + time.sleep(sleep or 0.01) + th.Thread(target=background, daemon=True).start() + try: while True: with openio(path) as f: changed = 0 for line in f: - changed += parse(line) + with lock: + changed += parse(line) - # need to redraw? - if (changed - and (not coalesce or changed >= coalesce) - and (not sleep or time.time()-ptime >= sleep)): - draw(ring) - if not cat: - ring.draw() - changed = 0 - ptime = time.time() + # need to redraw? + if changed and (not coalesce or changed >= coalesce): + if sleep: + event.set() + else: + draw(ring) + if not cat: + ring.draw() + changed = 0 if not keep_open: break @@ -829,6 +858,9 @@ def main(path='-', *, except KeyboardInterrupt: pass + if sleep: + done = True + lock.acquire() # avoids https://bugs.python.org/issue42717 if not cat: sys.stdout.write('\n') diff --git a/scripts/watch.py b/scripts/watch.py new file mode 100755 index 00000000..dff06011 --- /dev/null +++ b/scripts/watch.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# +# Traditional watch command, but with higher resolution updates and a bit +# different options/output format +# +# Example: +# ./scripts/watch.py -s0.1 date +# +# Copyright (c) 2022, The littlefs authors. +# SPDX-License-Identifier: BSD-3-Clause +# + +import collections as co +import errno +import fcntl +import io +import os +import pty +import re +import shutil +import struct +import subprocess as sp +import sys +import termios +import time + +try: + import inotify_simple +except ModuleNotFoundError: + inotify_simple = None + + +def openio(path, mode='r', buffering=-1): + # allow '-' for stdin/stdout + if path == '-': + if mode == 'r': + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def inotifywait(paths): + # wait for interesting events + inotify = inotify_simple.INotify() + flags = (inotify_simple.flags.ATTRIB + | inotify_simple.flags.CREATE + | inotify_simple.flags.DELETE + | inotify_simple.flags.DELETE_SELF + | inotify_simple.flags.MODIFY + | inotify_simple.flags.MOVED_FROM + | inotify_simple.flags.MOVED_TO + | inotify_simple.flags.MOVE_SELF) + + # recurse into directories + for path in paths: + if os.path.isdir(path): + for dir, _, files in os.walk(path): + inotify.add_watch(dir, flags) + for f in files: + inotify.add_watch(os.path.join(dir, f), flags) + else: + inotify.add_watch(path, flags) + + # wait for event + inotify.read() + +class LinesIO: + def __init__(self, maxlen=None): + self.maxlen = maxlen + self.lines = co.deque(maxlen=maxlen) + self.tail = io.StringIO() + + # trigger automatic sizing + if maxlen == 0: + self.resize(0) + + def write(self, s): + # note using split here ensures the trailing string has no newline + lines = s.split('\n') + + if len(lines) > 1 and self.tail.getvalue(): + self.tail.write(lines[0]) + lines[0] = self.tail.getvalue() + self.tail = io.StringIO() + + self.lines.extend(lines[:-1]) + + if lines[-1]: + self.tail.write(lines[-1]) + + def resize(self, maxlen): + self.maxlen = maxlen + if maxlen == 0: + maxlen = shutil.get_terminal_size((80, 5))[1] + if maxlen != self.lines.maxlen: + self.lines = co.deque(self.lines, maxlen=maxlen) + + canvas_lines = 1 + def draw(self): + # did terminal size change? + if self.maxlen == 0: + self.resize(0) + + # first thing first, give ourself a canvas + while LinesIO.canvas_lines < len(self.lines): + sys.stdout.write('\n') + LinesIO.canvas_lines += 1 + + # clear the bottom of the canvas if we shrink + shrink = LinesIO.canvas_lines - len(self.lines) + if shrink > 0: + for i in range(shrink): + sys.stdout.write('\r') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dA' % (shrink-1-i)) + sys.stdout.write('\x1b[K') + if shrink-1-i > 0: + sys.stdout.write('\x1b[%dB' % (shrink-1-i)) + sys.stdout.write('\x1b[%dA' % shrink) + LinesIO.canvas_lines = len(self.lines) + + for i, line in enumerate(self.lines): + # move cursor, clear line, disable/reenable line wrapping + sys.stdout.write('\r') + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) + sys.stdout.write('\x1b[K') + sys.stdout.write('\x1b[?7l') + sys.stdout.write(line) + sys.stdout.write('\x1b[?7h') + if len(self.lines)-1-i > 0: + sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) + sys.stdout.flush() + + +def main(command, *, + lines=0, + cat=False, + sleep=None, + keep_open=False, + keep_open_paths=None, + exit_on_error=False): + returncode = 0 + try: + while True: + # reset ring each run + if cat: + ring = sys.stdout + else: + ring = LinesIO(lines) + + try: + # run the command under a pseudoterminal + mpty, spty = pty.openpty() + + # forward terminal size + w, h = shutil.get_terminal_size((80, 5)) + if lines: + h = lines + fcntl.ioctl(spty, termios.TIOCSWINSZ, + struct.pack('HHHH', h, w, 0, 0)) + + proc = sp.Popen(command, + stdout=spty, + stderr=spty, + close_fds=False) + os.close(spty) + mpty = os.fdopen(mpty, 'r', 1) + + while True: + try: + line = mpty.readline() + except OSError as e: + if e.errno != errno.EIO: + raise + break + if not line: + break + + ring.write(line) + if not cat: + ring.draw() + + mpty.close() + proc.wait() + if exit_on_error and proc.returncode != 0: + returncode = proc.returncode + break + except OSError as e: + if e.errno != errno.ETXTBSY: + raise + pass + + # try to inotifywait + if keep_open and inotify_simple is not None: + if keep_open_paths: + paths = set(keep_paths) + else: + # guess inotify paths from command + paths = set() + for p in command: + for p in { + p, + re.sub('^-.', '', p), + re.sub('^--[^=]+=', '', p)}: + if p and os.path.exists(p): + paths.add(p) + ptime = time.time() + inotifywait(paths) + # sleep for a minimum amount of time, this helps issues around + # rapidly updating files + time.sleep(max(0, (sleep or 0.1) - (time.time()-ptime))) + else: + time.sleep(sleep or 0.1) + except KeyboardInterrupt: + pass + + if not cat: + sys.stdout.write('\n') + sys.exit(returncode) + + +if __name__ == "__main__": + import sys + import argparse + parser = argparse.ArgumentParser( + description="Traditional watch command, but with higher resolution " + "updates and a bit different options/output format.", + allow_abbrev=False) + parser.add_argument( + 'command', + nargs=argparse.REMAINDER, + help="Command to run.") + parser.add_argument( + '-n', '--lines', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Show this many lines of history. 0 uses the terminal height. " + "Defaults to 0.") + parser.add_argument( + '-z', '--cat', + action='store_true', + help="Pipe directly to stdout.") + parser.add_argument( + '-s', '--sleep', + type=float, + help="Seconds to sleep between runs. Defaults to 0.1.") + parser.add_argument( + '-k', '--keep-open', + action='store_true', + help="Try to use inotify to wait for changes.") + parser.add_argument( + '-K', '--keep-open-path', + dest='keep_open_paths', + action='append', + help="Use this path for inotify. Defaults to guessing.") + parser.add_argument( + '-e', '--exit-on-error', + action='store_true', + help="Exit on error.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_args()).items() + if v is not None}))