Allow general options to run a@ initial kamaki
[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 ArgumentParseManager
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 kloger = None
49
50
51 def _construct_command_syntax(cls):
52         spec = getargspec(cls.main.im_func)
53         args = spec.args[1:]
54         n = len(args) - len(spec.defaults or ())
55         required = ' '.join('<%s>' % x\
56             .replace('____', '[:')\
57             .replace('___', ':')\
58             .replace('__', ']').\
59             replace('_', ' ') for x in args[:n])
60         optional = ' '.join('[%s]' % x\
61             .replace('____', '[:')\
62             .replace('___', ':')\
63             .replace('__', ']').\
64             replace('_', ' ') for x in args[n:])
65         cls.syntax = ' '.join(x for x in [required, optional] if x)
66         if spec.varargs:
67             cls.syntax += ' <%s ...>' % spec.varargs
68
69
70 def _get_cmd_tree_from_spec(spec, cmd_tree_list):
71     for tree in cmd_tree_list:
72         if tree.name == spec:
73             return tree
74     return None
75
76
77 _best_match = []
78
79
80 def _num_of_matching_terms(basic_list, attack_list):
81     if not attack_list:
82         return len(basic_list)
83
84     matching_terms = 0
85     for i, term in enumerate(basic_list):
86         try:
87             if term != attack_list[i]:
88                 break
89         except IndexError:
90             break
91         matching_terms += 1
92     return matching_terms
93
94
95 def _update_best_match(name_terms, prefix=[]):
96     if prefix:
97         pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
98     else:
99         pref_list = []
100
101     num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
102     global _best_match
103     if not prefix:
104         _best_match = []
105
106     if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
107         if len(_best_match) < num_of_matching_terms:
108             _best_match = name_terms[:num_of_matching_terms]
109         return True
110     return False
111
112
113 def command(cmd_tree, prefix='', descedants_depth=1):
114     """Load a class as a command
115         e.g. spec_cmd0_cmd1 will be command spec cmd0
116
117         :param cmd_tree: is initialized in cmd_spec file and is the structure
118             where commands are loaded. Var name should be _commands
119         :param prefix: if given, load only commands prefixed with prefix,
120         :param descedants_depth: is the depth of the tree descedants of the
121             prefix command. It is used ONLY if prefix and if prefix is not
122             a terminal command
123
124         :returns: the specified class object
125     """
126
127     def wrap(cls):
128         global kloger
129         cls_name = cls.__name__
130
131         if not cmd_tree:
132             if _debug:
133                 kloger.warning('command %s found but not loaded' % cls_name)
134             return cls
135
136         name_terms = cls_name.split('_')
137         if not _update_best_match(name_terms, prefix):
138             if _debug:
139                 kloger.warning('%s failed to update_best_match' % cls_name)
140             return None
141
142         global _best_match
143         max_len = len(_best_match) + descedants_depth
144         if len(name_terms) > max_len:
145             partial = '_'.join(name_terms[:max_len])
146             if not cmd_tree.has_command(partial):  # add partial path
147                 cmd_tree.add_command(partial)
148             if _debug:
149                 kloger.warning('%s failed max_len test' % cls_name)
150             return None
151
152         cls.description, sep, cls.long_description\
153         = cls.__doc__.partition('\n')
154         _construct_command_syntax(cls)
155
156         cmd_tree.add_command(cls_name, cls.description, cls)
157         return cls
158     return wrap
159
160
161 def get_cmd_terms():
162     global command
163     return [term for term in command.func_defaults[0]\
164         if not term.startswith('-')]
165
166 cmd_spec_locations = [
167     'kamaki.cli.commands',
168     'kamaki.commands',
169     'kamaki.cli',
170     'kamaki',
171     '']
172
173
174 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
175     """handle logging for clients package"""
176
177     def add_handler(name, level, prefix=''):
178         h = logging.StreamHandler()
179         fmt = logging.Formatter(prefix + '%(message)s')
180         h.setFormatter(fmt)
181         logger = logging.getLogger(name)
182         logger.addHandler(h)
183         logger.setLevel(level)
184
185     if silent:
186         add_handler('', logging.CRITICAL)
187         return
188
189     if debug:
190         add_handler('requests', logging.INFO, prefix='* ')
191         add_handler('clients.send', logging.DEBUG, prefix='> ')
192         add_handler('clients.recv', logging.DEBUG, prefix='< ')
193         add_handler('kamaki', logging.DEBUG, prefix='DEBUG: ')
194     elif verbose:
195         add_handler('requests', logging.INFO, prefix='* ')
196         add_handler('clients.send', logging.INFO, prefix='> ')
197         add_handler('clients.recv', logging.INFO, prefix='< ')
198         add_handler('kamaki', logging.INFO, prefix='INFO: ')
199     elif include:
200         add_handler('clients.recv', logging.INFO)
201     add_handler('kamaki', logging.WARNING, prefix='WARNING: ')
202     global kloger
203     kloger = logging.getLogger('kamaki.warning')
204
205
206 def _init_session(arguments):
207     global _help
208     _help = arguments['help'].value
209     global _debug
210     _debug = arguments['debug'].value
211     global _verbose
212     _verbose = arguments['verbose'].value
213     global _colors
214     _colors = arguments['config'].get('global', 'colors')
215     if not (stdout.isatty() and _colors == 'on'):
216         from kamaki.cli.utils import remove_colors
217         remove_colors()
218     _silent = arguments['silent'].value
219     _include = arguments['include'].value
220     _setup_logging(_silent, _debug, _verbose, _include)
221
222
223 def get_command_group(unparsed, arguments):
224     groups = arguments['config'].get_groups()
225     for term in unparsed:
226         if term.startswith('-'):
227             continue
228         if term in groups:
229             unparsed.remove(term)
230             return term
231         return None
232     return None
233
234
235 def _load_spec_module(spec, arguments, module):
236     spec_name = arguments['config'].get(spec, 'cli')
237     if spec_name is None:
238         return None
239     pkg = None
240     for location in cmd_spec_locations:
241         location += spec_name if location == '' else '.%s' % spec_name
242         try:
243             pkg = __import__(location, fromlist=[module])
244             return pkg
245         except ImportError:
246             continue
247     return pkg
248
249
250 def _groups_help(arguments):
251     global _debug
252     global kloger
253     descriptions = {}
254     for spec in arguments['config'].get_groups():
255         pkg = _load_spec_module(spec, arguments, '_commands')
256         if pkg:
257             cmds = None
258             try:
259                 cmds = [
260                     cmd for cmd in getattr(pkg, '_commands')\
261                     if arguments['config'].get(cmd.name, 'cli')
262                 ]
263             except AttributeError:
264                 if _debug:
265                     kloger.warning('No description for %s' % spec)
266             try:
267                 for cmd in cmds:
268                     descriptions[cmd.name] = cmd.description
269             except TypeError:
270                 if _debug:
271                     kloger.warning('no cmd specs in module %s' % spec)
272         elif _debug:
273             kloger.warning('Loading of %s cmd spec failed' % spec)
274     print('\nOptions:\n - - - -')
275     print_dict(descriptions)
276
277
278 def _print_subcommands_help(cmd):
279     printout = {}
280     for subcmd in cmd.get_subcommands():
281         spec, sep, print_path = subcmd.path.partition('_')
282         printout[print_path.replace('_', ' ')] = subcmd.description
283     if printout:
284         print('\nOptions:\n - - - -')
285         print_dict(printout)
286
287
288 def _update_parser_help(parser, cmd):
289     global _best_match
290     parser.syntax = parser.syntax.split('<')[0]
291     parser.syntax += ' '.join(_best_match)
292
293     if cmd.is_command:
294         cls = cmd.get_class()
295         parser.syntax += ' ' + cls.syntax
296         parser.update_arguments(cls().arguments)
297         # arguments = cls().arguments
298         # update_arguments(parser, arguments)
299     else:
300         parser.syntax += ' <...>'
301     if cmd.has_description:
302         parser.parser.description = cmd.help
303
304
305 def _print_error_message(cli_err):
306     errmsg = '%s' % cli_err
307     if cli_err.importance == 1:
308         errmsg = magenta(errmsg)
309     elif cli_err.importance == 2:
310         errmsg = yellow(errmsg)
311     elif cli_err.importance > 2:
312         errmsg = red(errmsg)
313     stdout.write(errmsg)
314     print_list(cli_err.details)
315
316
317 def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
318     matched = [term for term in unparsed if not term.startswith('-')]
319     while matched:
320         try:
321             return cmd_tree.get_command('_'.join(matched))
322         except KeyError:
323             matched = matched[:-1]
324     return None
325
326
327 def _exec_cmd(instance, cmd_args, help_method):
328     try:
329         return instance.main(*cmd_args)
330     except TypeError as err:
331         if err.args and err.args[0].startswith('main()'):
332             print(magenta('Syntax error'))
333             if _debug:
334                 raise err
335             if _verbose:
336                 print(unicode(err))
337             help_method()
338         else:
339             raise
340     return 1
341
342
343 def set_command_params(parameters):
344     """Add a parameters list to a command
345
346     :param paramters: (list of str) a list of parameters
347     """
348     global command
349     def_params = list(command.func_defaults)
350     def_params[0] = parameters
351     command.func_defaults = tuple(def_params)
352
353
354 #def one_cmd(parser, unparsed, arguments):
355 def one_cmd(parser):
356     group = get_command_group(list(parser.unparsed), parser.arguments)
357     if not group:
358         parser.parser.print_help()
359         _groups_help(parser.arguments)
360         exit(0)
361
362     nonargs = [term for term in parser.unparsed if not term.startswith('-')]
363     set_command_params(nonargs)
364
365     global _best_match
366     _best_match = []
367
368     spec_module = _load_spec_module(group, parser.arguments, '_commands')
369
370     cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
371
372     if _best_match:
373         cmd = cmd_tree.get_command('_'.join(_best_match))
374     else:
375         cmd = _get_best_match_from_cmd_tree(cmd_tree, parser.unparsed)
376         _best_match = cmd.path.split('_')
377     if cmd is None:
378         if _debug or _verbose:
379             print('Unexpected error: failed to load command')
380         exit(1)
381
382     _update_parser_help(parser, cmd)
383
384     if _help or not cmd.is_command:
385         parser.parser.print_help()
386         _print_subcommands_help(cmd)
387         exit(0)
388
389     cls = cmd.get_class()
390     executable = cls(parser.arguments)
391     parser.update_arguments(executable.arguments)
392     #parsed, unparsed = parse_known_args(parser, executable.arguments)
393     for term in _best_match:
394         parser.unparsed.remove(term)
395     _exec_cmd(executable, parser.unparsed, parser.parser.print_help)
396
397
398 def _load_all_commands(cmd_tree, arguments):
399     _config = arguments['config']
400     for spec in [spec for spec in _config.get_groups()\
401             if _config.get(spec, 'cli')]:
402         try:
403             spec_module = _load_spec_module(spec, arguments, '_commands')
404             spec_commands = getattr(spec_module, '_commands')
405         except AttributeError:
406             if _debug:
407                 global kloger
408                 kloger.warning('No valid description for %s' % spec)
409             continue
410         for spec_tree in spec_commands:
411             if spec_tree.name == spec:
412                 cmd_tree.add_tree(spec_tree)
413                 break
414
415
416 def run_shell(exe_string, parser):
417     from command_shell import _init_shell
418     shell = _init_shell(exe_string, parser)
419     _load_all_commands(shell.cmd_tree, parser.arguments)
420     shell.run(parser)
421
422
423 def main():
424     try:
425         exe = basename(argv[0])
426         parser = ArgumentParseManager(exe)
427
428         if parser.arguments['version'].value:
429             exit(0)
430
431         _init_session(parser.arguments)
432
433         if parser.unparsed:
434             _history = History(
435                 parser.arguments['config'].get('history', 'file'))
436             _history.add(' '.join([exe] + argv[1:]))
437             one_cmd(parser)
438         elif _help:
439             parser.parser.print_help()
440             _groups_help(parser.arguments)
441         else:
442             run_shell(exe, parser)
443     except CLIError as err:
444         _print_error_message(err)
445         if _debug:
446             raise err
447         exit(1)
448     except Exception as er:
449         print('Unknown Error: %s' % er)
450         if _debug:
451             raise
452         exit(1)