tracebd.py 23 KB

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