summary.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  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. '_sort': fields,
  297. '_types': types_,
  298. })
  299. def fold(Result, results, *,
  300. by=None,
  301. defines=None,
  302. **_):
  303. if by is None:
  304. by = Result._by
  305. for k in it.chain(by or [], (k for k, _ in defines or [])):
  306. if k not in Result._by and k not in Result._fields:
  307. print("error: could not find field %r?" % k)
  308. sys.exit(-1)
  309. # filter by matching defines
  310. if defines is not None:
  311. results_ = []
  312. for r in results:
  313. if all(getattr(r, k) in vs for k, vs in defines):
  314. results_.append(r)
  315. results = results_
  316. # organize results into conflicts
  317. folding = co.OrderedDict()
  318. for r in results:
  319. name = tuple(getattr(r, k) for k in by)
  320. if name not in folding:
  321. folding[name] = []
  322. folding[name].append(r)
  323. # merge conflicts
  324. folded = []
  325. for name, rs in folding.items():
  326. folded.append(sum(rs[1:], start=rs[0]))
  327. return folded
  328. def table(Result, results, diff_results=None, *,
  329. by=None,
  330. fields=None,
  331. sort=None,
  332. summary=False,
  333. all=False,
  334. percent=False,
  335. **_):
  336. all_, all = all, __builtins__.all
  337. if by is None:
  338. by = Result._by
  339. if fields is None:
  340. fields = Result._fields
  341. types = Result._types
  342. # fold again
  343. results = fold(Result, results, by=by)
  344. if diff_results is not None:
  345. diff_results = fold(Result, diff_results, by=by)
  346. # organize by name
  347. table = {
  348. ','.join(str(getattr(r, k) or '') for k in by): r
  349. for r in results}
  350. diff_table = {
  351. ','.join(str(getattr(r, k) or '') for k in by): r
  352. for r in diff_results or []}
  353. names = list(table.keys() | diff_table.keys())
  354. # sort again, now with diff info, note that python's sort is stable
  355. names.sort()
  356. if diff_results is not None:
  357. names.sort(key=lambda n: tuple(
  358. types[k].ratio(
  359. getattr(table.get(n), k, None),
  360. getattr(diff_table.get(n), k, None))
  361. for k in fields),
  362. reverse=True)
  363. if sort:
  364. for k, reverse in reversed(sort):
  365. names.sort(
  366. key=lambda n: tuple(
  367. (getattr(table[n], k),)
  368. if getattr(table.get(n), k, None) is not None else ()
  369. for k in ([k] if k else [
  370. k for k in Result._sort if k in fields])),
  371. reverse=reverse ^ (not k or k in Result._fields))
  372. # build up our lines
  373. lines = []
  374. # header
  375. header = []
  376. header.append('%s%s' % (
  377. ','.join(by),
  378. ' (%d added, %d removed)' % (
  379. sum(1 for n in table if n not in diff_table),
  380. sum(1 for n in diff_table if n not in table))
  381. if diff_results is not None and not percent else '')
  382. if not summary else '')
  383. if diff_results is None:
  384. for k in fields:
  385. header.append(k)
  386. elif percent:
  387. for k in fields:
  388. header.append(k)
  389. else:
  390. for k in fields:
  391. header.append('o'+k)
  392. for k in fields:
  393. header.append('n'+k)
  394. for k in fields:
  395. header.append('d'+k)
  396. header.append('')
  397. lines.append(header)
  398. def table_entry(name, r, diff_r=None, ratios=[]):
  399. entry = []
  400. entry.append(name)
  401. if diff_results is None:
  402. for k in fields:
  403. entry.append(getattr(r, k).table()
  404. if getattr(r, k, None) is not None
  405. else types[k].none)
  406. elif percent:
  407. for k in fields:
  408. entry.append(getattr(r, k).diff_table()
  409. if getattr(r, k, None) is not None
  410. else types[k].diff_none)
  411. else:
  412. for k in fields:
  413. entry.append(getattr(diff_r, k).diff_table()
  414. if getattr(diff_r, k, None) is not None
  415. else types[k].diff_none)
  416. for k in fields:
  417. entry.append(getattr(r, k).diff_table()
  418. if getattr(r, k, None) is not None
  419. else types[k].diff_none)
  420. for k in fields:
  421. entry.append(types[k].diff_diff(
  422. getattr(r, k, None),
  423. getattr(diff_r, k, None)))
  424. if diff_results is None:
  425. entry.append('')
  426. elif percent:
  427. entry.append(' (%s)' % ', '.join(
  428. '+∞%' if t == +m.inf
  429. else '-∞%' if t == -m.inf
  430. else '%+.1f%%' % (100*t)
  431. for t in ratios))
  432. else:
  433. entry.append(' (%s)' % ', '.join(
  434. '+∞%' if t == +m.inf
  435. else '-∞%' if t == -m.inf
  436. else '%+.1f%%' % (100*t)
  437. for t in ratios
  438. if t)
  439. if any(ratios) else '')
  440. return entry
  441. # entries
  442. if not summary:
  443. for name in names:
  444. r = table.get(name)
  445. if diff_results is None:
  446. diff_r = None
  447. ratios = None
  448. else:
  449. diff_r = diff_table.get(name)
  450. ratios = [
  451. types[k].ratio(
  452. getattr(r, k, None),
  453. getattr(diff_r, k, None))
  454. for k in fields]
  455. if not all_ and not any(ratios):
  456. continue
  457. lines.append(table_entry(name, r, diff_r, ratios))
  458. # total
  459. r = next(iter(fold(Result, results, by=[])), None)
  460. if diff_results is None:
  461. diff_r = None
  462. ratios = None
  463. else:
  464. diff_r = next(iter(fold(Result, diff_results, by=[])), None)
  465. ratios = [
  466. types[k].ratio(
  467. getattr(r, k, None),
  468. getattr(diff_r, k, None))
  469. for k in fields]
  470. lines.append(table_entry('TOTAL', r, diff_r, ratios))
  471. # find the best widths, note that column 0 contains the names and column -1
  472. # the ratios, so those are handled a bit differently
  473. widths = [
  474. ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
  475. for w, i in zip(
  476. it.chain([23], it.repeat(7)),
  477. range(len(lines[0])-1))]
  478. # print our table
  479. for line in lines:
  480. print('%-*s %s%s' % (
  481. widths[0], line[0],
  482. ' '.join('%*s' % (w, x)
  483. for w, x in zip(widths[1:], line[1:-1])),
  484. line[-1]))
  485. def openio(path, mode='r', buffering=-1):
  486. # allow '-' for stdin/stdout
  487. if path == '-':
  488. if mode == 'r':
  489. return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
  490. else:
  491. return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
  492. else:
  493. return open(path, mode, buffering)
  494. def main(csv_paths, *,
  495. by=None,
  496. fields=None,
  497. defines=None,
  498. sort=None,
  499. **args):
  500. # separate out renames
  501. renames = list(it.chain.from_iterable(
  502. ((k, v) for v in vs)
  503. for k, vs in it.chain(by or [], fields or [])))
  504. if by is not None:
  505. by = [k for k, _ in by]
  506. if fields is not None:
  507. fields = [k for k, _ in fields]
  508. # figure out types
  509. types = {}
  510. for t in TYPES.keys():
  511. for k in args.get(t, []):
  512. if k in types:
  513. print("error: conflicting type for field %r?" % k)
  514. sys.exit(-1)
  515. types[k] = TYPES[t]
  516. # rename types?
  517. if renames:
  518. types_ = {}
  519. for new_k, old_k in renames:
  520. if old_k in types:
  521. types_[new_k] = types[old_k]
  522. types.update(types_)
  523. # figure out merge operations
  524. ops = {}
  525. for o in OPS.keys():
  526. for k in args.get(o, []):
  527. if k in ops:
  528. print("error: conflicting op for field %r?" % k)
  529. sys.exit(-1)
  530. ops[k] = OPS[o]
  531. # rename ops?
  532. if renames:
  533. ops_ = {}
  534. for new_k, old_k in renames:
  535. if old_k in ops:
  536. ops_[new_k] = ops[old_k]
  537. ops.update(ops_)
  538. # find CSV files
  539. results = []
  540. for path in csv_paths:
  541. try:
  542. with openio(path) as f:
  543. reader = csv.DictReader(f, restval='')
  544. for r in reader:
  545. # rename fields?
  546. if renames:
  547. # make a copy so renames can overlap
  548. r_ = {}
  549. for new_k, old_k in renames:
  550. if old_k in r:
  551. r_[new_k] = r[old_k]
  552. r.update(r_)
  553. results.append(r)
  554. except FileNotFoundError:
  555. pass
  556. # homogenize
  557. Result = infer(results,
  558. by=by,
  559. fields=fields,
  560. types=types,
  561. ops=ops,
  562. renames=renames)
  563. results_ = []
  564. for r in results:
  565. if not any(k in r and r[k].strip()
  566. for k in Result._fields):
  567. continue
  568. try:
  569. results_.append(Result(**{
  570. k: r[k] for k in Result._by + Result._fields
  571. if k in r and r[k].strip()}))
  572. except TypeError:
  573. pass
  574. results = results_
  575. # fold
  576. results = fold(Result, results, by=by, defines=defines)
  577. # sort, note that python's sort is stable
  578. results.sort()
  579. if sort:
  580. for k, reverse in reversed(sort):
  581. results.sort(
  582. key=lambda r: tuple(
  583. (getattr(r, k),) if getattr(r, k) is not None else ()
  584. for k in ([k] if k else Result._sort)),
  585. reverse=reverse ^ (not k or k in Result._fields))
  586. # write results to CSV
  587. if args.get('output'):
  588. with openio(args['output'], 'w') as f:
  589. writer = csv.DictWriter(f, Result._by + Result._fields)
  590. writer.writeheader()
  591. for r in results:
  592. # note we need to go through getattr to resolve lazy fields
  593. writer.writerow({
  594. k: getattr(r, k) for k in Result._by + Result._fields})
  595. # find previous results?
  596. if args.get('diff'):
  597. diff_results = []
  598. try:
  599. with openio(args['diff']) as f:
  600. reader = csv.DictReader(f, restval='')
  601. for r in reader:
  602. # rename fields?
  603. if renames:
  604. # make a copy so renames can overlap
  605. r_ = {}
  606. for new_k, old_k in renames:
  607. if old_k in r:
  608. r_[new_k] = r[old_k]
  609. r.update(r_)
  610. if not any(k in r and r[k].strip()
  611. for k in Result._fields):
  612. continue
  613. try:
  614. diff_results.append(Result(**{
  615. k: r[k] for k in Result._by + Result._fields
  616. if k in r and r[k].strip()}))
  617. except TypeError:
  618. pass
  619. except FileNotFoundError:
  620. pass
  621. # fold
  622. diff_results = fold(Result, diff_results, by=by, defines=defines)
  623. # print table
  624. if not args.get('quiet'):
  625. table(Result, results,
  626. diff_results if args.get('diff') else None,
  627. by=by,
  628. fields=fields,
  629. sort=sort,
  630. **args)
  631. if __name__ == "__main__":
  632. import argparse
  633. import sys
  634. parser = argparse.ArgumentParser(
  635. description="Summarize measurements in CSV files.",
  636. allow_abbrev=False)
  637. parser.add_argument(
  638. 'csv_paths',
  639. nargs='*',
  640. help="Input *.csv files.")
  641. parser.add_argument(
  642. '-q', '--quiet',
  643. action='store_true',
  644. help="Don't show anything, useful with -o.")
  645. parser.add_argument(
  646. '-o', '--output',
  647. help="Specify CSV file to store results.")
  648. parser.add_argument(
  649. '-d', '--diff',
  650. help="Specify CSV file to diff against.")
  651. parser.add_argument(
  652. '-a', '--all',
  653. action='store_true',
  654. help="Show all, not just the ones that changed.")
  655. parser.add_argument(
  656. '-p', '--percent',
  657. action='store_true',
  658. help="Only show percentage change, not a full diff.")
  659. parser.add_argument(
  660. '-b', '--by',
  661. action='append',
  662. type=lambda x: (
  663. lambda k,v=None: (k, v.split(',') if v is not None else ())
  664. )(*x.split('=', 1)),
  665. help="Group by this field. Can rename fields with new_name=old_name.")
  666. parser.add_argument(
  667. '-f', '--field',
  668. dest='fields',
  669. action='append',
  670. type=lambda x: (
  671. lambda k,v=None: (k, v.split(',') if v is not None else ())
  672. )(*x.split('=', 1)),
  673. help="Show this field. Can rename fields with new_name=old_name.")
  674. parser.add_argument(
  675. '-D', '--define',
  676. dest='defines',
  677. action='append',
  678. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  679. help="Only include results where this field is this value. May include "
  680. "comma-separated options.")
  681. class AppendSort(argparse.Action):
  682. def __call__(self, parser, namespace, value, option):
  683. if namespace.sort is None:
  684. namespace.sort = []
  685. namespace.sort.append((value, True if option == '-S' else False))
  686. parser.add_argument(
  687. '-s', '--sort',
  688. nargs='?',
  689. action=AppendSort,
  690. help="Sort by this field.")
  691. parser.add_argument(
  692. '-S', '--reverse-sort',
  693. nargs='?',
  694. action=AppendSort,
  695. help="Sort by this field, but backwards.")
  696. parser.add_argument(
  697. '-Y', '--summary',
  698. action='store_true',
  699. help="Only show the total.")
  700. parser.add_argument(
  701. '--int',
  702. action='append',
  703. help="Treat these fields as ints.")
  704. parser.add_argument(
  705. '--float',
  706. action='append',
  707. help="Treat these fields as floats.")
  708. parser.add_argument(
  709. '--frac',
  710. action='append',
  711. help="Treat these fields as fractions.")
  712. parser.add_argument(
  713. '--sum',
  714. action='append',
  715. help="Add these fields (the default).")
  716. parser.add_argument(
  717. '--prod',
  718. action='append',
  719. help="Multiply these fields.")
  720. parser.add_argument(
  721. '--min',
  722. action='append',
  723. help="Take the minimum of these fields.")
  724. parser.add_argument(
  725. '--max',
  726. action='append',
  727. help="Take the maximum of these fields.")
  728. parser.add_argument(
  729. '--mean',
  730. action='append',
  731. help="Average these fields.")
  732. parser.add_argument(
  733. '--stddev',
  734. action='append',
  735. help="Find the standard deviation of these fields.")
  736. parser.add_argument(
  737. '--gmean',
  738. action='append',
  739. help="Find the geometric mean of these fields.")
  740. parser.add_argument(
  741. '--gstddev',
  742. action='append',
  743. help="Find the geometric standard deviation of these fields.")
  744. sys.exit(main(**{k: v
  745. for k, v in vars(parser.parse_intermixed_args()).items()
  746. if v is not None}))