summary.py 23 KB

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