-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcli_taxo.py
executable file
·350 lines (319 loc) · 13.3 KB
/
cli_taxo.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Parse CLI help and pretty print command taxonomy
# @author Alister Lewis-Bowen <bowena@vmware.com>
from __future__ import print_function
import sys
import os
import getopt
import subprocess
from colorama import Fore, Back, Style
import string
import re
from enum import Enum
from collections import defaultdict
INIT_CMD = ''
HELP_OPT = '-h'
HELP_OPT_POSITIONS = Enum('HELP_OPT_POSITIONS', 'before after')
HELP_OPT_POSITION = HELP_OPT_POSITIONS.after.name
USAGE_TOKEN_RE = '^(Usage:)|^(Usage of)'
USAGE_RE = '^\s+?(\w+?)\s+'
OPTIONS_TOKEN_RE = '^Options:'
OPTIONS_RE = '^\s+?(-.+?)\s\s'
COMMANDS_TOKEN_RE = '^(Commands:)|^(Management Commands:)'
COMMANDS_RE = '^\s+(\w+?)\s+'
SHOW_OPTIONS = False
SHOW_COMMANDS = True
SHOW_USAGE = True
EXCLUDE_HELP_OPTS = False
OUTPUT_FORMATS = Enum('OUTPUT_FORMATS', 'tree csv table bash zsh')
OUTPUT_FORMAT = OUTPUT_FORMATS.tree.name
TABLE_COLS = 6
COMPLETION_CMDS = defaultdict(list)
MAX_DEPTH = 3
_DEBUG=False
# If usage description returned is a man page, make sure it
# - returned with out a pager
# - returned with no formatting control chars
os.environ['MANPAGER'] = 'col -b'
def usage():
print('Parse CLI command usage description to Pretty print a CLI '
"command taxonomy")
print("\nUsage:")
print(" cli_taxo <command_name> [--help-opt <string>] "
"[--help-opt-position before|after] "
"[--commands-filter <reg_ex>] "
"[--commands-token <reg_ex>] "
"[--options-filters <reg_ex>] "
"[--options-token <reg_ex>] "
"[--exclude-help] "
"[-o tree|csv|table|bash|zsh | --output tree|csv|table|bash|zsh] "
"[-O | --show-opts] "
"[--depth <number>} "
"[-D]")
print(" cli_taxo -h | --help")
print("\nOptions:")
print(" -h, --help Show this usage description")
print(" --help-opt The command option string used to show the "
"usage description text. Defaults to: ", HELP_OPT)
print(" --help-opt-position Placement of the command option string used to show the "
"usage description text. Typically the help command option is used after a "
"subcommand but sometime, a CLI requires it before. Defaults to: ", HELP_OPT_POSITION)
print(" --commands-filter The regular expression to extract the command "
"from the description text. Defaults to: ", COMMANDS_RE)
print(" --commands-token The regular expression to find the line after "
"which the CLI commands are found in the description text. "
"Defaults to: ", COMMANDS_TOKEN_RE)
print(" --options-filter The regular expression to extract the option "
"from the description text. Defaults to: ", OPTIONS_RE)
print(" --options-token The regular expression to find the line after "
"which the CLI options are found in the description text. "
"Defaults to: ", OPTIONS_TOKEN_RE)
print(" --exclude-help Exclude any help options from the output.")
print(" -o tree|csv|table|bash|zsh, --output tree|csv|table|bash|zsh "
"Defaults to: ", OUTPUT_FORMAT)
print(" -O, --show-opts Include options in the output")
print(" -d, --depth Limit the depth of command to parse. Defaults to: ", MAX_DEPTH)
print(" -D Display debug information to STDERR")
print("\nExamples:")
print("Generate an ASCII tree of the docker CLI commands with no options: ")
print(" cli_taxo.py docker")
print("\nGenerate a tree of the kubectl CLI command and options: ")
print(" cli_taxo.py kubectl \\")
print(" --commands-token 'Commands\s\(\S+\):|Commands:' \\")
print(" --commands-filter '^\s\s((?!#)\S+)\s+[A-Z]' \\")
print(" --options-token '^Options:' \\")
print(" --options-filter '^\s+?(-.+?):' \\")
print(" --show-opts")
print("\nIt is useful to run cli_taxo in debug mode when constructing the ")
print("regualr expressions to filter on the CLI commands and options.")
print("\nThe output formats include an ASCII tree, comma seperated values, a ")
print("very simple wiki/markdown table, and a bash autocompletion script ")
print("\nExamples of using the autocompletion output: ")
print(" cli_taxo.py docker --show-opts --output bash > ~/.docker/completion.bash.inc")
print(" printf \"\\n# Docker shell completion\\nsource '$HOME/.docker/completion.bash.inc'\\n\" >> $HOME/.bash_profile")
print(" source $HOME/.bash_profile")
print("\nTo apply the bash autocompletion to the current shell: ")
print(" source <(cli_taxo.py docker --show-opts --output bash)")
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def run_command(command):
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return iter(p.stdout.readline, '')
def parse_options_and_commands(command, depth=-1):
depth += 1
if depth > MAX_DEPTH : return
if _DEBUG:
eprint("\n{:s}Parsing: {:s}{:s}".format(
' '*depth+Fore.GREEN,
' '.join(map(str, command)),
Fore.RESET))
if USAGE_TOKEN_RE:
found_usage = False
else:
found_usage = True
if OPTIONS_TOKEN_RE:
found_options = False
else:
found_options = True
if COMMANDS_TOKEN_RE:
found_commands = False
else:
found_commands = True
for _line in run_command(command):
line = _line.strip('\n')
if not line or line.isspace(): continue
# line = line.decode('utf-8')
# line = "".join(i for i in line if 31 < ord(i) < 127)
if _DEBUG:
eprint("\n{:s}[{:s}{:s}] Line >>{:s}<<{:s}".format(
' '*depth+Style.DIM,
'U✓' if found_usage else 'U ',
'C✓' if found_commands else 'C ',
'O✓' if found_options else 'O ',
line, Style.RESET_ALL))
if _OPTIONS_TOKEN_RE.search(line) and not found_options:
found_options = True
if _OPTIONS_RE.search(line) and SHOW_OPTIONS and found_options:
for match in _OPTIONS_RE.search(line).groups():
if _DEBUG:
eprint('{:s} Opt match: >>{:s}<<'.format(
' '*depth, match))
if match and not (_HELP_RE.search(match)
and EXCLUDE_HELP_OPTS):
content = format_item(depth, command, match)
if content is not None:
print(content)
if _COMMANDS_TOKEN_RE.search(line) and not found_commands:
found_commands = True
if _COMMANDS_RE.search(line) and SHOW_COMMANDS and found_commands:
for match in _COMMANDS_RE.search(line).groups():
if _DEBUG:
eprint('{:s} Cmd match: >>{:s}<<'.format(
' '*depth, match))
if match and not (_HELP_RE.search(match)
and EXCLUDE_HELP_OPTS):
content = format_item(depth, command, match)
if content is not None:
print(content)
_command = command[:-1]
if HELP_OPT_POSITION == HELP_OPT_POSITIONS.after.name:
_command.extend([match, HELP_OPT])
elif HELP_OPT_POSITION == HELP_OPT_POSITIONS.before.name:
_command.extend([HELP_OPT, match])
parse_options_and_commands(_command, depth)
# if _USAGE_TOKEN_RE.search(line) and not found_usage:
# found_usage = True
# if _USAGE_RE.search(line) and SHOW_USAGE and found_usage:
# for match in _USAGE_RE.search(line).groups():
# if _DEBUG:
# eprint('{:s} Usage match: >>{:s}<<'.format(
# ' '*depth, match))
# if match and not (_HELP_RE.search(match)
# and EXCLUDE_HELP_OPTS):
# content = format_item(depth, command, match)
# if content is not None:
# print(content)
depth -= 1
def format_item(depth, command, item):
_command = command[:-1]
item = item.strip()
if OUTPUT_FORMAT == OUTPUT_FORMATS.csv.name:
item = string.replace(item, ',', ' | ')
return ','.join(_command) + ',' + item
elif OUTPUT_FORMAT == OUTPUT_FORMATS.table.name:
return '| '*2 +'| '*depth + item +' |'*(TABLE_COLS-1-depth)
elif OUTPUT_FORMAT == OUTPUT_FORMATS.bash.name or OUTPUT_FORMAT == OUTPUT_FORMATS.zsh.name:
COMPLETION_CMDS[command[depth]].append(item)
return
else: # OUTPUT_FORMATS.tree
if depth == 0:
prefix = '└── '
else:
prefix = '│ '*depth + '└── '
return prefix + item
def create_bash_completion_script():
with open(os.path.dirname(sys.argv[0]) +'/bash_completion.tmpl', 'r') as file:
content = file.read()
content = content.replace('%CMD%', INIT_CMD)
parts = content.partition('%COMPLETIONS%')
content = parts[0]
top_level_commands = []
for command, subcommands in COMPLETION_CMDS.iteritems():
top_level_commands.append(command)
content = content +" "+ command +") cmds=\""+ ' '.join(subcommands) +"\";;\n"
content = content +" *) cmds=\""+ ' '.join(top_level_commands) +'";;'
content = content + parts[2]
print(content)
def create_zsh_completion_script():
print('Generate the bash completion to a file, then run the following to enable it...')
print("\tautoload bashcompinit")
print("\tbashcompinit")
print("\tsource /path/to/your/bash_completion_file")
def main(argv):
try:
opts, non_opts = getopt.gnu_getopt(argv, "ho:d:OD", [
'help-opt=',
'help-opt-position=',
'commands-filter=',
'commands-token=',
'options-filter=',
'options-token=',
'exclude-help',
'show-opts',
'output=',
'depth=',
'help'])
except getopt.GetoptError:
usage()
sys.exit()
if not non_opts:
print("\033[31;1mError: \033[0mPlease provide the command name\n")
usage()
sys.exit()
else:
global INIT_CMD
INIT_CMD = non_opts[0]
for opt, arg in opts:
if opt == '--help-opt':
global HELP_OPT
HELP_OPT = arg
if opt == '--help-opt-position':
global HELP_OPT_POSITION
if arg in HELP_OPT_POSITIONS.__members__:
HELP_OPT_POSITION = arg
else:
print("\033[31;1mError: \033[0mPlease use the correct help option position\n")
usage()
sys.exit()
elif opt == '--commands-filter':
global COMMANDS_RE
COMMANDS_RE = arg
elif opt == '--commands-token':
global COMMANDS_TOKEN_RE
COMMANDS_TOKEN_RE = arg
elif opt == '--options-filter':
global OPTIONS_RE
OPTIONS_RE = arg
elif opt == '--options-token':
global OPTIONS_TOKEN_RE
OPTIONS_TOKEN_RE = arg
elif opt == '--exclude-help':
global EXCLUDE_HELP_OPTS
EXCLUDE_HELP_OPTS = True
elif opt in ('-o', '--output'):
global OUTPUT_FORMAT
if arg in OUTPUT_FORMATS.__members__:
OUTPUT_FORMAT = arg
else:
print("\033[31;1mError: \033[0mPlease use the correct output format\n")
usage()
sys.exit()
elif opt in ('-O', '--show_opts'):
global SHOW_OPTIONS
SHOW_OPTIONS = True
elif opt in ('-d', '--depth'):
global MAX_DEPTH
MAX_DEPTH = arg
elif opt == '-D':
global _DEBUG
_DEBUG = True
elif opt in ('-h', '--help'):
usage()
sys.exit()
if _DEBUG:
eprint('INT_CMD:', INIT_CMD)
eprint('HELP_OPT:', HELP_OPT)
eprint('HELP_OPT_POSITION:', HELP_OPT_POSITION)
eprint('OPTIONS_TOKEN_RE:', OPTIONS_TOKEN_RE)
eprint('OPTIONS_RE:', OPTIONS_RE)
eprint('COMMANDS_TOKEN_RE:', COMMANDS_TOKEN_RE)
eprint('COMMANDS_RE:', COMMANDS_RE)
eprint('SHOW_OPTIONS:', SHOW_OPTIONS)
eprint('SHOW_COMMANDS:', SHOW_COMMANDS)
eprint('EXCLUDE_HELP_OPTS:', EXCLUDE_HELP_OPTS)
eprint('OUTPUT_FORMAT:', OUTPUT_FORMAT)
global _OPTIONS_TOKEN_RE
_OPTIONS_TOKEN_RE = re.compile(r""+OPTIONS_TOKEN_RE)
global _OPTIONS_RE
_OPTIONS_RE = re.compile(r""+OPTIONS_RE)
global _COMMANDS_TOKEN_RE
_COMMANDS_TOKEN_RE = re.compile(r""+COMMANDS_TOKEN_RE)
global _COMMANDS_RE
_COMMANDS_RE = re.compile(r""+COMMANDS_RE)
global _HELP_RE
_HELP_RE = re.compile(r'help')
if OUTPUT_FORMAT == OUTPUT_FORMATS.table.name:
print('| '+ INIT_CMD +' |'*(TABLE_COLS))
elif OUTPUT_FORMAT == OUTPUT_FORMATS.bash.name or OUTPUT_FORMAT == OUTPUT_FORMATS.zsh.name:
pass
else:
print(INIT_CMD)
parse_options_and_commands([INIT_CMD, HELP_OPT])
if OUTPUT_FORMAT == OUTPUT_FORMATS.bash.name:
create_bash_completion_script()
elif OUTPUT_FORMAT == OUTPUT_FORMATS.zsh.name:
create_zsh_completion_script()
del os.environ['MANPAGER']
if __name__ == "__main__":
main(sys.argv[1:])