1 2def iter_clean_lines(lines): 3 lines = iter(lines) 4 for line in lines: 5 line = line.strip() 6 if line.startswith('# XXX'): 7 continue 8 yield line 9 10 11def parse_table_lines(lines): 12 lines = iter_clean_lines(lines) 13 14 for line in lines: 15 if line.startswith(('####', '#----')): 16 kind = 0 if line[1] == '#' else 1 17 try: 18 line = next(lines).strip() 19 except StopIteration: 20 line = '' 21 if not line.startswith('# '): 22 raise NotImplementedError(line) 23 yield kind, line[2:].lstrip() 24 continue 25 26 maybe = None 27 while line.startswith('#'): 28 if line != '#' and line[1] == ' ': 29 maybe = line[2:].lstrip() 30 try: 31 line = next(lines).strip() 32 except StopIteration: 33 return 34 if not line: 35 break 36 else: 37 if line: 38 if maybe: 39 yield 2, maybe 40 yield 'row', line 41 42 43def iter_sections(lines): 44 header = None 45 section = [] 46 for kind, value in parse_table_lines(lines): 47 if kind == 'row': 48 if not section: 49 if header is None: 50 header = value 51 continue 52 raise NotImplementedError(value) 53 yield tuple(section), value 54 else: 55 if header is None: 56 header = False 57 section[kind:] = [value] 58 59 60def collect_sections(lines): 61 sections = {} 62 for section, row in iter_sections(lines): 63 if section not in sections: 64 sections[section] = [row] 65 else: 66 sections[section].append(row) 67 return sections 68 69 70def collate_sections(lines): 71 collated = {} 72 for section, rows in collect_sections(lines).items(): 73 parent = collated 74 current = () 75 for name in section: 76 current += (name,) 77 try: 78 child, secrows, totalrows = parent[name] 79 except KeyError: 80 child = {} 81 secrows = [] 82 totalrows = [] 83 parent[name] = (child, secrows, totalrows) 84 parent = child 85 if current == section: 86 secrows.extend(rows) 87 totalrows.extend(rows) 88 return collated 89 90 91############################# 92# the commands 93 94def cmd_count_by_section(lines): 95 div = ' ' + '-' * 50 96 total = 0 97 def render_tree(root, depth=0): 98 nonlocal total 99 indent = ' ' * depth 100 for name, data in root.items(): 101 subroot, rows, totalrows = data 102 sectotal = f'({len(totalrows)})' if totalrows != rows else '' 103 count = len(rows) if rows else '' 104 if depth == 0: 105 yield div 106 yield f'{sectotal:>7} {count:>4} {indent}{name}' 107 yield from render_tree(subroot, depth+1) 108 total += len(rows) 109 sections = collate_sections(lines) 110 yield from render_tree(sections) 111 yield div 112 yield f'(total: {total})' 113 114 115############################# 116# the script 117 118def parse_args(argv=None, prog=None): 119 import argparse 120 parser = argparse.ArgumentParser(prog=prog) 121 parser.add_argument('filename') 122 123 args = parser.parse_args(argv) 124 ns = vars(args) 125 126 return ns 127 128 129def main(filename): 130 with open(filename) as infile: 131 for line in cmd_count_by_section(infile): 132 print(line) 133 134 135if __name__ == '__main__': 136 kwargs = parse_args() 137 main(**kwargs) 138