tracebd.py 23 KB

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