2 # Copyright 2011-2012 GRNET S.A. All rights reserved.
4 # Redistribution and use in source and binary forms, with or
5 # without modification, are permitted provided that the following
8 # 1. Redistributions of source code must retain the above
9 # copyright notice, this list of conditions and the following
12 # 2. Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials
15 # provided with the distribution.
17 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 # POSSIBILITY OF SUCH DAMAGE.
30 # The views and conclusions contained in the software and
31 # documentation are those of the authors and should not be
32 # interpreted as representing official policies, either expressed
33 # or implied, of GRNET S.A.
35 #from __future__ import print_function
39 from inspect import getargspec
40 from argparse import ArgumentParser, ArgumentError
41 from os.path import basename
42 from sys import exit, stdout, argv
44 from kamaki.cli.errors import CLIError, CLICmdSpecError
45 from kamaki.cli.utils import magenta, red, yellow, print_dict, print_list,\
47 from kamaki.cli.command_tree import CommandTree
48 from kamaki.cli.argument import _arguments, parse_known_args
49 from kamaki.cli.history import History
51 cmd_spec_locations = [
52 'kamaki.cli.commands',
57 _commands = CommandTree(name='kamaki',
58 description='A command line tool for poking clouds')
60 # If empty, all commands are loaded, if not empty, only commands in this list
61 # e.g. [store, lele, list, lolo] is good to load store_list but not list_store
62 # First arg should always refer to a group
63 candidate_command_terms = []
64 allow_no_commands = False
65 allow_all_commands = False
66 allow_subclass_signatures = False
69 def _allow_class_in_cmd_tree(cls):
70 global allow_all_commands
71 if allow_all_commands:
73 global allow_no_commands
77 term_list = cls.__name__.split('_')
78 global candidate_command_terms
80 for term in candidate_command_terms:
82 index += 1 if term_list[index] == term else 0
83 except IndexError: # Whole term list matched!
85 if allow_subclass_signatures:
86 if index == len(candidate_command_terms) and len(term_list) > index:
87 try: # is subterm already in _commands?
88 _commands.get_command('_'.join(term_list[:index + 1]))
89 except KeyError: # No, so it must be placed there
93 return True if index == len(term_list) else False
97 """Class decorator that registers a class as a CLI command"""
100 """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
102 if not _allow_class_in_cmd_tree(cls):
105 cls.description, sep, cls.long_description\
106 = cls.__doc__.partition('\n')
108 # Generate a syntax string based on main's arguments
109 spec = getargspec(cls.main.im_func)
111 n = len(args) - len(spec.defaults or ())
112 required = ' '.join('<%s>' % x\
113 .replace('____', '[:')\
114 .replace('___', ':')\
115 .replace('__', ']').\
116 replace('_', ' ') for x in args[:n])
117 optional = ' '.join('[%s]' % x\
118 .replace('____', '[:')\
119 .replace('___', ':')\
120 .replace('__', ']').\
121 replace('_', ' ') for x in args[n:])
122 cls.syntax = ' '.join(x for x in [required, optional] if x)
124 cls.syntax += ' <%s ...>' % spec.varargs
126 # store each term, one by one, first
127 _commands.add_command(cls.__name__, cls.description, cls)
133 def _update_parser(parser, arguments):
134 for name, argument in arguments.items():
136 argument.update_parser(parser, name)
137 except ArgumentError:
141 def _init_parser(exe):
142 parser = ArgumentParser(add_help=False)
143 parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
144 _update_parser(parser, _arguments)
148 def _print_error_message(cli_err):
149 errmsg = '%s' % cli_err
150 if cli_err.importance == 1:
151 errmsg = magenta(errmsg)
152 elif cli_err.importance == 2:
153 errmsg = yellow(errmsg)
154 elif cli_err.importance > 2:
157 print_list(cli_err.details)
160 def get_command_group(unparsed):
161 groups = _arguments['config'].get_groups()
162 for grp_candidate in unparsed:
163 if grp_candidate in groups:
164 unparsed.remove(grp_candidate)
169 def load_command(group, unparsed, reload_package=False):
170 global candidate_command_terms
171 candidate_command_terms = [group] + unparsed
172 load_group_package(group, reload_package)
174 #From all possible parsed commands, chose the first match in user string
175 final_cmd = _commands.get_command(group)
176 for term in unparsed:
177 cmd = final_cmd.get_subcmd(term)
180 unparsed.remove(cmd.name)
185 """Load only group names and descriptions"""
186 global allow_no_commands
187 allow_no_commands = True # load only descriptions
188 for grp in _arguments['config'].get_groups():
189 load_group_package(grp)
190 allow_no_commands = False
193 def load_group_package(group, reload_package=False):
194 spec_pkg = _arguments['config'].value.get(group, 'cli')
197 for location in cmd_spec_locations:
198 location += spec_pkg if location == '' else ('.' + spec_pkg)
200 package = __import__(location, fromlist=['API_DESCRIPTION'])
205 for grp, descr in package.API_DESCRIPTION.items():
206 _commands.add_command(grp, descr)
208 raise CLICmdSpecError(details='Cmd Spec Package %s load failed' % spec_pkg)
211 def print_commands(prefix=None, full_depth=False):
212 cmd_list = _commands.get_groups() if prefix is None\
213 else _commands.get_subcommands(prefix)
215 for subcmd in cmd_list:
216 if subcmd.sublen() > 0:
217 sublen_str = '( %s more terms ... )' % subcmd.sublen()
218 cmds[subcmd.name] = [subcmd.help, sublen_str]\
219 if subcmd.has_description else sublen_str
221 cmds[subcmd.name] = subcmd.help
224 print_dict(cmds, ident=12)
226 _commands.pretty_print()
229 def setup_logging(silent=False, debug=False, verbose=False, include=False):
230 """handle logging for clients package"""
232 def add_handler(name, level, prefix=''):
233 h = logging.StreamHandler()
234 fmt = logging.Formatter(prefix + '%(message)s')
236 logger = logging.getLogger(name)
238 logger.setLevel(level)
241 add_handler('', logging.CRITICAL)
243 add_handler('requests', logging.INFO, prefix='* ')
244 add_handler('clients.send', logging.DEBUG, prefix='> ')
245 add_handler('clients.recv', logging.DEBUG, prefix='< ')
247 add_handler('requests', logging.INFO, prefix='* ')
248 add_handler('clients.send', logging.INFO, prefix='> ')
249 add_handler('clients.recv', logging.INFO, prefix='< ')
251 add_handler('clients.recv', logging.INFO)
253 add_handler('', logging.WARNING)
256 def _exec_cmd(instance, cmd_args, help_method):
258 return instance.main(*cmd_args)
259 except TypeError as err:
260 if err.args and err.args[0].startswith('main()'):
261 print(magenta('Syntax error'))
262 if instance.get_argument('verbose'):
267 except CLIError as err:
268 if instance.get_argument('debug'):
270 _print_error_message(err)
279 exe = basename(argv[0])
280 parser = _init_parser(exe)
281 parsed, unparsed = parse_known_args(parser, _arguments)
282 _colors = _arguments['config'].get('global', 'colors')
285 _history = History(_arguments['config'].get('history', 'file'))
286 _history.add(' '.join([exe] + argv[1:]))
287 _debug = _arguments['debug'].value
288 _help = _arguments['help'].value
289 _verbose = _arguments['verbose'].value
290 if _arguments['version'].value:
293 group = get_command_group(unparsed)
297 print_commands(full_depth=_debug)
300 cmd = load_command(group, unparsed)
301 # Find the most specific subcommand
302 for term in list(unparsed):
305 if cmd.contains(term):
306 cmd = cmd.get_subcmd(term)
307 unparsed.remove(term)
309 if _help or not cmd.is_command:
310 if cmd.has_description:
311 parser.description = cmd.help
314 parser.description =\
315 _commands.get_closest_ancestor_command(cmd.path).help
317 parser.description = ' '
318 parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
320 cli = cmd.get_class()
321 parser.prog += cli.syntax
322 _update_parser(parser, cli().arguments)
324 parser.prog += '[...]'
327 # load one more level just to see what is missing
328 global allow_subclass_signatures
329 allow_subclass_signatures = True
330 load_command(group, cmd.path.split('_')[1:], reload_package=True)
332 print_commands(cmd.path, full_depth=_debug)
335 setup_logging(silent=_arguments['silent'].value,
338 include=_arguments['include'].value)
339 cli = cmd.get_class()
340 executable = cli(_arguments)
341 _update_parser(parser, executable.arguments)
342 parser.prog = '%s %s %s'\
343 % (exe, cmd.path.replace('_', ' '), cli.syntax)
344 parsed, new_unparsed = parse_known_args(parser, _arguments)
345 unparsed = [term for term in unparsed if term in new_unparsed]
346 ret = _exec_cmd(executable, unparsed, parser.print_help)
348 except Exception as err:
350 from traceback import print_stack
353 err = err if isinstance(err, CLIError)\
354 else CLIError('Unexpected Error (%s): %s' % (type(err), err))
355 _print_error_message(err)
358 from command_shell import _fix_arguments, Shell
363 shell.set_prompt(basename(argv[0]))
364 from kamaki import __version__ as version
366 shell.do_EOF = shell.do_exit
372 shell = _start_shell()
373 _config = _arguments['config']
375 for grp in _config.get_groups():
376 global allow_all_commands
377 allow_all_commands = True
378 load_group_package(grp)
379 setup_logging(silent=_arguments['silent'].value,
380 debug=_arguments['debug'].value,
381 verbose=_arguments['verbose'].value,
382 include=_arguments['include'].value)
383 shell.cmd_tree = _commands