Browse Source

Added teepipe.py and watch.py

Christopher Haster 3 years ago
parent
commit
b2a2cc9a19

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 64 - 11
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
-
-        for j, line in enumerate(self.lines):
+            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-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
 

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 1 - 0
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)

+ 45 - 17
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
-
-        for j, line in enumerate(self.lines):
+            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-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')
 
 

+ 73 - 0
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}))

+ 1 - 0
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)

+ 55 - 23
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
-
-        for j, line in enumerate(self.lines):
+            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-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)
-
-                    # 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()
+                    with lock:
+                        changed += parse(line)
+
+                        # 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')
 

+ 265 - 0
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}))