summary.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  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. if not any(k in r and r[k].strip()
  561. for k in Result._fields):
  562. continue
  563. try:
  564. results_.append(Result(**{
  565. k: r[k] for k in Result._by + Result._fields
  566. if k in r and r[k].strip()}))
  567. except TypeError:
  568. pass
  569. results = results_
  570. # fold
  571. results = fold(Result, results, by=by, defines=defines)
  572. # sort, note that python's sort is stable
  573. results.sort()
  574. if sort:
  575. for k, reverse in reversed(sort):
  576. results.sort(key=lambda r: (getattr(r, k),)
  577. if getattr(r, k) is not None else (),
  578. reverse=reverse ^ (not k or k in Result._fields))
  579. # write results to CSV
  580. if args.get('output'):
  581. with openio(args['output'], 'w') as f:
  582. writer = csv.DictWriter(f, Result._by + Result._fields)
  583. writer.writeheader()
  584. for r in results:
  585. # note we need to go through getattr to resolve lazy fields
  586. writer.writerow({
  587. k: getattr(r, k) for k in Result._by + Result._fields})
  588. # find previous results?
  589. if args.get('diff'):
  590. diff_results = []
  591. try:
  592. with openio(args['diff']) as f:
  593. reader = csv.DictReader(f, restval='')
  594. for r in reader:
  595. # rename fields?
  596. if renames:
  597. # make a copy so renames can overlap
  598. r_ = {}
  599. for new_k, old_k in renames:
  600. if old_k in r:
  601. r_[new_k] = r[old_k]
  602. r.update(r_)
  603. if not any(k in r and r[k].strip()
  604. for k in Result._fields):
  605. continue
  606. try:
  607. diff_results.append(Result(**{
  608. k: r[k] for k in Result._by + Result._fields
  609. if k in r and r[k].strip()}))
  610. except TypeError:
  611. pass
  612. except FileNotFoundError:
  613. pass
  614. # fold
  615. diff_results = fold(Result, diff_results, by=by, defines=defines)
  616. # print table
  617. if not args.get('quiet'):
  618. table(Result, results,
  619. diff_results if args.get('diff') else None,
  620. by=by,
  621. fields=fields,
  622. sort=sort,
  623. **args)
  624. if __name__ == "__main__":
  625. import argparse
  626. import sys
  627. parser = argparse.ArgumentParser(
  628. description="Summarize measurements in CSV files.",
  629. allow_abbrev=False)
  630. parser.add_argument(
  631. 'csv_paths',
  632. nargs='*',
  633. help="Input *.csv files.")
  634. parser.add_argument(
  635. '-q', '--quiet',
  636. action='store_true',
  637. help="Don't show anything, useful with -o.")
  638. parser.add_argument(
  639. '-o', '--output',
  640. help="Specify CSV file to store results.")
  641. parser.add_argument(
  642. '-d', '--diff',
  643. help="Specify CSV file to diff against.")
  644. parser.add_argument(
  645. '-a', '--all',
  646. action='store_true',
  647. help="Show all, not just the ones that changed.")
  648. parser.add_argument(
  649. '-p', '--percent',
  650. action='store_true',
  651. help="Only show percentage change, not a full diff.")
  652. parser.add_argument(
  653. '-b', '--by',
  654. action='append',
  655. type=lambda x: (
  656. lambda k,v=None: (k, v.split(',') if v is not None else ())
  657. )(*x.split('=', 1)),
  658. help="Group by this field. Can rename fields with new_name=old_name.")
  659. parser.add_argument(
  660. '-f', '--field',
  661. dest='fields',
  662. action='append',
  663. type=lambda x: (
  664. lambda k,v=None: (k, v.split(',') if v is not None else ())
  665. )(*x.split('=', 1)),
  666. help="Show this field. Can rename fields with new_name=old_name.")
  667. parser.add_argument(
  668. '-D', '--define',
  669. dest='defines',
  670. action='append',
  671. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  672. help="Only include results where this field is this value. May include "
  673. "comma-separated options.")
  674. class AppendSort(argparse.Action):
  675. def __call__(self, parser, namespace, value, option):
  676. if namespace.sort is None:
  677. namespace.sort = []
  678. namespace.sort.append((value, True if option == '-S' else False))
  679. parser.add_argument(
  680. '-s', '--sort',
  681. action=AppendSort,
  682. help="Sort by this fields.")
  683. parser.add_argument(
  684. '-S', '--reverse-sort',
  685. action=AppendSort,
  686. help="Sort by this fields, but backwards.")
  687. parser.add_argument(
  688. '-Y', '--summary',
  689. action='store_true',
  690. help="Only show the total.")
  691. parser.add_argument(
  692. '--int',
  693. action='append',
  694. help="Treat these fields as ints.")
  695. parser.add_argument(
  696. '--float',
  697. action='append',
  698. help="Treat these fields as floats.")
  699. parser.add_argument(
  700. '--frac',
  701. action='append',
  702. help="Treat these fields as fractions.")
  703. parser.add_argument(
  704. '--sum',
  705. action='append',
  706. help="Add these fields (the default).")
  707. parser.add_argument(
  708. '--prod',
  709. action='append',
  710. help="Multiply these fields.")
  711. parser.add_argument(
  712. '--min',
  713. action='append',
  714. help="Take the minimum of these fields.")
  715. parser.add_argument(
  716. '--max',
  717. action='append',
  718. help="Take the maximum of these fields.")
  719. parser.add_argument(
  720. '--mean',
  721. action='append',
  722. help="Average these fields.")
  723. parser.add_argument(
  724. '--stddev',
  725. action='append',
  726. help="Find the standard deviation of these fields.")
  727. parser.add_argument(
  728. '--gmean',
  729. action='append',
  730. help="Find the geometric mean of these fields.")
  731. parser.add_argument(
  732. '--gstddev',
  733. action='append',
  734. help="Find the geometric standard deviation of these fields.")
  735. sys.exit(main(**{k: v
  736. for k, v in vars(parser.parse_intermixed_args()).items()
  737. if v is not None}))