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, red, magenta, yellow
42 from kamaki.cli.errors import CLIError
51 # command auxiliary methods
58 '____', '[:').replace(
64 def _construct_command_syntax(cls):
65 spec = getargspec(cls.main.im_func)
67 n = len(args) - len(spec.defaults or ())
68 required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
69 optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
70 cls.syntax = ' '.join(x for x in [required, optional] if x)
72 cls.syntax += ' <%s ...>' % spec.varargs
75 def _num_of_matching_terms(basic_list, attack_list):
77 return len(basic_list)
80 for i, term in enumerate(basic_list):
82 if term != attack_list[i]:
90 def _update_best_match(name_terms, prefix=[]):
92 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
96 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
101 if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
102 if len(_best_match) < num_of_matching_terms:
103 _best_match = name_terms[:num_of_matching_terms]
108 def command(cmd_tree, prefix='', descedants_depth=1):
109 """Load a class as a command
110 e.g. spec_cmd0_cmd1 will be command spec cmd0
112 :param cmd_tree: is initialized in cmd_spec file and is the structure
113 where commands are loaded. Var name should be _commands
114 :param prefix: if given, load only commands prefixed with prefix,
115 :param descedants_depth: is the depth of the tree descedants of the
116 prefix command. It is used ONLY if prefix and if prefix is not
119 :returns: the specified class object
124 cls_name = cls.__name__
128 kloger.warning('command %s found but not loaded' % cls_name)
131 name_terms = cls_name.split('_')
132 if not _update_best_match(name_terms, prefix):
134 kloger.warning('%s failed to update_best_match' % cls_name)
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)
144 kloger.warning('%s failed max_len test' % cls_name)
148 cls.description, sep, cls.long_description
149 ) = cls.__doc__.partition('\n')
150 _construct_command_syntax(cls)
152 cmd_tree.add_command(cls_name, cls.description, cls)
157 cmd_spec_locations = [
158 'kamaki.cli.commands',
165 # Generic init auxiliary functions
168 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
169 """handle logging for clients package"""
171 def add_handler(name, level, prefix=''):
172 h = logging.StreamHandler()
173 fmt = logging.Formatter(prefix + '%(message)s')
175 logger = logging.getLogger(name)
177 logger.setLevel(level)
180 add_handler('', logging.CRITICAL)
184 add_handler('requests', logging.INFO, prefix='* ')
185 add_handler('clients.send', logging.DEBUG, prefix='> ')
186 add_handler('clients.recv', logging.DEBUG, prefix='< ')
187 add_handler('kamaki', logging.DEBUG, prefix='(debug): ')
189 add_handler('requests', logging.INFO, prefix='* ')
190 add_handler('clients.send', logging.INFO, prefix='> ')
191 add_handler('clients.recv', logging.INFO, prefix='< ')
192 add_handler('kamaki', logging.INFO, prefix='(i): ')
194 add_handler('data.send', logging.INFO, prefix='>[data]: ')
195 add_handler('data.recv', logging.INFO, prefix='<[data]: ')
196 add_handler('kamaki', logging.WARNING, prefix='(warning): ')
198 kloger = logging.getLogger('kamaki')
201 def _init_session(arguments):
203 _help = arguments['help'].value
205 _debug = arguments['debug'].value
207 _include = arguments['include'].value
209 _verbose = arguments['verbose'].value
211 _colors = arguments['config'].get('global', 'colors')
212 if not (stdout.isatty() and _colors == 'on'):
213 from kamaki.cli.utils import remove_colors
215 _silent = arguments['silent'].value
216 _setup_logging(_silent, _debug, _verbose, _include)
219 def _load_spec_module(spec, arguments, module):
220 spec_name = arguments['config'].get(spec, 'cli')
221 if spec_name is None:
224 for location in cmd_spec_locations:
225 location += spec_name if location == '' else '.%s' % spec_name
227 pkg = __import__(location, fromlist=[module])
234 def _groups_help(arguments):
238 for spec in arguments['config'].get_groups():
239 pkg = _load_spec_module(spec, arguments, '_commands')
243 _cnf = arguments['config']
244 cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
246 except AttributeError:
248 kloger.warning('No description for %s' % spec)
251 descriptions[cmd.name] = cmd.description
254 kloger.warning('no cmd specs in module %s' % spec)
256 kloger.warning('Loading of %s cmd spec failed' % spec)
257 print('\nOptions:\n - - - -')
258 print_dict(descriptions)
261 def _load_all_commands(cmd_tree, arguments):
262 _cnf = arguments['config']
263 specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
266 spec_module = _load_spec_module(spec, arguments, '_commands')
267 spec_commands = getattr(spec_module, '_commands')
268 except AttributeError:
271 kloger.warning('No valid description for %s' % spec)
273 for spec_tree in spec_commands:
274 if spec_tree.name == spec:
275 cmd_tree.add_tree(spec_tree)
279 # Methods to be used by CLI implementations
282 def print_subcommands_help(cmd):
284 for subcmd in cmd.get_subcommands():
285 spec, sep, print_path = subcmd.path.partition('_')
286 printout[print_path.replace('_', ' ')] = subcmd.description
288 print('\nOptions:\n - - - -')
292 def update_parser_help(parser, cmd):
294 parser.syntax = parser.syntax.split('<')[0]
295 parser.syntax += ' '.join(_best_match)
299 cls = cmd.get_class()
300 parser.syntax += ' ' + cls.syntax
301 parser.update_arguments(cls().arguments)
302 description = getattr(cls, 'long_description', '')
303 description = description.strip()
305 parser.syntax += ' <...>'
306 if cmd.has_description:
307 parser.parser.description = cmd.help + (
308 ('\n%s' % description) if description else '')
310 parser.parser.description = description
313 def print_error_message(cli_err):
314 errmsg = '%s' % cli_err
315 if cli_err.importance == 1:
316 errmsg = magenta(errmsg)
317 elif cli_err.importance == 2:
318 errmsg = yellow(errmsg)
319 elif cli_err.importance > 2:
322 for errmsg in cli_err.details:
323 print('| %s' % errmsg)
326 def exec_cmd(instance, cmd_args, help_method):
328 return instance.main(*cmd_args)
329 except TypeError as err:
330 if err.args and err.args[0].startswith('main()'):
331 print(magenta('Syntax error'))
342 def get_command_group(unparsed, arguments):
343 groups = arguments['config'].get_groups()
344 for term in unparsed:
345 if term.startswith('-'):
348 unparsed.remove(term)
354 def set_command_params(parameters):
355 """Add a parameters list to a command
357 :param paramters: (list of str) a list of parameters
360 def_params = list(command.func_defaults)
361 def_params[0] = parameters
362 command.func_defaults = tuple(def_params)
367 def run_one_cmd(exe_string, parser):
370 parser.arguments['config'].get('history', 'file'))
371 _history.add(' '.join([exe_string] + argv[1:]))
372 from kamaki.cli import one_command
373 one_command.run(parser, _help)
376 def run_shell(exe_string, parser):
377 from command_shell import _init_shell
378 shell = _init_shell(exe_string, parser)
379 _load_all_commands(shell.cmd_tree, parser.arguments)
385 exe = basename(argv[0])
386 parser = ArgumentParseManager(exe)
388 if parser.arguments['version'].value:
391 log_file = parser.arguments['config'].get('global', 'log_file')
393 from kamaki.logger import set_log_filename
394 set_log_filename(log_file)
396 _init_session(parser.arguments)
398 from kamaki.cli.utils import suggest_missing
402 run_one_cmd(exe, parser)
404 parser.parser.print_help()
405 _groups_help(parser.arguments)
407 run_shell(exe, parser)
408 except CLIError as err:
409 print_error_message(err)
413 except Exception as er:
414 print('Unknown Error: %s' % er)