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)
295 cls = cmd.get_class()
296 parser.syntax += ' ' + cls.syntax
297 parser.update_arguments(cls().arguments)
298 # arguments = cls().arguments
299 # update_arguments(parser, arguments)
301 parser.syntax += ' <...>'
302 if cmd.has_description:
303 parser.parser.description = cmd.help
306 def print_error_message(cli_err):
307 errmsg = '%s' % cli_err
308 if cli_err.importance == 1:
309 errmsg = magenta(errmsg)
310 elif cli_err.importance == 2:
311 errmsg = yellow(errmsg)
312 elif cli_err.importance > 2:
315 print_list(cli_err.details)
318 def exec_cmd(instance, cmd_args, help_method):
320 return instance.main(*cmd_args)
321 except TypeError as err:
322 if err.args and err.args[0].startswith('main()'):
323 print(magenta('Syntax error'))
334 def get_command_group(unparsed, arguments):
335 groups = arguments['config'].get_groups()
336 for term in unparsed:
337 if term.startswith('-'):
340 unparsed.remove(term)
346 def set_command_params(parameters):
347 """Add a parameters list to a command
349 :param paramters: (list of str) a list of parameters
352 def_params = list(command.func_defaults)
353 def_params[0] = parameters
354 command.func_defaults = tuple(def_params)
359 def run_one_cmd(exe_string, parser):
362 parser.arguments['config'].get('history', 'file'))
363 _history.add(' '.join([exe_string] + argv[1:]))
364 from kamaki.cli import one_command
365 one_command.run(parser, _help)
368 def run_shell(exe_string, parser):
369 from command_shell import _init_shell
370 shell = _init_shell(exe_string, parser)
371 _load_all_commands(shell.cmd_tree, parser.arguments)
377 exe = basename(argv[0])
378 parser = ArgumentParseManager(exe)
380 if parser.arguments['version'].value:
383 _init_session(parser.arguments)
386 run_one_cmd(exe, parser)
388 parser.parser.print_help()
389 _groups_help(parser.arguments)
391 run_shell(exe, parser)
392 except CLIError as err:
393 print_error_message(err)
397 except Exception as er:
398 print('Unknown Error: %s' % er)