-
Notifications
You must be signed in to change notification settings - Fork 0
/
extract-abbrevs.py
executable file
·178 lines (145 loc) · 4.62 KB
/
extract-abbrevs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
# Extract abbreviations from help text
from dataclasses import dataclass, field
from subprocess import check_output, STDOUT, CalledProcessError
from sys import argv, stdin
@dataclass
class Subcommand:
cmd: str
def abbrev(self):
return self.cmd[0]
def deconflict(self, abbrev):
if self.abbrev() == abbrev:
return self.cmd[0] + self.cmd[-1]
return self.abbrev()
@dataclass
class Option:
short: str
long: str
def abbrev(self):
if self.short is None:
return self.long[0]
return self.short
def deconflict(self, abbrev):
if self.short is not None:
return self.short
if self.abbrev() == abbrev:
return self.long[0] + self.long[-1]
return self.abbrev()
def extract(prog, s):
options = []
subcommands = []
for line in s.splitlines():
# Subcommands
if not line.startswith(" "):
continue
# Often, they start with >2 spaces, then the name of the subcommand
line = line.lstrip()
words = line.split()
if len(words) <= 1:
continue
if words[0][0] != "-":
# The description comes immediately after, or, in the case of
# cargo aliases, just after the alias. The description usually
# starts with a capital.
if words[1][0].isupper() or (len(words) > 2 and words[2][0].isupper()):
sub = words[0].replace(",", "")
if sub in prog:
# recursion!
continue
if not sub.isalnum():
continue
if sub[0].isupper():
continue
subcommands += [Subcommand(sub)]
continue
if "--" not in line:
continue
# Options
line = line.strip().split()
if len(line) < 1:
continue
short = None
if line[0].startswith("-") and not line[0].startswith("--"):
canonical = True
short = line[0][len("-"):][0]
line.pop(0)
while len(line) > 0:
if line[0].startswith("--"):
break
line.pop(0)
if line == []:
continue
if line[0].startswith("--"):
opt = line[0][len("--"):]
if "[" in opt:
opt = opt[:opt.find("[")]
if "=" in opt:
opt = opt[:opt.find("=")]
opt = opt.replace("]", "")
opt = opt.replace(",", "")
if not opt.replace("-", "").isalnum():
continue
if opt == "":
continue
if 2 >= len(opt):
continue
options += [Option(short, opt)]
subs = []
for sub in subcommands:
# Deconflict
abbrev = sub.abbrev()
for sub2 in subcommands:
if sub2 == sub:
continue
abbrev = sub.deconflict(sub2.abbrev())
if abbrev != sub.abbrev():
break
for opt in options:
if abbrev != sub.abbrev():
break
abbrev = sub.deconflict(opt.abbrev())
if len(abbrev) >= len(sub.cmd):
continue
subs += [f'"{prog},{abbrev},{sub.cmd}"']
opts = []
for opt in options:
# Deconflict
abbrev = opt.abbrev()
for opt2 in options:
if opt2 == opt:
continue
abbrev = opt.deconflict(opt2.abbrev())
if abbrev != opt.abbrev():
break
for sub in subcommands:
if abbrev != opt.abbrev():
break
abbrev = opt.deconflict(sub.abbrev())
opts += [f'"{prog},{abbrev},--{opt.long}"']
return (subcommands, subs, opts)
def go(cmds):
r = []
try:
out = check_output(cmds + ["--help"], stderr=STDOUT, encoding="utf-8")
except CalledProcessError:
return []
pfx = " ".join(cmds)
(subcmds, subs, opts) = extract(pfx, out)
for s in subcmds:
if s.cmd not in cmds:
r.extend(go(cmds + [s.cmd]))
for s in subs:
r += [(cmds, s)]
for o in opts:
r += [(cmds, o)]
return r
def main():
if len(argv) < 2:
print("Usage: ./extract-abbrevs.py COMMAND")
return
everything = sorted(go(argv[1:]), key=lambda x: len("".join(x[0])), reverse=True)
for (_, thing) in everything:
print(thing)
if __name__ == "__main__":
main()