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
43 from kamaki.cli import logger
53 # command auxiliary methods
60 '____', '[:').replace(
66 def _construct_command_syntax(cls):
67 spec = getargspec(cls.main.im_func)
69 n = len(args) - len(spec.defaults or ())
70 required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
71 optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
72 cls.syntax = ' '.join(x for x in [required, optional] if x)
74 cls.syntax += ' <%s ...>' % spec.varargs
77 def _num_of_matching_terms(basic_list, attack_list):
79 return len(basic_list)
82 for i, term in enumerate(basic_list):
84 if term != attack_list[i]:
92 def _update_best_match(name_terms, prefix=[]):
94 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
98 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
103 if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
104 if len(_best_match) < num_of_matching_terms:
105 _best_match = name_terms[:num_of_matching_terms]
110 def command(cmd_tree, prefix='', descedants_depth=1):
111 """Load a class as a command
112 e.g. spec_cmd0_cmd1 will be command spec cmd0
114 :param cmd_tree: is initialized in cmd_spec file and is the structure
115 where commands are loaded. Var name should be _commands
116 :param prefix: if given, load only commands prefixed with prefix,
117 :param descedants_depth: is the depth of the tree descedants of the
118 prefix command. It is used ONLY if prefix and if prefix is not
121 :returns: the specified class object
126 cls_name = cls.__name__
130 kloger.warning('command %s found but not loaded' % cls_name)
133 name_terms = cls_name.split('_')
134 if not _update_best_match(name_terms, prefix):
136 kloger.warning('%s failed to update_best_match' % cls_name)
140 max_len = len(_best_match) + descedants_depth
141 if len(name_terms) > max_len:
142 partial = '_'.join(name_terms[:max_len])
143 if not cmd_tree.has_command(partial): # add partial path
144 cmd_tree.add_command(partial)
146 kloger.warning('%s failed max_len test' % cls_name)
150 cls.description, sep, cls.long_description
151 ) = cls.__doc__.partition('\n')
152 _construct_command_syntax(cls)
154 cmd_tree.add_command(cls_name, cls.description, cls)
159 cmd_spec_locations = [
160 'kamaki.cli.commands',
167 # Generic init auxiliary functions
170 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
171 """handle logging for clients package"""
174 logger.add_stream_logger(__name__, logging.CRITICAL)
177 sfmt, rfmt = '> %(message)s', '< %(message)s'
179 print('Logging location: %s' % logger.get_log_filename())
180 logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
181 logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
182 logger.add_stream_logger(__name__, logging.DEBUG)
184 logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
185 logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
186 logger.add_stream_logger(__name__, logging.INFO)
188 logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
189 logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
190 logger.add_stream_logger(__name__, logging.WARNING)
192 kloger = logger.get_logger(__name__)
195 def _init_session(arguments):
197 _help = arguments['help'].value
199 _debug = arguments['debug'].value
201 _include = arguments['include'].value
203 _verbose = arguments['verbose'].value
205 _colors = arguments['config'].get('global', 'colors')
206 if not (stdout.isatty() and _colors == 'on'):
207 from kamaki.cli.utils import remove_colors
209 _silent = arguments['silent'].value
210 _setup_logging(_silent, _debug, _verbose, _include)
213 def _load_spec_module(spec, arguments, module):
214 spec_name = arguments['config'].get(spec, 'cli')
215 if spec_name is None:
218 for location in cmd_spec_locations:
219 location += spec_name if location == '' else '.%s' % spec_name
221 pkg = __import__(location, fromlist=[module])
228 def _groups_help(arguments):
232 for spec in arguments['config'].get_groups():
233 pkg = _load_spec_module(spec, arguments, '_commands')
237 _cnf = arguments['config']
238 cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
240 except AttributeError:
242 kloger.warning('No description for %s' % spec)
245 descriptions[cmd.name] = cmd.description
248 kloger.warning('no cmd specs in module %s' % spec)
250 kloger.warning('Loading of %s cmd spec failed' % spec)
251 print('\nOptions:\n - - - -')
252 print_dict(descriptions)
255 def _load_all_commands(cmd_tree, arguments):
256 _cnf = arguments['config']
257 specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
260 spec_module = _load_spec_module(spec, arguments, '_commands')
261 spec_commands = getattr(spec_module, '_commands')
262 except AttributeError:
265 kloger.warning('No valid description for %s' % spec)
267 for spec_tree in spec_commands:
268 if spec_tree.name == spec:
269 cmd_tree.add_tree(spec_tree)
273 # Methods to be used by CLI implementations
276 def print_subcommands_help(cmd):
278 for subcmd in cmd.get_subcommands():
279 spec, sep, print_path = subcmd.path.partition('_')
280 printout[print_path.replace('_', ' ')] = subcmd.description
282 print('\nOptions:\n - - - -')
286 def update_parser_help(parser, cmd):
288 parser.syntax = parser.syntax.split('<')[0]
289 parser.syntax += ' '.join(_best_match)
293 cls = cmd.get_class()
294 parser.syntax += ' ' + cls.syntax
295 parser.update_arguments(cls().arguments)
296 description = getattr(cls, 'long_description', '')
297 description = description.strip()
299 parser.syntax += ' <...>'
300 if cmd.has_description:
301 parser.parser.description = cmd.help + (
302 ('\n%s' % description) if description else '')
304 parser.parser.description = description
307 def print_error_message(cli_err):
308 errmsg = '%s' % cli_err
309 if cli_err.importance == 1:
310 errmsg = magenta(errmsg)
311 elif cli_err.importance == 2:
312 errmsg = yellow(errmsg)
313 elif cli_err.importance > 2:
316 for errmsg in cli_err.details:
317 print('| %s' % errmsg)
320 def exec_cmd(instance, cmd_args, help_method):
322 return instance.main(*cmd_args)
323 except TypeError as err:
324 if err.args and err.args[0].startswith('main()'):
325 print(magenta('Syntax error'))
336 def get_command_group(unparsed, arguments):
337 groups = arguments['config'].get_groups()
338 for term in unparsed:
339 if term.startswith('-'):
342 unparsed.remove(term)
348 def set_command_params(parameters):
349 """Add a parameters list to a command
351 :param paramters: (list of str) a list of parameters
354 def_params = list(command.func_defaults)
355 def_params[0] = parameters
356 command.func_defaults = tuple(def_params)
361 def run_one_cmd(exe_string, parser):
364 parser.arguments['config'].get('history', 'file'))
365 _history.add(' '.join([exe_string] + argv[1:]))
366 from kamaki.cli import one_command
367 one_command.run(parser, _help)
370 def run_shell(exe_string, parser):
371 from command_shell import _init_shell
372 shell = _init_shell(exe_string, parser)
373 _load_all_commands(shell.cmd_tree, parser.arguments)
379 exe = basename(argv[0])
380 parser = ArgumentParseManager(exe)
382 if parser.arguments['version'].value:
385 log_file = parser.arguments['config'].get('global', 'log_file')
387 logger.set_log_filename(log_file)
389 filelog = logger.add_file_logger(__name__.split('.')[0])
390 filelog.info('Log call: %s' % ' '.join(argv))
392 _init_session(parser.arguments)
394 from kamaki.cli.utils import suggest_missing
398 run_one_cmd(exe, parser)
400 parser.parser.print_help()
401 _groups_help(parser.arguments)
403 run_shell(exe, parser)
404 except CLIError as err:
405 print_error_message(err)
409 except Exception as er:
410 print('Unknown Error: %s' % er)