summary.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. #!/usr/bin/env python3
  2. #
  3. # Script to summarize the outputs of other scripts. Operates on CSV files.
  4. #
  5. # Example:
  6. # ./scripts/code.py lfs.o lfs_util.o -q -o lfs.code.csv
  7. # ./scripts/data.py lfs.o lfs_util.o -q -o lfs.data.csv
  8. # ./scripts/summary.py lfs.code.csv lfs.data.csv -q -o lfs.csv
  9. # ./scripts/summary.py -Y lfs.csv -f code=code_size,data=data_size
  10. #
  11. # Copyright (c) 2022, The littlefs authors.
  12. # SPDX-License-Identifier: BSD-3-Clause
  13. #
  14. import collections as co
  15. import csv
  16. import functools as ft
  17. import itertools as it
  18. import math as m
  19. import os
  20. import re
  21. # supported merge operations
  22. #
  23. # this is a terrible way to express these
  24. #
  25. OPS = {
  26. 'sum': lambda xs: sum(xs[1:], start=xs[0]),
  27. 'prod': lambda xs: m.prod(xs[1:], start=xs[0]),
  28. 'min': min,
  29. 'max': max,
  30. 'mean': lambda xs: Float(sum(float(x) for x in xs) / len(xs)),
  31. 'stddev': lambda xs: (
  32. lambda mean: Float(
  33. m.sqrt(sum((float(x) - mean)**2 for x in xs) / len(xs)))
  34. )(sum(float(x) for x in xs) / len(xs)),
  35. 'gmean': lambda xs: Float(m.prod(float(x) for x in xs)**(1/len(xs))),
  36. 'gstddev': lambda xs: (
  37. lambda gmean: Float(
  38. m.exp(m.sqrt(sum(m.log(float(x)/gmean)**2 for x in xs) / len(xs)))
  39. if gmean else m.inf)
  40. )(m.prod(float(x) for x in xs)**(1/len(xs))),
  41. }
  42. # integer fields
  43. class Int(co.namedtuple('Int', 'x')):
  44. __slots__ = ()
  45. def __new__(cls, x=0):
  46. if isinstance(x, Int):
  47. return x
  48. if isinstance(x, str):
  49. try:
  50. x = int(x, 0)
  51. except ValueError:
  52. # also accept +-∞ and +-inf
  53. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  54. x = m.inf
  55. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  56. x = -m.inf
  57. else:
  58. raise
  59. assert isinstance(x, int) or m.isinf(x), x
  60. return super().__new__(cls, x)
  61. def __str__(self):
  62. if self.x == m.inf:
  63. return '∞'
  64. elif self.x == -m.inf:
  65. return '-∞'
  66. else:
  67. return str(self.x)
  68. def __int__(self):
  69. assert not m.isinf(self.x)
  70. return self.x
  71. def __float__(self):
  72. return float(self.x)
  73. none = '%7s' % '-'
  74. def table(self):
  75. return '%7s' % (self,)
  76. diff_none = '%7s' % '-'
  77. diff_table = table
  78. def diff_diff(self, other):
  79. new = self.x if self else 0
  80. old = other.x if other else 0
  81. diff = new - old
  82. if diff == +m.inf:
  83. return '%7s' % '+∞'
  84. elif diff == -m.inf:
  85. return '%7s' % '-∞'
  86. else:
  87. return '%+7d' % diff
  88. def ratio(self, other):
  89. new = self.x if self else 0
  90. old = other.x if other else 0
  91. if m.isinf(new) and m.isinf(old):
  92. return 0.0
  93. elif m.isinf(new):
  94. return +m.inf
  95. elif m.isinf(old):
  96. return -m.inf
  97. elif not old and not new:
  98. return 0.0
  99. elif not old:
  100. return 1.0
  101. else:
  102. return (new-old) / old
  103. def __add__(self, other):
  104. return self.__class__(self.x + other.x)
  105. def __sub__(self, other):
  106. return self.__class__(self.x - other.x)
  107. def __mul__(self, other):
  108. return self.__class__(self.x * other.x)
  109. # float fields
  110. class Float(co.namedtuple('Float', 'x')):
  111. __slots__ = ()
  112. def __new__(cls, x=0.0):
  113. if isinstance(x, Float):
  114. return x
  115. if isinstance(x, str):
  116. try:
  117. x = float(x)
  118. except ValueError:
  119. # also accept +-∞ and +-inf
  120. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  121. x = m.inf
  122. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  123. x = -m.inf
  124. else:
  125. raise
  126. assert isinstance(x, float), x
  127. return super().__new__(cls, x)
  128. def __str__(self):
  129. if self.x == m.inf:
  130. return '∞'
  131. elif self.x == -m.inf:
  132. return '-∞'
  133. else:
  134. return '%.1f' % self.x
  135. def __float__(self):
  136. return float(self.x)
  137. none = Int.none
  138. table = Int.table
  139. diff_none = Int.diff_none
  140. diff_table = Int.diff_table
  141. diff_diff = Int.diff_diff
  142. ratio = Int.ratio
  143. __add__ = Int.__add__
  144. __sub__ = Int.__sub__
  145. __mul__ = Int.__mul__
  146. # fractional fields, a/b
  147. class Frac(co.namedtuple('Frac', 'a,b')):
  148. __slots__ = ()
  149. def __new__(cls, a=0, b=None):
  150. if isinstance(a, Frac) and b is None:
  151. return a
  152. if isinstance(a, str) and b is None:
  153. a, b = a.split('/', 1)
  154. if b is None:
  155. b = a
  156. return super().__new__(cls, Int(a), Int(b))
  157. def __str__(self):
  158. return '%s/%s' % (self.a, self.b)
  159. def __float__(self):
  160. return float(self.a)
  161. none = '%11s %7s' % ('-', '-')
  162. def table(self):
  163. t = self.a.x/self.b.x if self.b.x else 1.0
  164. return '%11s %7s' % (
  165. self,
  166. '∞%' if t == +m.inf
  167. else '-∞%' if t == -m.inf
  168. else '%.1f%%' % (100*t))
  169. diff_none = '%11s' % '-'
  170. def diff_table(self):
  171. return '%11s' % (self,)
  172. def diff_diff(self, other):
  173. new_a, new_b = self if self else (Int(0), Int(0))
  174. old_a, old_b = other if other else (Int(0), Int(0))
  175. return '%11s' % ('%s/%s' % (
  176. new_a.diff_diff(old_a).strip(),
  177. new_b.diff_diff(old_b).strip()))
  178. def ratio(self, other):
  179. new_a, new_b = self if self else (Int(0), Int(0))
  180. old_a, old_b = other if other else (Int(0), Int(0))
  181. new = new_a.x/new_b.x if new_b.x else 1.0
  182. old = old_a.x/old_b.x if old_b.x else 1.0
  183. return new - old
  184. def __add__(self, other):
  185. return self.__class__(self.a + other.a, self.b + other.b)
  186. def __sub__(self, other):
  187. return self.__class__(self.a - other.a, self.b - other.b)
  188. def __mul__(self, other):
  189. return self.__class__(self.a * other.a, self.b + other.b)
  190. def __lt__(self, other):
  191. self_t = self.a.x/self.b.x if self.b.x else 1.0
  192. other_t = other.a.x/other.b.x if other.b.x else 1.0
  193. return (self_t, self.a.x) < (other_t, other.a.x)
  194. def __gt__(self, other):
  195. return self.__class__.__lt__(other, self)
  196. def __le__(self, other):
  197. return not self.__gt__(other)
  198. def __ge__(self, other):
  199. return not self.__lt__(other)
  200. # available types
  201. TYPES = co.OrderedDict([
  202. ('int', Int),
  203. ('float', Float),
  204. ('frac', Frac)
  205. ])
  206. def infer(results, *,
  207. by=None,
  208. fields=None,
  209. types={},
  210. ops={},
  211. renames=[],
  212. **_):
  213. # if fields not specified, try to guess from data
  214. if fields is None:
  215. fields = co.OrderedDict()
  216. for r in results:
  217. for k, v in r.items():
  218. if (by is None or k not in by) and v.strip():
  219. types_ = []
  220. for t in fields.get(k, TYPES.values()):
  221. try:
  222. t(v)
  223. types_.append(t)
  224. except ValueError:
  225. pass
  226. fields[k] = types_
  227. fields = list(k for k, v in fields.items() if v)
  228. # deduplicate fields
  229. fields = list(co.OrderedDict.fromkeys(fields).keys())
  230. # if by not specified, guess it's anything not in fields and not a
  231. # source of a rename
  232. if by is None:
  233. by = co.OrderedDict()
  234. for r in results:
  235. # also ignore None keys, these are introduced by csv.DictReader
  236. # when header + row mismatch
  237. by.update((k, True) for k in r.keys()
  238. if k is not None
  239. and k not in fields
  240. and not any(k == old_k for _, old_k in renames))
  241. by = list(by.keys())
  242. # deduplicate fields
  243. by = list(co.OrderedDict.fromkeys(by).keys())
  244. # find best type for all fields
  245. types_ = {}
  246. for k in fields:
  247. if k in types:
  248. types_[k] = types[k]
  249. else:
  250. for t in TYPES.values():
  251. for r in results:
  252. if k in r and r[k].strip():
  253. try:
  254. t(r[k])
  255. except ValueError:
  256. break
  257. else:
  258. types_[k] = t
  259. break
  260. else:
  261. print("error: no type matches field %r?" % k)
  262. sys.exit(-1)
  263. types = types_
  264. # does folding change the type?
  265. types_ = {}
  266. for k, t in types.items():
  267. types_[k] = ops.get(k, OPS['sum'])([t()]).__class__
  268. # create result class
  269. def __new__(cls, **r):
  270. return cls.__mro__[1].__new__(cls,
  271. **{k: r.get(k) for k in by},
  272. **{k: r[k] if k in r and isinstance(r[k], list)
  273. else [types[k](r[k])] if k in r
  274. else []
  275. for k in fields})
  276. def __add__(self, other):
  277. return self.__class__(
  278. **{k: getattr(self, k) for k in by},
  279. **{k: object.__getattribute__(self, k)
  280. + object.__getattribute__(other, k)
  281. for k in fields})
  282. def __getattribute__(self, k):
  283. if k in fields:
  284. if object.__getattribute__(self, k):
  285. return ops.get(k, OPS['sum'])(object.__getattribute__(self, k))
  286. else:
  287. return None
  288. return object.__getattribute__(self, k)
  289. return type('Result', (co.namedtuple('Result', by + fields),), {
  290. '__slots__': (),
  291. '__new__': __new__,
  292. '__add__': __add__,
  293. '__getattribute__': __getattribute__,
  294. '_by': by,
  295. '_fields': fields,
  296. '_types': types_,
  297. })
  298. def fold(Result, results, *,
  299. by=None,
  300. defines=None,
  301. **_):
  302. if by is None:
  303. by = Result._by
  304. for k in it.chain(by or [], (k for k, _ in defines or [])):
  305. if k not in Result._by and k not in Result._fields:
  306. print("error: could not find field %r?" % k)
  307. sys.exit(-1)
  308. # filter by matching defines
  309. if defines is not None:
  310. results_ = []
  311. for r in results:
  312. if all(getattr(r, k) in vs for k, vs in defines):
  313. results_.append(r)
  314. results = results_
  315. # organize results into conflicts
  316. folding = co.OrderedDict()
  317. for r in results:
  318. name = tuple(getattr(r, k) for k in by)
  319. if name not in folding:
  320. folding[name] = []
  321. folding[name].append(r)
  322. # merge conflicts
  323. folded = []
  324. for name, rs in folding.items():
  325. folded.append(sum(rs[1:], start=rs[0]))
  326. return folded
  327. def table(Result, results, diff_results=None, *,
  328. by=None,
  329. fields=None,
  330. sort=None,
  331. summary=False,
  332. all=False,
  333. percent=False,
  334. **_):
  335. all_, all = all, __builtins__.all
  336. if by is None:
  337. by = Result._by
  338. if fields is None:
  339. fields = Result._fields
  340. types = Result._types
  341. # fold again
  342. results = fold(Result, results, by=by)
  343. if diff_results is not None:
  344. diff_results = fold(Result, diff_results, by=by)
  345. # organize by name
  346. table = {
  347. ','.join(str(getattr(r, k) or '') for k in by): r
  348. for r in results}
  349. diff_table = {
  350. ','.join(str(getattr(r, k) or '') for k in by): r
  351. for r in diff_results or []}
  352. names = list(table.keys() | diff_table.keys())
  353. # sort again, now with diff info, note that python's sort is stable
  354. names.sort()
  355. if diff_results is not None:
  356. names.sort(key=lambda n: tuple(
  357. types[k].ratio(
  358. getattr(table.get(n), k, None),
  359. getattr(diff_table.get(n), k, None))
  360. for k in fields),
  361. reverse=True)
  362. if sort:
  363. for k, reverse in reversed(sort):
  364. names.sort(key=lambda n: (getattr(table[n], k),)
  365. if getattr(table.get(n), k, None) is not None else (),
  366. reverse=reverse ^ (not k or k in Result._fields))
  367. # build up our lines
  368. lines = []
  369. # header
  370. header = []
  371. header.append('%s%s' % (
  372. ','.join(by),
  373. ' (%d added, %d removed)' % (
  374. sum(1 for n in table if n not in diff_table),
  375. sum(1 for n in diff_table if n not in table))
  376. if diff_results is not None and not percent else '')
  377. if not summary else '')
  378. if diff_results is None:
  379. for k in fields:
  380. header.append(k)
  381. elif percent:
  382. for k in fields:
  383. header.append(k)
  384. else:
  385. for k in fields:
  386. header.append('o'+k)
  387. for k in fields:
  388. header.append('n'+k)
  389. for k in fields:
  390. header.append('d'+k)
  391. header.append('')
  392. lines.append(header)
  393. def table_entry(name, r, diff_r=None, ratios=[]):
  394. entry = []
  395. entry.append(name)
  396. if diff_results is None:
  397. for k in fields:
  398. entry.append(getattr(r, k).table()
  399. if getattr(r, k, None) is not None
  400. else types[k].none)
  401. elif percent:
  402. for k in fields:
  403. entry.append(getattr(r, k).diff_table()
  404. if getattr(r, k, None) is not None
  405. else types[k].diff_none)
  406. else:
  407. for k in fields:
  408. entry.append(getattr(diff_r, k).diff_table()
  409. if getattr(diff_r, k, None) is not None
  410. else types[k].diff_none)
  411. for k in fields:
  412. entry.append(getattr(r, k).diff_table()
  413. if getattr(r, k, None) is not None
  414. else types[k].diff_none)
  415. for k in fields:
  416. entry.append(types[k].diff_diff(
  417. getattr(r, k, None),
  418. getattr(diff_r, k, None)))
  419. if diff_results is None:
  420. entry.append('')
  421. elif percent:
  422. entry.append(' (%s)' % ', '.join(
  423. '+∞%' if t == +m.inf
  424. else '-∞%' if t == -m.inf
  425. else '%+.1f%%' % (100*t)
  426. for t in ratios))
  427. else:
  428. entry.append(' (%s)' % ', '.join(
  429. '+∞%' if t == +m.inf
  430. else '-∞%' if t == -m.inf
  431. else '%+.1f%%' % (100*t)
  432. for t in ratios
  433. if t)
  434. if any(ratios) else '')
  435. return entry
  436. # entries
  437. if not summary:
  438. for name in names:
  439. r = table.get(name)
  440. if diff_results is None:
  441. diff_r = None
  442. ratios = None
  443. else:
  444. diff_r = diff_table.get(name)
  445. ratios = [
  446. types[k].ratio(
  447. getattr(r, k, None),
  448. getattr(diff_r, k, None))
  449. for k in fields]
  450. if not all_ and not any(ratios):
  451. continue
  452. lines.append(table_entry(name, r, diff_r, ratios))
  453. # total
  454. r = next(iter(fold(Result, results, by=[])), None)
  455. if diff_results is None:
  456. diff_r = None
  457. ratios = None
  458. else:
  459. diff_r = next(iter(fold(Result, diff_results, by=[])), None)
  460. ratios = [
  461. types[k].ratio(
  462. getattr(r, k, None),
  463. getattr(diff_r, k, None))
  464. for k in fields]
  465. lines.append(table_entry('TOTAL', r, diff_r, ratios))
  466. # find the best widths, note that column 0 contains the names and column -1
  467. # the ratios, so those are handled a bit differently
  468. widths = [
  469. ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
  470. for w, i in zip(
  471. it.chain([23], it.repeat(7)),
  472. range(len(lines[0])-1))]
  473. # print our table
  474. for line in lines:
  475. print('%-*s %s%s' % (
  476. widths[0], line[0],
  477. ' '.join('%*s' % (w, x)
  478. for w, x in zip(widths[1:], line[1:-1])),
  479. line[-1]))
  480. def openio(path, mode='r', buffering=-1):
  481. # allow '-' for stdin/stdout
  482. if path == '-':
  483. if mode == 'r':
  484. return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
  485. else:
  486. return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
  487. else:
  488. return open(path, mode, buffering)
  489. def main(csv_paths, *,
  490. by=None,
  491. fields=None,
  492. defines=None,
  493. sort=None,
  494. **args):
  495. # separate out renames
  496. renames = list(it.chain.from_iterable(
  497. ((k, v) for v in vs)
  498. for k, vs in it.chain(by or [], fields or [])))
  499. if by is not None:
  500. by = [k for k, _ in by]
  501. if fields is not None:
  502. fields = [k for k, _ in fields]
  503. # figure out types
  504. types = {}
  505. for t in TYPES.keys():
  506. for k in args.get(t, []):
  507. if k in types:
  508. print("error: conflicting type for field %r?" % k)
  509. sys.exit(-1)
  510. types[k] = TYPES[t]
  511. # rename types?
  512. if renames:
  513. types_ = {}
  514. for new_k, old_k in renames:
  515. if old_k in types:
  516. types_[new_k] = types[old_k]
  517. types.update(types_)
  518. # figure out merge operations
  519. ops = {}
  520. for o in OPS.keys():
  521. for k in args.get(o, []):
  522. if k in ops:
  523. print("error: conflicting op for field %r?" % k)
  524. sys.exit(-1)
  525. ops[k] = OPS[o]
  526. # rename ops?
  527. if renames:
  528. ops_ = {}
  529. for new_k, old_k in renames:
  530. if old_k in ops:
  531. ops_[new_k] = ops[old_k]
  532. ops.update(ops_)
  533. # find CSV files
  534. results = []
  535. for path in csv_paths:
  536. try:
  537. with openio(path) as f:
  538. reader = csv.DictReader(f, restval='')
  539. for r in reader:
  540. # rename fields?
  541. if renames:
  542. # make a copy so renames can overlap
  543. r_ = {}
  544. for new_k, old_k in renames:
  545. if old_k in r:
  546. r_[new_k] = r[old_k]
  547. r.update(r_)
  548. results.append(r)
  549. except FileNotFoundError:
  550. pass
  551. # homogenize
  552. Result = infer(results,
  553. by=by,
  554. fields=fields,
  555. types=types,
  556. ops=ops,
  557. renames=renames)
  558. results_ = []
  559. for r in results:
  560. try:
  561. results_.append(Result(**{
  562. k: r[k] for k in Result._by + Result._fields
  563. if k in r and r[k].strip()}))
  564. except TypeError:
  565. pass
  566. results = results_
  567. # fold
  568. results = fold(Result, results, by=by, defines=defines)
  569. # sort, note that python's sort is stable
  570. results.sort()
  571. if sort:
  572. for k, reverse in reversed(sort):
  573. results.sort(key=lambda r: (getattr(r, k),)
  574. if getattr(r, k) is not None else (),
  575. reverse=reverse ^ (not k or k in Result._fields))
  576. # write results to CSV
  577. if args.get('output'):
  578. with openio(args['output'], 'w') as f:
  579. writer = csv.DictWriter(f, Result._by + Result._fields)
  580. writer.writeheader()
  581. for r in results:
  582. # note we need to go through getattr to resolve lazy fields
  583. writer.writerow({
  584. k: getattr(r, k) for k in Result._by + Result._fields})
  585. # find previous results?
  586. if args.get('diff'):
  587. diff_results = []
  588. try:
  589. with openio(args['diff']) as f:
  590. reader = csv.DictReader(f, restval='')
  591. for r in reader:
  592. # rename fields?
  593. if renames:
  594. # make a copy so renames can overlap
  595. r_ = {}
  596. for new_k, old_k in renames:
  597. if old_k in r:
  598. r_[new_k] = r[old_k]
  599. r.update(r_)
  600. try:
  601. diff_results.append(Result(**{
  602. k: r[k] for k in Result._by + Result._fields
  603. if k in r and r[k].strip()}))
  604. except TypeError:
  605. pass
  606. except FileNotFoundError:
  607. pass
  608. # fold
  609. diff_results = fold(Result, diff_results, by=by, defines=defines)
  610. # print table
  611. if not args.get('quiet'):
  612. table(Result, results,
  613. diff_results if args.get('diff') else None,
  614. by=by,
  615. fields=fields,
  616. sort=sort,
  617. **args)
  618. if __name__ == "__main__":
  619. import argparse
  620. import sys
  621. parser = argparse.ArgumentParser(
  622. description="Summarize measurements in CSV files.",
  623. allow_abbrev=False)
  624. parser.add_argument(
  625. 'csv_paths',
  626. nargs='*',
  627. help="Input *.csv files.")
  628. parser.add_argument(
  629. '-q', '--quiet',
  630. action='store_true',
  631. help="Don't show anything, useful with -o.")
  632. parser.add_argument(
  633. '-o', '--output',
  634. help="Specify CSV file to store results.")
  635. parser.add_argument(
  636. '-d', '--diff',
  637. help="Specify CSV file to diff against.")
  638. parser.add_argument(
  639. '-a', '--all',
  640. action='store_true',
  641. help="Show all, not just the ones that changed.")
  642. parser.add_argument(
  643. '-p', '--percent',
  644. action='store_true',
  645. help="Only show percentage change, not a full diff.")
  646. parser.add_argument(
  647. '-b', '--by',
  648. action='append',
  649. type=lambda x: (
  650. lambda k,v=None: (k, v.split(',') if v is not None else ())
  651. )(*x.split('=', 1)),
  652. help="Group by this field. Can rename fields with new_name=old_name.")
  653. parser.add_argument(
  654. '-f', '--field',
  655. dest='fields',
  656. action='append',
  657. type=lambda x: (
  658. lambda k,v=None: (k, v.split(',') if v is not None else ())
  659. )(*x.split('=', 1)),
  660. help="Show this field. Can rename fields with new_name=old_name.")
  661. parser.add_argument(
  662. '-D', '--define',
  663. dest='defines',
  664. action='append',
  665. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  666. help="Only include results where this field is this value. May include "
  667. "comma-separated options.")
  668. class AppendSort(argparse.Action):
  669. def __call__(self, parser, namespace, value, option):
  670. if namespace.sort is None:
  671. namespace.sort = []
  672. namespace.sort.append((value, True if option == '-S' else False))
  673. parser.add_argument(
  674. '-s', '--sort',
  675. action=AppendSort,
  676. help="Sort by this fields.")
  677. parser.add_argument(
  678. '-S', '--reverse-sort',
  679. action=AppendSort,
  680. help="Sort by this fields, but backwards.")
  681. parser.add_argument(
  682. '-Y', '--summary',
  683. action='store_true',
  684. help="Only show the total.")
  685. parser.add_argument(
  686. '--int',
  687. action='append',
  688. help="Treat these fields as ints.")
  689. parser.add_argument(
  690. '--float',
  691. action='append',
  692. help="Treat these fields as floats.")
  693. parser.add_argument(
  694. '--frac',
  695. action='append',
  696. help="Treat these fields as fractions.")
  697. parser.add_argument(
  698. '--sum',
  699. action='append',
  700. help="Add these fields (the default).")
  701. parser.add_argument(
  702. '--prod',
  703. action='append',
  704. help="Multiply these fields.")
  705. parser.add_argument(
  706. '--min',
  707. action='append',
  708. help="Take the minimum of these fields.")
  709. parser.add_argument(
  710. '--max',
  711. action='append',
  712. help="Take the maximum of these fields.")
  713. parser.add_argument(
  714. '--mean',
  715. action='append',
  716. help="Average these fields.")
  717. parser.add_argument(
  718. '--stddev',
  719. action='append',
  720. help="Find the standard deviation of these fields.")
  721. parser.add_argument(
  722. '--gmean',
  723. action='append',
  724. help="Find the geometric mean of these fields.")
  725. parser.add_argument(
  726. '--gstddev',
  727. action='append',
  728. help="Find the geometric standard deviation of these fields.")
  729. sys.exit(main(**{k: v
  730. for k, v in vars(parser.parse_intermixed_args()).items()
  731. if v is not None}))