tailpipe.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. import threading as th
  5. import time
  6. def openio(path, mode='r'):
  7. if path == '-':
  8. if 'r' in mode:
  9. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  10. else:
  11. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  12. else:
  13. return open(path, mode)
  14. def main(path, lines=1, keep_open=False):
  15. ring = [None] * lines
  16. i = 0
  17. count = 0
  18. lock = th.Lock()
  19. event = th.Event()
  20. done = False
  21. # do the actual reading in a background thread
  22. def read():
  23. nonlocal i
  24. nonlocal count
  25. nonlocal done
  26. while True:
  27. with openio(path, 'r') as f:
  28. for line in f:
  29. with lock:
  30. ring[i] = line
  31. i = (i + 1) % lines
  32. count = min(lines, count + 1)
  33. event.set()
  34. if not keep_open:
  35. break
  36. done = True
  37. th.Thread(target=read, daemon=True).start()
  38. try:
  39. last_count = 1
  40. while not done:
  41. time.sleep(0.01)
  42. event.wait()
  43. event.clear()
  44. # create a copy to avoid corrupt output
  45. with lock:
  46. ring_ = ring.copy()
  47. i_ = i
  48. count_ = count
  49. # first thing first, give ourself a canvas
  50. while last_count < count_:
  51. sys.stdout.write('\n')
  52. last_count += 1
  53. for j in range(count_):
  54. # move cursor, clear line, disable/reenable line wrapping
  55. sys.stdout.write('\r%s\x1b[K\x1b[?7l%s\x1b[?7h%s' % (
  56. '\x1b[%dA' % (count_-1-j) if count_-1-j > 0 else '',
  57. ring_[(i_-count+j) % lines][:-1],
  58. '\x1b[%dB' % (count_-1-j) if count_-1-j > 0 else ''))
  59. sys.stdout.flush()
  60. except KeyboardInterrupt:
  61. pass
  62. sys.stdout.write('\n')
  63. if __name__ == "__main__":
  64. import sys
  65. import argparse
  66. parser = argparse.ArgumentParser(
  67. description="Efficiently displays the last n lines of a file/pipe.")
  68. parser.add_argument(
  69. 'path',
  70. nargs='?',
  71. default='-',
  72. help="Path to read from.")
  73. parser.add_argument(
  74. '-n',
  75. '--lines',
  76. type=lambda x: int(x, 0),
  77. default=1,
  78. help="Number of lines to show, defaults to 1.")
  79. parser.add_argument(
  80. '-k',
  81. '--keep-open',
  82. action='store_true',
  83. help="Reopen the pipe on EOF, useful when multiple "
  84. "processes are writing.")
  85. sys.exit(main(**{k: v
  86. for k, v in vars(parser.parse_args()).items()
  87. if v is not None}))