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 if _help or not cmd.is_command:
302 if cmd.has_description:
303 parser.description = cmd.help
307 = _commands.get_closest_ancestor_command(cmd.path).help
309 parser.description = ' '
310 parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
312 cli = cmd.get_class()
313 parser.prog += cli.syntax
314 _update_parser(parser, cli().arguments)
316 parser.prog += '[...]'
319 # load one more level just to see what is missing
320 global allow_subclass_signatures
321 allow_subclass_signatures = True
322 load_command(group, cmd.path.split('_')[1:], reload_package=True)
324 print_commands(cmd.path, full_depth=_debug)
327 setup_logging(silent=_arguments['silent'].value,
330 include=_arguments['include'].value)
331 cli = cmd.get_class()
332 executable = cli(_arguments)
333 _update_parser(parser, executable.arguments)
334 parser.prog = '%s %s %s'\
335 % (exe, cmd.path.replace('_', ' '), cli.syntax)
336 parsed, new_unparsed = parse_known_args(parser, _arguments)
337 unparsed = [term for term in unparsed if term in new_unparsed]
338 ret = _exec_cmd(executable, unparsed, parser.print_help)
340 except CLIError as err:
343 _print_error_message(err)
346 from command_shell import _fix_arguments, Shell
351 shell.set_prompt(basename(argv[0]))
352 from kamaki import __version__ as version
354 shell.do_EOF = shell.do_exit
360 shell = _start_shell()
361 _config = _arguments['config']
363 for grp in _config.get_groups():
364 global allow_all_commands
365 allow_all_commands = True
366 load_group_package(grp)
367 setup_logging(silent=_arguments['silent'].value,
368 debug=_arguments['debug'].value,
369 verbose=_arguments['verbose'].value,
370 include=_arguments['include'].value)
371 shell.cmd_tree = _commands