1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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
35 from sys import argv, exit, stdout
36 from os.path import basename
37 from inspect import getargspec
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
51 def _construct_command_syntax(cls):
52 spec = getargspec(cls.main.im_func)
54 n = len(args) - len(spec.defaults or ())
55 required = ' '.join('<%s>' % x\
56 .replace('____', '[:')\
59 replace('_', ' ') for x in args[:n])
60 optional = ' '.join('[%s]' % x\
61 .replace('____', '[:')\
64 replace('_', ' ') for x in args[n:])
65 cls.syntax = ' '.join(x for x in [required, optional] if x)
67 cls.syntax += ' <%s ...>' % spec.varargs
70 def _get_cmd_tree_from_spec(spec, cmd_tree_list):
71 for tree in cmd_tree_list:
80 def _num_of_matching_terms(basic_list, attack_list):
82 return len(basic_list)
85 for i, term in enumerate(basic_list):
87 if term != attack_list[i]:
95 def _update_best_match(name_terms, prefix=[]):
97 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
101 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
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]
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
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
124 :returns: the specified class object
129 cls_name = cls.__name__
133 kloger.warning('command %s found but not loaded' % cls_name)
136 name_terms = cls_name.split('_')
137 if not _update_best_match(name_terms, prefix):
139 kloger.warning('%s failed to update_best_match' % cls_name)
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)
149 kloger.warning('%s failed max_len test' % cls_name)
152 cls.description, sep, cls.long_description\
153 = cls.__doc__.partition('\n')
154 _construct_command_syntax(cls)
156 cmd_tree.add_command(cls_name, cls.description, cls)
163 return [term for term in command.func_defaults[0]\
164 if not term.startswith('-')]
166 cmd_spec_locations = [
167 'kamaki.cli.commands',
174 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
175 """handle logging for clients package"""
177 def add_handler(name, level, prefix=''):
178 h = logging.StreamHandler()
179 fmt = logging.Formatter(prefix + '%(message)s')
181 logger = logging.getLogger(name)
183 logger.setLevel(level)
186 add_handler('', logging.CRITICAL)
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: ')
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: ')
200 add_handler('clients.recv', logging.INFO)
201 add_handler('kamaki', logging.WARNING, prefix='WARNING: ')
203 kloger = logging.getLogger('kamaki.warning')
206 def _init_session(arguments):
208 _help = arguments['help'].value
210 _debug = arguments['debug'].value
212 _verbose = arguments['verbose'].value
214 _colors = arguments['config'].get('global', 'colors')
215 if not (stdout.isatty() and _colors == 'on'):
216 from kamaki.cli.utils import remove_colors
218 _silent = arguments['silent'].value
219 _include = arguments['include'].value
220 _setup_logging(_silent, _debug, _verbose, _include)
223 def get_command_group(unparsed, arguments):
224 groups = arguments['config'].get_groups()
225 for term in unparsed:
226 if term.startswith('-'):
229 unparsed.remove(term)
235 def _load_spec_module(spec, arguments, module):
236 spec_name = arguments['config'].get(spec, 'cli')
237 if spec_name is None:
240 for location in cmd_spec_locations:
241 location += spec_name if location == '' else '.%s' % spec_name
243 pkg = __import__(location, fromlist=[module])
250 def _groups_help(arguments):
254 for spec in arguments['config'].get_groups():
255 pkg = _load_spec_module(spec, arguments, '_commands')
260 cmd for cmd in getattr(pkg, '_commands')\
261 if arguments['config'].get(cmd.name, 'cli')
263 except AttributeError:
265 kloger.warning('No description for %s' % spec)
268 descriptions[cmd.name] = cmd.description
271 kloger.warning('no cmd specs in module %s' % spec)
273 kloger.warning('Loading of %s cmd spec failed' % spec)
274 print('\nOptions:\n - - - -')
275 print_dict(descriptions)
278 def _print_subcommands_help(cmd):
280 for subcmd in cmd.get_subcommands():
281 spec, sep, print_path = subcmd.path.partition('_')
282 printout[print_path.replace('_', ' ')] = subcmd.description
284 print('\nOptions:\n - - - -')
288 def _update_parser_help(parser, cmd):
290 parser.syntax = parser.syntax.split('<')[0]
291 parser.syntax += ' '.join(_best_match)
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)
300 parser.syntax += ' <...>'
301 if cmd.has_description:
302 parser.parser.description = cmd.help
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:
314 print_list(cli_err.details)
317 def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
318 matched = [term for term in unparsed if not term.startswith('-')]
321 return cmd_tree.get_command('_'.join(matched))
323 matched = matched[:-1]
327 def _exec_cmd(instance, cmd_args, help_method):
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'))
343 def set_command_params(parameters):
344 """Add a parameters list to a command
346 :param paramters: (list of str) a list of parameters
349 def_params = list(command.func_defaults)
350 def_params[0] = parameters
351 command.func_defaults = tuple(def_params)
354 #def one_cmd(parser, unparsed, arguments):
356 group = get_command_group(list(parser.unparsed), parser.arguments)
358 parser.parser.print_help()
359 _groups_help(parser.arguments)
362 nonargs = [term for term in parser.unparsed if not term.startswith('-')]
363 set_command_params(nonargs)
368 spec_module = _load_spec_module(group, parser.arguments, '_commands')
370 cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
373 cmd = cmd_tree.get_command('_'.join(_best_match))
375 cmd = _get_best_match_from_cmd_tree(cmd_tree, parser.unparsed)
376 _best_match = cmd.path.split('_')
378 if _debug or _verbose:
379 print('Unexpected error: failed to load command')
382 _update_parser_help(parser, cmd)
384 if _help or not cmd.is_command:
385 parser.parser.print_help()
386 _print_subcommands_help(cmd)
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)
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')]:
403 spec_module = _load_spec_module(spec, arguments, '_commands')
404 spec_commands = getattr(spec_module, '_commands')
405 except AttributeError:
408 kloger.warning('No valid description for %s' % spec)
410 for spec_tree in spec_commands:
411 if spec_tree.name == spec:
412 cmd_tree.add_tree(spec_tree)
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)
425 exe = basename(argv[0])
426 parser = ArgumentParseManager(exe)
428 if parser.arguments['version'].value:
431 _init_session(parser.arguments)
435 parser.arguments['config'].get('history', 'file'))
436 _history.add(' '.join([exe] + argv[1:]))
439 parser.parser.print_help()
440 _groups_help(parser.arguments)
442 run_shell(exe, parser)
443 except CLIError as err:
444 _print_error_message(err)
448 except Exception as er:
449 print('Unknown Error: %s' % er)