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
50 # command auxiliary methods
55 def _construct_command_syntax(cls):
56 spec = getargspec(cls.main.im_func)
58 n = len(args) - len(spec.defaults or ())
59 required = ' '.join('<%s>' % x\
60 .replace('____', '[:')\
63 replace('_', ' ') for x in args[:n])
64 optional = ' '.join('[%s]' % x\
65 .replace('____', '[:')\
68 replace('_', ' ') for x in args[n:])
69 cls.syntax = ' '.join(x for x in [required, optional] if x)
71 cls.syntax += ' <%s ...>' % spec.varargs
74 def _num_of_matching_terms(basic_list, attack_list):
76 return len(basic_list)
79 for i, term in enumerate(basic_list):
81 if term != attack_list[i]:
89 def _update_best_match(name_terms, prefix=[]):
91 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
95 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
100 if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
101 if len(_best_match) < num_of_matching_terms:
102 _best_match = name_terms[:num_of_matching_terms]
107 def command(cmd_tree, prefix='', descedants_depth=1):
108 """Load a class as a command
109 e.g. spec_cmd0_cmd1 will be command spec cmd0
111 :param cmd_tree: is initialized in cmd_spec file and is the structure
112 where commands are loaded. Var name should be _commands
113 :param prefix: if given, load only commands prefixed with prefix,
114 :param descedants_depth: is the depth of the tree descedants of the
115 prefix command. It is used ONLY if prefix and if prefix is not
118 :returns: the specified class object
123 cls_name = cls.__name__
127 kloger.warning('command %s found but not loaded' % cls_name)
130 name_terms = cls_name.split('_')
131 if not _update_best_match(name_terms, prefix):
133 kloger.warning('%s failed to update_best_match' % cls_name)
137 max_len = len(_best_match) + descedants_depth
138 if len(name_terms) > max_len:
139 partial = '_'.join(name_terms[:max_len])
140 if not cmd_tree.has_command(partial): # add partial path
141 cmd_tree.add_command(partial)
143 kloger.warning('%s failed max_len test' % cls_name)
146 cls.description, sep, cls.long_description\
147 = cls.__doc__.partition('\n')
148 _construct_command_syntax(cls)
150 cmd_tree.add_command(cls_name, cls.description, cls)
155 cmd_spec_locations = [
156 'kamaki.cli.commands',
163 # Generic init auxiliary functions
166 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
167 """handle logging for clients package"""
169 def add_handler(name, level, prefix=''):
170 h = logging.StreamHandler()
171 fmt = logging.Formatter(prefix + '%(message)s')
173 logger = logging.getLogger(name)
175 logger.setLevel(level)
178 add_handler('', logging.CRITICAL)
182 add_handler('requests', logging.INFO, prefix='* ')
183 add_handler('clients.send', logging.DEBUG, prefix='> ')
184 add_handler('clients.recv', logging.DEBUG, prefix='< ')
185 add_handler('kamaki', logging.DEBUG, prefix='(debug): ')
187 add_handler('requests', logging.INFO, prefix='* ')
188 add_handler('clients.send', logging.INFO, prefix='> ')
189 add_handler('clients.recv', logging.INFO, prefix='< ')
190 add_handler('kamaki', logging.INFO, prefix='(i): ')
192 add_handler('clients.recv', logging.INFO)
193 add_handler('kamaki', logging.WARNING, prefix='(warning): ')
195 kloger = logging.getLogger('kamaki')
198 def _init_session(arguments):
200 _help = arguments['help'].value
202 _debug = arguments['debug'].value
204 _verbose = arguments['verbose'].value
206 _colors = arguments['config'].get('global', 'colors')
207 if not (stdout.isatty() and _colors == 'on'):
208 from kamaki.cli.utils import remove_colors
210 _silent = arguments['silent'].value
211 _include = arguments['include'].value
212 _setup_logging(_silent, _debug, _verbose, _include)
215 def _load_spec_module(spec, arguments, module):
216 spec_name = arguments['config'].get(spec, 'cli')
217 if spec_name is None:
220 for location in cmd_spec_locations:
221 location += spec_name if location == '' else '.%s' % spec_name
223 pkg = __import__(location, fromlist=[module])
230 def _groups_help(arguments):
234 for spec in arguments['config'].get_groups():
235 pkg = _load_spec_module(spec, arguments, '_commands')
240 cmd for cmd in getattr(pkg, '_commands')\
241 if arguments['config'].get(cmd.name, 'cli')
243 except AttributeError:
245 kloger.warning('No description for %s' % spec)
248 descriptions[cmd.name] = cmd.description
251 kloger.warning('no cmd specs in module %s' % spec)
253 kloger.warning('Loading of %s cmd spec failed' % spec)
254 print('\nOptions:\n - - - -')
255 print_dict(descriptions)
258 def _load_all_commands(cmd_tree, arguments):
259 _config = arguments['config']
260 for spec in [spec for spec in _config.get_groups()\
261 if _config.get(spec, 'cli')]:
263 spec_module = _load_spec_module(spec, arguments, '_commands')
264 spec_commands = getattr(spec_module, '_commands')
265 except AttributeError:
268 kloger.warning('No valid description for %s' % spec)
270 for spec_tree in spec_commands:
271 if spec_tree.name == spec:
272 cmd_tree.add_tree(spec_tree)
276 # Methods to be used by CLI implementations
279 def print_subcommands_help(cmd):
281 for subcmd in cmd.get_subcommands():
282 spec, sep, print_path = subcmd.path.partition('_')
283 printout[print_path.replace('_', ' ')] = subcmd.description
285 print('\nOptions:\n - - - -')
289 def update_parser_help(parser, cmd):
291 parser.syntax = parser.syntax.split('<')[0]
292 parser.syntax += ' '.join(_best_match)
296 cls = cmd.get_class()
297 parser.syntax += ' ' + cls.syntax
298 parser.update_arguments(cls().arguments)
299 description = getattr(cls, 'long_description', '')
300 description = description.strip()
302 parser.syntax += ' <...>'
303 if cmd.has_description:
304 parser.parser.description = cmd.help\
305 + ((' . . . %s' % description) if description else '')
307 parser.parser.description = description
310 def print_error_message(cli_err):
311 errmsg = '%s' % cli_err
312 if cli_err.importance == 1:
313 errmsg = magenta(errmsg)
314 elif cli_err.importance == 2:
315 errmsg = yellow(errmsg)
316 elif cli_err.importance > 2:
319 for errmsg in cli_err.details:
320 print('| %s' % errmsg)
323 def exec_cmd(instance, cmd_args, help_method):
325 return instance.main(*cmd_args)
326 except TypeError as err:
327 if err.args and err.args[0].startswith('main()'):
328 print(magenta('Syntax error'))
339 def get_command_group(unparsed, arguments):
340 groups = arguments['config'].get_groups()
341 for term in unparsed:
342 if term.startswith('-'):
345 unparsed.remove(term)
351 def set_command_params(parameters):
352 """Add a parameters list to a command
354 :param paramters: (list of str) a list of parameters
357 def_params = list(command.func_defaults)
358 def_params[0] = parameters
359 command.func_defaults = tuple(def_params)
364 def run_one_cmd(exe_string, parser):
367 parser.arguments['config'].get('history', 'file'))
368 _history.add(' '.join([exe_string] + argv[1:]))
369 from kamaki.cli import one_command
370 one_command.run(parser, _help)
373 def run_shell(exe_string, parser):
374 from command_shell import _init_shell
375 shell = _init_shell(exe_string, parser)
376 _load_all_commands(shell.cmd_tree, parser.arguments)
382 exe = basename(argv[0])
383 parser = ArgumentParseManager(exe)
385 if parser.arguments['version'].value:
388 _init_session(parser.arguments)
391 run_one_cmd(exe, parser)
393 parser.parser.print_help()
394 _groups_help(parser.arguments)
396 run_shell(exe, parser)
397 except CLIError as err:
398 print_error_message(err)
402 except Exception as er:
403 print('Unknown Error: %s' % er)