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.logger import add_stream_logger, get_logger
52 # command auxiliary methods
59 '____', '[:').replace(
65 def _construct_command_syntax(cls):
66 spec = getargspec(cls.main.im_func)
68 n = len(args) - len(spec.defaults or ())
69 required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
70 optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
71 cls.syntax = ' '.join(x for x in [required, optional] if x)
73 cls.syntax += ' <%s ...>' % spec.varargs
76 def _num_of_matching_terms(basic_list, attack_list):
78 return len(basic_list)
81 for i, term in enumerate(basic_list):
83 if term != attack_list[i]:
91 def _update_best_match(name_terms, prefix=[]):
93 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
97 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
102 if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
103 if len(_best_match) < num_of_matching_terms:
104 _best_match = name_terms[:num_of_matching_terms]
109 def command(cmd_tree, prefix='', descedants_depth=1):
110 """Load a class as a command
111 e.g. spec_cmd0_cmd1 will be command spec cmd0
113 :param cmd_tree: is initialized in cmd_spec file and is the structure
114 where commands are loaded. Var name should be _commands
115 :param prefix: if given, load only commands prefixed with prefix,
116 :param descedants_depth: is the depth of the tree descedants of the
117 prefix command. It is used ONLY if prefix and if prefix is not
120 :returns: the specified class object
125 cls_name = cls.__name__
129 kloger.warning('command %s found but not loaded' % cls_name)
132 name_terms = cls_name.split('_')
133 if not _update_best_match(name_terms, prefix):
135 kloger.warning('%s failed to update_best_match' % cls_name)
139 max_len = len(_best_match) + descedants_depth
140 if len(name_terms) > max_len:
141 partial = '_'.join(name_terms[:max_len])
142 if not cmd_tree.has_command(partial): # add partial path
143 cmd_tree.add_command(partial)
145 kloger.warning('%s failed max_len test' % cls_name)
149 cls.description, sep, cls.long_description
150 ) = cls.__doc__.partition('\n')
151 _construct_command_syntax(cls)
153 cmd_tree.add_command(cls_name, cls.description, cls)
158 cmd_spec_locations = [
159 'kamaki.cli.commands',
166 # Generic init auxiliary functions
169 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
170 """handle logging for clients package"""
173 add_stream_logger(__name__, logging.CRITICAL)
176 sfmt, rfmt = '> %(message)s', '< %(message)s'
178 add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
179 add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
180 add_stream_logger(__name__, logging.DEBUG)
182 add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
183 add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
184 add_stream_logger(__name__, logging.INFO)
186 add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
187 add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
188 add_stream_logger(__name__, logging.WARNING)
190 kloger = get_logger(__name__)
193 def _init_session(arguments):
195 _help = arguments['help'].value
197 _debug = arguments['debug'].value
199 _include = arguments['include'].value
201 _verbose = arguments['verbose'].value
203 _colors = arguments['config'].get('global', 'colors')
204 if not (stdout.isatty() and _colors == 'on'):
205 from kamaki.cli.utils import remove_colors
207 _silent = arguments['silent'].value
208 _setup_logging(_silent, _debug, _verbose, _include)
211 def _load_spec_module(spec, arguments, module):
212 spec_name = arguments['config'].get(spec, 'cli')
213 if spec_name is None:
216 for location in cmd_spec_locations:
217 location += spec_name if location == '' else '.%s' % spec_name
219 pkg = __import__(location, fromlist=[module])
226 def _groups_help(arguments):
230 for spec in arguments['config'].get_groups():
231 pkg = _load_spec_module(spec, arguments, '_commands')
235 _cnf = arguments['config']
236 cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
238 except AttributeError:
240 kloger.warning('No description for %s' % spec)
243 descriptions[cmd.name] = cmd.description
246 kloger.warning('no cmd specs in module %s' % spec)
248 kloger.warning('Loading of %s cmd spec failed' % spec)
249 print('\nOptions:\n - - - -')
250 print_dict(descriptions)
253 def _load_all_commands(cmd_tree, arguments):
254 _cnf = arguments['config']
255 specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
258 spec_module = _load_spec_module(spec, arguments, '_commands')
259 spec_commands = getattr(spec_module, '_commands')
260 except AttributeError:
263 kloger.warning('No valid description for %s' % spec)
265 for spec_tree in spec_commands:
266 if spec_tree.name == spec:
267 cmd_tree.add_tree(spec_tree)
271 # Methods to be used by CLI implementations
274 def print_subcommands_help(cmd):
276 for subcmd in cmd.get_subcommands():
277 spec, sep, print_path = subcmd.path.partition('_')
278 printout[print_path.replace('_', ' ')] = subcmd.description
280 print('\nOptions:\n - - - -')
284 def update_parser_help(parser, cmd):
286 parser.syntax = parser.syntax.split('<')[0]
287 parser.syntax += ' '.join(_best_match)
291 cls = cmd.get_class()
292 parser.syntax += ' ' + cls.syntax
293 parser.update_arguments(cls().arguments)
294 description = getattr(cls, 'long_description', '')
295 description = description.strip()
297 parser.syntax += ' <...>'
298 if cmd.has_description:
299 parser.parser.description = cmd.help + (
300 ('\n%s' % description) if description else '')
302 parser.parser.description = description
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 for errmsg in cli_err.details:
315 print('| %s' % errmsg)
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 log_file = parser.arguments['config'].get('global', 'log_file')
385 from kamaki.logger import set_log_filename
386 set_log_filename(log_file)
388 _init_session(parser.arguments)
390 from kamaki.cli.utils import suggest_missing
394 run_one_cmd(exe, parser)
396 parser.parser.print_help()
397 _groups_help(parser.arguments)
399 run_shell(exe, parser)
400 except CLIError as err:
401 print_error_message(err)
405 except Exception as er:
406 print('Unknown Error: %s' % er)