Create ArgumentParseManager, with __init__
[kamaki] / kamaki / cli / __init__.py
1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
33
34 import logging
35 from sys import argv, exit, stdout
36 from os.path import basename
37 from inspect import getargspec
38
39 from kamaki.cli.argument import _arguments, parse_known_args, update_arguments
40 from kamaki.cli.history import History
41 from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
42 from kamaki.cli.errors import CLIError
43
44 _help = False
45 _debug = False
46 _verbose = False
47 _colors = False
48
49
50 def _construct_command_syntax(cls):
51         spec = getargspec(cls.main.im_func)
52         args = spec.args[1:]
53         n = len(args) - len(spec.defaults or ())
54         required = ' '.join('<%s>' % x\
55             .replace('____', '[:')\
56             .replace('___', ':')\
57             .replace('__', ']').\
58             replace('_', ' ') for x in args[:n])
59         optional = ' '.join('[%s]' % x\
60             .replace('____', '[:')\
61             .replace('___', ':')\
62             .replace('__', ']').\
63             replace('_', ' ') for x in args[n:])
64         cls.syntax = ' '.join(x for x in [required, optional] if x)
65         if spec.varargs:
66             cls.syntax += ' <%s ...>' % spec.varargs
67
68
69 def _get_cmd_tree_from_spec(spec, cmd_tree_list):
70     for tree in cmd_tree_list:
71         if tree.name == spec:
72             return tree
73     return None
74
75
76 _best_match = []
77
78
79 def _num_of_matching_terms(basic_list, attack_list):
80     if not attack_list:
81         return len(basic_list)
82
83     matching_terms = 0
84     for i, term in enumerate(basic_list):
85         try:
86             if term != attack_list[i]:
87                 break
88         except IndexError:
89             break
90         matching_terms += 1
91     return matching_terms
92
93
94 def _update_best_match(name_terms, prefix=[]):
95     if prefix:
96         pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
97     else:
98         pref_list = []
99
100     num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
101     global _best_match
102     if not prefix:
103         _best_match = []
104
105     if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
106         if len(_best_match) < num_of_matching_terms:
107             _best_match = name_terms[:num_of_matching_terms]
108         return True
109     return False
110
111
112 def command(cmd_tree, prefix='', descedants_depth=1):
113     """Load a class as a command
114         spec_cmd0_cmd1 will be command spec cmd0
115         @cmd_tree is initialized in cmd_spec file and is the structure
116             where commands are loaded. Var name should be _commands
117         @param prefix if given, load only commands prefixed with prefix,
118         @param descedants_depth is the depth of the tree descedants of the
119             prefix command. It is used ONLY if prefix and if prefix is not
120             a terminal command
121     """
122
123     def wrap(cls):
124         cls_name = cls.__name__
125
126         if not cmd_tree:
127             if _debug:
128                 print('Warning: command %s found but not loaded' % cls_name)
129             return cls
130
131         name_terms = cls_name.split('_')
132         if not _update_best_match(name_terms, prefix):
133             if _debug:
134                 print('Warning: %s failed to update_best_match' % cls_name)
135             return None
136
137         global _best_match
138         max_len = len(_best_match) + descedants_depth
139         if len(name_terms) > max_len:
140             partial = '_'.join(name_terms[:max_len])
141             if not cmd_tree.has_command(partial):  # add partial path
142                 cmd_tree.add_command(partial)
143             if _debug:
144                 print('Warning: %s failed max_len test' % cls_name)
145             return None
146
147         cls.description, sep, cls.long_description\
148         = cls.__doc__.partition('\n')
149         _construct_command_syntax(cls)
150
151         cmd_tree.add_command(cls_name, cls.description, cls)
152         return cls
153     return wrap
154
155
156 def get_cmd_terms():
157     global command
158     return [term for term in command.func_defaults[0]\
159         if not term.startswith('-')]
160
161 cmd_spec_locations = [
162     'kamaki.cli.commands',
163     'kamaki.commands',
164     'kamaki.cli',
165     'kamaki',
166     '']
167
168
169 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
170     """handle logging for clients package"""
171
172     def add_handler(name, level, prefix=''):
173         h = logging.StreamHandler()
174         fmt = logging.Formatter(prefix + '%(message)s')
175         h.setFormatter(fmt)
176         logger = logging.getLogger(name)
177         logger.addHandler(h)
178         logger.setLevel(level)
179
180     if silent:
181         add_handler('', logging.CRITICAL)
182     elif debug:
183         add_handler('requests', logging.INFO, prefix='* ')
184         add_handler('clients.send', logging.DEBUG, prefix='> ')
185         add_handler('clients.recv', logging.DEBUG, prefix='< ')
186     elif verbose:
187         add_handler('requests', logging.INFO, prefix='* ')
188         add_handler('clients.send', logging.INFO, prefix='> ')
189         add_handler('clients.recv', logging.INFO, prefix='< ')
190     elif include:
191         add_handler('clients.recv', logging.INFO)
192     else:
193         add_handler('', logging.WARNING)
194
195
196 def _init_session(arguments):
197     global _help
198     _help = arguments['help'].value
199     global _debug
200     _debug = arguments['debug'].value
201     global _verbose
202     _verbose = arguments['verbose'].value
203     global _colors
204     _colors = arguments['config'].get('global', 'colors')
205     if not (stdout.isatty() and _colors == 'on'):
206         from kamaki.cli.utils import remove_colors
207         remove_colors()
208     _silent = arguments['silent'].value
209     _include = arguments['include'].value
210     _setup_logging(_silent, _debug, _verbose, _include)
211
212
213 def get_command_group(unparsed, arguments):
214     groups = arguments['config'].get_groups()
215     for term in unparsed:
216         if term.startswith('-'):
217             continue
218         if term in groups:
219             unparsed.remove(term)
220             return term
221         return None
222     return None
223
224
225 def _load_spec_module(spec, arguments, module):
226     spec_name = arguments['config'].get(spec, 'cli')
227     if spec_name is None:
228         return None
229     pkg = None
230     for location in cmd_spec_locations:
231         location += spec_name if location == '' else '.%s' % spec_name
232         try:
233             pkg = __import__(location, fromlist=[module])
234             return pkg
235         except ImportError:
236             continue
237     return pkg
238
239
240 def _groups_help(arguments):
241     global _debug
242     descriptions = {}
243     for spec in arguments['config'].get_groups():
244         pkg = _load_spec_module(spec, arguments, '_commands')
245         if pkg:
246             cmds = None
247             try:
248                 cmds = [
249                     cmd for cmd in getattr(pkg, '_commands')\
250                     if arguments['config'].get(cmd.name, 'cli')
251                 ]
252             except AttributeError:
253                 if _debug:
254                     print('Warning: No description for %s' % spec)
255             try:
256                 for cmd in cmds:
257                     descriptions[cmd.name] = cmd.description
258             except TypeError:
259                 if _debug:
260                     print('Warning: no cmd specs in module %s' % spec)
261         elif _debug:
262             print('Warning: Loading of %s cmd spec failed' % spec)
263     print('\nOptions:\n - - - -')
264     print_dict(descriptions)
265
266
267 def _print_subcommands_help(cmd):
268     printout = {}
269     for subcmd in cmd.get_subcommands():
270         spec, sep, print_path = subcmd.path.partition('_')
271         printout[print_path.replace('_', ' ')] = subcmd.description
272     if printout:
273         print('\nOptions:\n - - - -')
274         print_dict(printout)
275
276
277 def _update_parser_help(parser, cmd):
278     global _best_match
279     parser.prog = parser.prog.split('<')[0]
280     parser.prog += ' '.join(_best_match)
281
282     if cmd.is_command:
283         cls = cmd.get_class()
284         parser.prog += ' ' + cls.syntax
285         arguments = cls().arguments
286         update_arguments(parser, arguments)
287     else:
288         parser.prog += ' <...>'
289     if cmd.has_description:
290         parser.description = cmd.help
291
292
293 def _print_error_message(cli_err):
294     errmsg = '%s' % cli_err
295     if cli_err.importance == 1:
296         errmsg = magenta(errmsg)
297     elif cli_err.importance == 2:
298         errmsg = yellow(errmsg)
299     elif cli_err.importance > 2:
300         errmsg = red(errmsg)
301     stdout.write(errmsg)
302     print_list(cli_err.details)
303
304
305 def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
306     matched = [term for term in unparsed if not term.startswith('-')]
307     while matched:
308         try:
309             return cmd_tree.get_command('_'.join(matched))
310         except KeyError:
311             matched = matched[:-1]
312     return None
313
314
315 def _exec_cmd(instance, cmd_args, help_method):
316     try:
317         return instance.main(*cmd_args)
318     except TypeError as err:
319         if err.args and err.args[0].startswith('main()'):
320             print(magenta('Syntax error'))
321             if _debug:
322                 raise err
323             if _verbose:
324                 print(unicode(err))
325             help_method()
326         else:
327             raise
328     return 1
329
330
331 def set_command_param(param, value):
332     if param == 'prefix':
333         pos = 0
334     elif param == 'descedants_depth':
335         pos = 1
336     else:
337         return
338     global command
339     def_params = list(command.func_defaults)
340     def_params[pos] = value
341     command.func_defaults = tuple(def_params)
342
343
344 def one_cmd(parser, unparsed, arguments):
345     group = get_command_group(list(unparsed), arguments)
346     if not group:
347         parser.print_help()
348         _groups_help(arguments)
349         exit(0)
350
351     set_command_param(
352         'prefix',
353         [term for term in unparsed if not term.startswith('-')]
354     )
355     global _best_match
356     _best_match = []
357
358     spec_module = _load_spec_module(group, arguments, '_commands')
359
360     cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
361
362     if _best_match:
363         cmd = cmd_tree.get_command('_'.join(_best_match))
364     else:
365         cmd = _get_best_match_from_cmd_tree(cmd_tree, unparsed)
366         _best_match = cmd.path.split('_')
367     if cmd is None:
368         if _debug or _verbose:
369             print('Unexpected error: failed to load command')
370         exit(1)
371
372     _update_parser_help(parser, cmd)
373
374     if _help or not cmd.is_command:
375         parser.print_help()
376         _print_subcommands_help(cmd)
377         exit(0)
378
379     cls = cmd.get_class()
380     executable = cls(arguments)
381     parsed, unparsed = parse_known_args(parser, executable.arguments)
382     for term in _best_match:
383         unparsed.remove(term)
384     _exec_cmd(executable, unparsed, parser.print_help)
385
386
387 def _load_all_commands(cmd_tree, arguments):
388     _config = arguments['config']
389     for spec in [spec for spec in _config.get_groups()\
390             if _config.get(spec, 'cli')]:
391         try:
392             spec_module = _load_spec_module(spec, arguments, '_commands')
393             spec_commands = getattr(spec_module, '_commands')
394         except AttributeError:
395             if _debug:
396                 print('Warning: No valid description for %s' % spec)
397             continue
398         for spec_tree in spec_commands:
399             if spec_tree.name == spec:
400                 cmd_tree.add_tree(spec_tree)
401                 break
402
403
404 def run_shell(exe_string, arguments):
405     from command_shell import _init_shell
406     shell = _init_shell(exe_string, arguments)
407     _load_all_commands(shell.cmd_tree, arguments)
408     shell.run(arguments)
409
410
411 from kamaki.cli.argument import ArgumentParseManager
412
413
414 def main():
415     try:
416         exe = basename(argv[0])
417         parser = ArgumentParseManager(exe)
418         parsed, unparsed = parse_known_args(parser.parser, parser.arguments)
419
420         if _arguments['version'].value:
421             exit(0)
422
423         _init_session(_arguments)
424
425         if unparsed:
426             _history = History(_arguments['config'].get('history', 'file'))
427             _history.add(' '.join([exe] + argv[1:]))
428             one_cmd(parser.parser, unparsed, parser.arguments)
429         elif _help:
430             parser.parser.print_help()
431             _groups_help(_arguments)
432         else:
433             run_shell(exe, _arguments)
434     except CLIError as err:
435         if _debug:
436             raise err
437         _print_error_message(err)
438         exit(1)
439     except Exception as err:
440         if _debug:
441             raise err
442         print('Unknown Error: %s' % err)