tracebd.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. #!/usr/bin/env python3
  2. #
  3. # Display operations on block devices based on trace output
  4. #
  5. import collections as co
  6. import itertools as it
  7. import math as m
  8. import os
  9. import re
  10. import shutil
  11. import threading as th
  12. import time
  13. def openio(path, mode='r'):
  14. if path == '-':
  15. if 'r' in mode:
  16. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  17. else:
  18. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  19. else:
  20. return open(path, mode)
  21. # space filling Hilbert-curve
  22. def hilbert_curve(width, height):
  23. # memoize the last curve
  24. if getattr(hilbert_curve, 'last', (None,))[0] == (width, height):
  25. return hilbert_curve.last[1]
  26. # based on generalized Hilbert curves:
  27. # https://github.com/jakubcerveny/gilbert
  28. #
  29. def hilbert_(x, y, a_x, a_y, b_x, b_y):
  30. w = abs(a_x+a_y)
  31. h = abs(b_x+b_y)
  32. a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0
  33. a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0
  34. b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0
  35. b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0
  36. # trivial row
  37. if h == 1:
  38. for _ in range(w):
  39. yield (x,y)
  40. x, y = x+a_dx, y+a_dy
  41. return
  42. # trivial column
  43. if w == 1:
  44. for _ in range(h):
  45. yield (x,y)
  46. x, y = x+b_dx, y+b_dy
  47. return
  48. a_x_, a_y_ = a_x//2, a_y//2
  49. b_x_, b_y_ = b_x//2, b_y//2
  50. w_ = abs(a_x_+a_y_)
  51. h_ = abs(b_x_+b_y_)
  52. if 2*w > 3*h:
  53. # prefer even steps
  54. if w_ % 2 != 0 and w > 2:
  55. a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy
  56. # split in two
  57. yield from hilbert_(x, y, a_x_, a_y_, b_x, b_y)
  58. yield from hilbert_(x+a_x_, y+a_y_, a_x-a_x_, a_y-a_y_, b_x, b_y)
  59. else:
  60. # prefer even steps
  61. if h_ % 2 != 0 and h > 2:
  62. b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy
  63. # split in three
  64. yield from hilbert_(x, y, b_x_, b_y_, a_x_, a_y_)
  65. yield from hilbert_(x+b_x_, y+b_y_, a_x, a_y, b_x-b_x_, b_y-b_y_)
  66. yield from hilbert_(
  67. x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy),
  68. -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_))
  69. if width >= height:
  70. curve = hilbert_(0, 0, +width, 0, 0, +height)
  71. else:
  72. curve = hilbert_(0, 0, 0, +height, +width, 0)
  73. curve = list(curve)
  74. hilbert_curve.last = ((width, height), curve)
  75. return curve
  76. # space filling Z-curve/Lebesgue-curve
  77. def lebesgue_curve(width, height):
  78. # memoize the last curve
  79. if getattr(lebesgue_curve, 'last', (None,))[0] == (width, height):
  80. return lebesgue_curve.last[1]
  81. # we create a truncated Z-curve by simply filtering out the points
  82. # that are outside our region
  83. curve = []
  84. for i in range(2**(2*m.ceil(m.log2(max(width, height))))):
  85. # we just operate on binary strings here because it's easier
  86. b = '{:0{}b}'.format(i, 2*m.ceil(m.log2(i+1)/2))
  87. x = int(b[1::2], 2) if b[1::2] else 0
  88. y = int(b[0::2], 2) if b[0::2] else 0
  89. if x < width and y < height:
  90. curve.append((x, y))
  91. lebesgue_curve.last = ((width, height), curve)
  92. return curve
  93. class Block:
  94. def __init__(self, wear=0, readed=False, proged=False, erased=False):
  95. self._ = ((wear << 3)
  96. | (1 if readed else 0)
  97. | (2 if proged else 0)
  98. | (4 if erased else False))
  99. @property
  100. def wear(self):
  101. return self._ >> 3
  102. @property
  103. def readed(self):
  104. return (self._ & 1) != 0
  105. @property
  106. def proged(self):
  107. return (self._ & 2) != 0
  108. @property
  109. def erased(self):
  110. return (self._ & 4) != 0
  111. def read(self):
  112. self._ |= 1
  113. def prog(self):
  114. self._ |= 2
  115. def erase(self):
  116. self._ = (self._ | 4) + 8
  117. def clear(self):
  118. self._ &= ~7
  119. def reset(self):
  120. self._ = 0
  121. def copy(self):
  122. return Block(self.wear, self.readed, self.proged, self.erased)
  123. def __add__(self, other):
  124. return Block(
  125. max(self.wear, other.wear),
  126. self.readed | other.readed,
  127. self.proged | other.proged,
  128. self.erased | other.erased)
  129. def draw(self,
  130. ascii=False,
  131. chars=None,
  132. wear_chars=None,
  133. color='always',
  134. read=True,
  135. prog=True,
  136. erase=True,
  137. wear=False,
  138. max_wear=None,
  139. block_cycles=None):
  140. if not chars: chars = '.rpe'
  141. c = chars[0]
  142. f = []
  143. if wear:
  144. if not wear_chars and ascii: wear_chars = '0123456789'
  145. elif not wear_chars: wear_chars = '.₁₂₃₄₅₆789'
  146. if block_cycles:
  147. w = self.wear / block_cycles
  148. else:
  149. w = self.wear / max(max_wear, len(wear_chars)-1)
  150. c = wear_chars[min(
  151. int(w*(len(wear_chars)-1)),
  152. len(wear_chars)-1)]
  153. if color == 'wear' or (
  154. color == 'always' and not read and not prog and not erase):
  155. if w*9 >= 9: f.append('\x1b[1;31m')
  156. elif w*9 >= 7: f.append('\x1b[35m')
  157. if erase and self.erased: c = chars[3]
  158. elif prog and self.proged: c = chars[2]
  159. elif read and self.readed: c = chars[1]
  160. if color == 'ops' or color == 'always':
  161. if erase and self.erased: f.append('\x1b[44m')
  162. elif prog and self.proged: f.append('\x1b[45m')
  163. elif read and self.readed: f.append('\x1b[42m')
  164. if color in ['always', 'wear', 'ops'] and f:
  165. return '%s%c\x1b[m' % (''.join(f), c)
  166. else:
  167. return c
  168. class Bd:
  169. def __init__(self, *, blocks=None, size=1, count=1, width=80):
  170. if blocks is not None:
  171. self.blocks = blocks
  172. self.size = size
  173. self.count = count
  174. self.width = width
  175. else:
  176. self.blocks = []
  177. self.size = None
  178. self.count = None
  179. self.width = None
  180. self.smoosh(size=size, count=count, width=width)
  181. def get(self, block=slice(None), off=slice(None)):
  182. if not isinstance(block, slice):
  183. block = slice(block, block+1)
  184. if not isinstance(off, slice):
  185. off = slice(off, off+1)
  186. if (not self.blocks
  187. or not self.width
  188. or not self.size
  189. or not self.count):
  190. return
  191. if self.count >= self.width:
  192. scale = (self.count+self.width-1) // self.width
  193. for i in range(
  194. (block.start if block.start is not None else 0)//scale,
  195. (min(block.stop if block.stop is not None else self.count,
  196. self.count)+scale-1)//scale):
  197. yield self.blocks[i]
  198. else:
  199. scale = self.width // self.count
  200. for i in range(
  201. block.start if block.start is not None else 0,
  202. min(block.stop if block.stop is not None else self.count,
  203. self.count)):
  204. for j in range(
  205. ((off.start if off.start is not None else 0)
  206. *scale)//self.size,
  207. (min(off.stop if off.stop is not None else self.size,
  208. self.size)*scale+self.size-1)//self.size):
  209. yield self.blocks[i*scale+j]
  210. def __getitem__(self, block=slice(None), off=slice(None)):
  211. if isinstance(block, tuple):
  212. block, off = block
  213. if not isinstance(block, slice):
  214. block = slice(block, block+1)
  215. if not isinstance(off, slice):
  216. off = slice(off, off+1)
  217. # needs resize?
  218. if ((block.stop is not None and block.stop > self.count)
  219. or (off.stop is not None and off.stop > self.size)):
  220. self.smoosh(
  221. count=max(block.stop or self.count, self.count),
  222. size=max(off.stop or self.size, self.size))
  223. return self.get(block, off)
  224. def smoosh(self, *, size=None, count=None, width=None):
  225. size = size or self.size
  226. count = count or self.count
  227. width = width or self.width
  228. if count >= width:
  229. scale = (count+width-1) // width
  230. self.blocks = [
  231. sum(self.get(slice(i,i+scale)), start=Block())
  232. for i in range(0, count, scale)]
  233. else:
  234. scale = width // count
  235. self.blocks = [
  236. sum(self.get(i, slice(j*(size//width),(j+1)*(size//width))),
  237. start=Block())
  238. for i in range(0, count)
  239. for j in range(scale)]
  240. self.size = size
  241. self.count = count
  242. self.width = width
  243. def read(self, block=slice(None), off=slice(None)):
  244. for c in self[block, off]:
  245. c.read()
  246. def prog(self, block=slice(None), off=slice(None)):
  247. for c in self[block, off]:
  248. c.prog()
  249. def erase(self, block=slice(None), off=slice(None)):
  250. for c in self[block, off]:
  251. c.erase()
  252. def clear(self, block=slice(None), off=slice(None)):
  253. for c in self[block, off]:
  254. c.clear()
  255. def reset(self, block=slice(None), off=slice(None)):
  256. for c in self[block, off]:
  257. c.reset()
  258. def copy(self):
  259. return Bd(
  260. blocks=[b.copy() for b in self.blocks],
  261. size=self.size, count=self.count, width=self.width)
  262. def main(path='-', *,
  263. read=False,
  264. prog=False,
  265. erase=False,
  266. wear=False,
  267. reset=False,
  268. ascii=False,
  269. chars=None,
  270. wear_chars=None,
  271. color='auto',
  272. block=(None,None),
  273. off=(None,None),
  274. block_size=None,
  275. block_count=None,
  276. block_cycles=None,
  277. width=None,
  278. height=1,
  279. scale=None,
  280. lines=None,
  281. coalesce=None,
  282. sleep=None,
  283. hilbert=False,
  284. lebesgue=False,
  285. keep_open=False):
  286. if not read and not prog and not erase and not wear:
  287. read = True
  288. prog = True
  289. erase = True
  290. if color == 'auto':
  291. color = 'always' if sys.stdout.isatty() else 'never'
  292. block_start = block[0]
  293. block_stop = block[1] if len(block) > 1 else block[0]+1
  294. off_start = off[0]
  295. off_stop = off[1] if len(off) > 1 else off[0]+1
  296. if block_start is None:
  297. block_start = 0
  298. if block_stop is None and block_count is not None:
  299. block_stop = block_count
  300. if off_start is None:
  301. off_start = 0
  302. if off_stop is None and block_size is not None:
  303. off_stop = block_size
  304. bd = Bd(
  305. size=(block_size if block_size is not None
  306. else off_stop-off_start if off_stop is not None
  307. else 1),
  308. count=(block_count if block_count is not None
  309. else block_stop-block_start if block_stop is not None
  310. else 1),
  311. width=(width or 80)*height)
  312. lock = th.Lock()
  313. event = th.Event()
  314. done = False
  315. # adjust width?
  316. def resmoosh():
  317. if width is None:
  318. w = shutil.get_terminal_size((80, 0))[0] * height
  319. elif width == 0:
  320. w = max(int(bd.count*(scale or 1)), 1)
  321. else:
  322. w = width * height
  323. if scale and int(bd.count*scale) > w:
  324. c = int(w/scale)
  325. elif scale and int(bd.count*scale) < w:
  326. w = max(int(bd.count*(scale or 1)), 1)
  327. c = bd.count
  328. else:
  329. c = bd.count
  330. if w != bd.width or c != bd.count:
  331. bd.smoosh(width=w, count=c)
  332. resmoosh()
  333. # parse a line of trace output
  334. pattern = re.compile(
  335. 'trace.*?bd_(?:'
  336. '(?P<create>create\w*)\('
  337. '(?:'
  338. 'block_size=(?P<block_size>\w+)'
  339. '|' 'block_count=(?P<block_count>\w+)'
  340. '|' '.*?' ')*' '\)'
  341. '|' '(?P<read>read)\('
  342. '\s*(?P<read_ctx>\w+)\s*' ','
  343. '\s*(?P<read_block>\w+)\s*' ','
  344. '\s*(?P<read_off>\w+)\s*' ','
  345. '\s*(?P<read_buffer>\w+)\s*' ','
  346. '\s*(?P<read_size>\w+)\s*' '\)'
  347. '|' '(?P<prog>prog)\('
  348. '\s*(?P<prog_ctx>\w+)\s*' ','
  349. '\s*(?P<prog_block>\w+)\s*' ','
  350. '\s*(?P<prog_off>\w+)\s*' ','
  351. '\s*(?P<prog_buffer>\w+)\s*' ','
  352. '\s*(?P<prog_size>\w+)\s*' '\)'
  353. '|' '(?P<erase>erase)\('
  354. '\s*(?P<erase_ctx>\w+)\s*' ','
  355. '\s*(?P<erase_block>\w+)\s*' '\)'
  356. '|' '(?P<sync>sync)\('
  357. '\s*(?P<sync_ctx>\w+)\s*' '\)' ')')
  358. def parse_line(line):
  359. # string searching is actually much faster than
  360. # the regex here
  361. if 'trace' not in line or 'bd' not in line:
  362. return False
  363. m = pattern.search(line)
  364. if not m:
  365. return False
  366. if m.group('create'):
  367. # update our block size/count
  368. size = int(m.group('block_size'), 0)
  369. count = int(m.group('block_count'), 0)
  370. if off_stop is not None:
  371. size = off_stop-off_start
  372. if block_stop is not None:
  373. count = block_stop-block_start
  374. with lock:
  375. if reset:
  376. bd.reset()
  377. # ignore the new values if block_stop/off_stop is explicit
  378. bd.smoosh(
  379. size=(size if off_stop is None
  380. else off_stop-off_start),
  381. count=(count if block_stop is None
  382. else block_stop-block_start))
  383. return True
  384. elif m.group('read') and read:
  385. block = int(m.group('read_block'), 0)
  386. off = int(m.group('read_off'), 0)
  387. size = int(m.group('read_size'), 0)
  388. if block_stop is not None and block >= block_stop:
  389. return False
  390. block -= block_start
  391. if off_stop is not None:
  392. if off >= off_stop:
  393. return False
  394. size = min(size, off_stop-off)
  395. off -= off_start
  396. with lock:
  397. bd.read(block, slice(off,off+size))
  398. return True
  399. elif m.group('prog') and prog:
  400. block = int(m.group('prog_block'), 0)
  401. off = int(m.group('prog_off'), 0)
  402. size = int(m.group('prog_size'), 0)
  403. if block_stop is not None and block >= block_stop:
  404. return False
  405. block -= block_start
  406. if off_stop is not None:
  407. if off >= off_stop:
  408. return False
  409. size = min(size, off_stop-off)
  410. off -= off_start
  411. with lock:
  412. bd.prog(block, slice(off,off+size))
  413. return True
  414. elif m.group('erase') and (erase or wear):
  415. block = int(m.group('erase_block'), 0)
  416. if block_stop is not None and block >= block_stop:
  417. return False
  418. block -= block_start
  419. with lock:
  420. bd.erase(block)
  421. return True
  422. else:
  423. return False
  424. # print a pretty line of trace output
  425. history = []
  426. def push_line():
  427. # create copy to avoid corrupt output
  428. with lock:
  429. resmoosh()
  430. bd_ = bd.copy()
  431. bd.clear()
  432. max_wear = None
  433. if wear:
  434. max_wear = max(b.wear for b in bd_.blocks)
  435. def draw(b):
  436. return b.draw(
  437. ascii=ascii,
  438. chars=chars,
  439. wear_chars=wear_chars,
  440. color=color,
  441. read=read,
  442. prog=prog,
  443. erase=erase,
  444. wear=wear,
  445. max_wear=max_wear,
  446. block_cycles=block_cycles)
  447. # fold via a curve?
  448. if height > 1:
  449. w = (len(bd.blocks)+height-1) // height
  450. if hilbert:
  451. grid = {}
  452. for (x,y),b in zip(hilbert_curve(w, height), bd_.blocks):
  453. grid[(x,y)] = draw(b)
  454. line = [
  455. ''.join(grid.get((x,y), ' ') for x in range(w))
  456. for y in range(height)]
  457. elif lebesgue:
  458. grid = {}
  459. for (x,y),b in zip(lebesgue_curve(w, height), bd_.blocks):
  460. grid[(x,y)] = draw(b)
  461. line = [
  462. ''.join(grid.get((x,y), ' ') for x in range(w))
  463. for y in range(height)]
  464. else:
  465. line = [
  466. ''.join(draw(b) for b in bd_.blocks[y*w:y*w+w])
  467. for y in range(height)]
  468. else:
  469. line = [''.join(draw(b) for b in bd_.blocks)]
  470. if not lines:
  471. # just go ahead and print here
  472. for row in line:
  473. sys.stdout.write(row)
  474. sys.stdout.write('\n')
  475. sys.stdout.flush()
  476. else:
  477. history.append(line)
  478. del history[:-lines]
  479. last_rows = 1
  480. def print_line():
  481. nonlocal last_rows
  482. if not lines:
  483. return
  484. # give ourself a canvas
  485. while last_rows < len(history)*height:
  486. sys.stdout.write('\n')
  487. last_rows += 1
  488. for i, row in enumerate(it.chain.from_iterable(history)):
  489. jump = len(history)*height-1-i
  490. # move cursor, clear line, disable/reenable line wrapping
  491. sys.stdout.write('\r')
  492. if jump > 0:
  493. sys.stdout.write('\x1b[%dA' % jump)
  494. sys.stdout.write('\x1b[K')
  495. sys.stdout.write('\x1b[?7l')
  496. sys.stdout.write(row)
  497. sys.stdout.write('\x1b[?7h')
  498. if jump > 0:
  499. sys.stdout.write('\x1b[%dB' % jump)
  500. if sleep is None or (coalesce and not lines):
  501. # read/parse coalesce number of operations
  502. try:
  503. while True:
  504. with openio(path) as f:
  505. changes = 0
  506. for line in f:
  507. change = parse_line(line)
  508. changes += change
  509. if change and changes % (coalesce or 1) == 0:
  510. push_line()
  511. print_line()
  512. # sleep between coalesced lines?
  513. if sleep is not None:
  514. time.sleep(sleep)
  515. if not keep_open:
  516. break
  517. # don't just flood open calls
  518. time.sleep(sleep)
  519. except KeyboardInterrupt:
  520. pass
  521. else:
  522. # read/parse in a background thread
  523. def parse():
  524. nonlocal done
  525. while True:
  526. with openio(path) as f:
  527. changes = 0
  528. for line in f:
  529. change = parse_line(line)
  530. changes += change
  531. if change and changes % (coalesce or 1) == 0:
  532. if coalesce:
  533. push_line()
  534. event.set()
  535. if not keep_open:
  536. break
  537. # don't just flood open calls
  538. time.sleep(sleep)
  539. done = True
  540. th.Thread(target=parse, daemon=True).start()
  541. try:
  542. while not done:
  543. time.sleep(sleep)
  544. event.wait()
  545. event.clear()
  546. if not coalesce:
  547. push_line()
  548. print_line()
  549. except KeyboardInterrupt:
  550. pass
  551. if lines:
  552. sys.stdout.write('\n')
  553. if __name__ == "__main__":
  554. import sys
  555. import argparse
  556. parser = argparse.ArgumentParser(
  557. description="Display operations on block devices based on "
  558. "trace output.")
  559. parser.add_argument(
  560. 'path',
  561. nargs='?',
  562. help="Path to read from.")
  563. parser.add_argument(
  564. '-r',
  565. '--read',
  566. action='store_true',
  567. help="Render reads.")
  568. parser.add_argument(
  569. '-p',
  570. '--prog',
  571. action='store_true',
  572. help="Render progs.")
  573. parser.add_argument(
  574. '-e',
  575. '--erase',
  576. action='store_true',
  577. help="Render erases.")
  578. parser.add_argument(
  579. '-w',
  580. '--wear',
  581. action='store_true',
  582. help="Render wear.")
  583. parser.add_argument(
  584. '-R',
  585. '--reset',
  586. action='store_true',
  587. help="Reset wear on block device initialization.")
  588. parser.add_argument(
  589. '-A',
  590. '--ascii',
  591. action='store_true',
  592. help="Don't use unicode characters.")
  593. parser.add_argument(
  594. '--chars',
  595. help="Characters to use for noop, read, prog, erase operations.")
  596. parser.add_argument(
  597. '--wear-chars',
  598. help="Characters to use to show wear.")
  599. parser.add_argument(
  600. '--color',
  601. choices=['never', 'always', 'auto', 'ops', 'wear'],
  602. help="When to use terminal colors, defaults to auto.")
  603. parser.add_argument(
  604. '-b',
  605. '--block',
  606. type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
  607. help="Show a specific block or range of blocks.")
  608. parser.add_argument(
  609. '-i',
  610. '--off',
  611. type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
  612. help="Show a specific offset or range of offsets.")
  613. parser.add_argument(
  614. '-B',
  615. '--block-size',
  616. type=lambda x: int(x, 0),
  617. help="Assume a specific block size.")
  618. parser.add_argument(
  619. '--block-count',
  620. type=lambda x: int(x, 0),
  621. help="Assume a specific block count.")
  622. parser.add_argument(
  623. '-C',
  624. '--block-cycles',
  625. type=lambda x: int(x, 0),
  626. help="Assumed maximum number of erase cycles when measuring wear.")
  627. parser.add_argument(
  628. '-W',
  629. '--width',
  630. type=lambda x: int(x, 0),
  631. help="Width in columns. A width of 0 indicates no limit. Defaults "
  632. "to terminal width or 80.")
  633. parser.add_argument(
  634. '-H',
  635. '--height',
  636. type=lambda x: int(x, 0),
  637. help="Height in rows. Defaults to 1.")
  638. parser.add_argument(
  639. '-x',
  640. '--scale',
  641. type=float,
  642. help="Number of characters per block, ignores --width if set.")
  643. parser.add_argument(
  644. '-n',
  645. '--lines',
  646. type=lambda x: int(x, 0),
  647. help="Number of lines to show, with 0 indicating no limit. "
  648. "Defaults to 0.")
  649. parser.add_argument(
  650. '-c',
  651. '--coalesce',
  652. type=lambda x: int(x, 0),
  653. help="Number of operations to coalesce together. Defaults to 1.")
  654. parser.add_argument(
  655. '-s',
  656. '--sleep',
  657. type=float,
  658. help="Time in seconds to sleep between reads, while coalescing "
  659. "operations.")
  660. parser.add_argument(
  661. '-I',
  662. '--hilbert',
  663. action='store_true',
  664. help="Render as a space-filling Hilbert curve.")
  665. parser.add_argument(
  666. '-Z',
  667. '--lebesgue',
  668. action='store_true',
  669. help="Render as a space-filling Z-curve.")
  670. parser.add_argument(
  671. '-k',
  672. '--keep-open',
  673. action='store_true',
  674. help="Reopen the pipe on EOF, useful when multiple "
  675. "processes are writing.")
  676. sys.exit(main(**{k: v
  677. for k, v in vars(parser.parse_args()).items()
  678. if v is not None}))