3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
13 # 2. Redistributions in binary form must reproduce the above
14 # copyright notice, this list of conditions and the following
15 # disclaimer in the documentation and/or other materials
16 # provided with the distribution.
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
36 from __future__ import print_function
39 #Monkey-patch everything for gevent early on
40 gevent.monkey.patch_all()
44 from inspect import getargspec
45 from argparse import ArgumentParser, ArgumentError
46 from os.path import basename
47 from sys import exit, stdout, stderr, argv
50 from collections import OrderedDict
52 from ordereddict import OrderedDict
54 #from kamaki import clients
55 from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
56 from .config import Config #TO BE REMOVED
57 from .utils import bold, magenta, red, yellow, print_list, print_dict
58 from .command_tree import CommandTree
59 from argument import _arguments, parse_known_args
60 from .history import History
62 cmd_spec_locations = [
63 'kamaki.cli.commands',
68 _commands = CommandTree(name='kamaki', description='A command line tool for poking clouds')
70 #If empty, all commands are loaded, if not empty, only commands in this list
71 #e.g. [store, lele, list, lolo] is good to load store_list but not list_store
72 #First arg should always refer to a group
73 candidate_command_terms = []
74 allow_no_commands = False
75 allow_all_commands = False
76 allow_subclass_signatures = False
78 def _allow_class_in_cmd_tree(cls):
79 global allow_all_commands
80 if allow_all_commands:
82 global allow_no_commands
86 term_list = cls.__name__.split('_')
87 global candidate_command_terms
89 for term in candidate_command_terms:
91 index += 1 if term_list[index] == term else 0
92 except IndexError: #Whole term list matched!
94 if allow_subclass_signatures:
95 if index == len(candidate_command_terms) and len(term_list) > index:
96 try: #is subterm already in _commands?
97 _commands.get_command('_'.join(term_list[:index+1]))
98 except KeyError: #No, so it must be placed there
102 return True if index == len(term_list) else False
105 """Class decorator that registers a class as a CLI command"""
108 """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
110 if not _allow_class_in_cmd_tree(cls):
113 cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
115 # Generate a syntax string based on main's arguments
116 spec = getargspec(cls.main.im_func)
118 n = len(args) - len(spec.defaults or ())
119 required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
120 replace('_', ' ') for x in args[:n])
121 optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
122 replace('_', ' ') for x in args[n:])
123 cls.syntax = ' '.join(x for x in [required, optional] if x)
125 cls.syntax += ' <%s ...>' % spec.varargs
127 #store each term, one by one, first
128 _commands.add_command(cls.__name__, cls.description, cls)
132 def _update_parser(parser, arguments):
133 for name, argument in arguments.items():
135 argument.update_parser(parser, name)
136 except ArgumentError:
139 def _init_parser(exe):
140 parser = ArgumentParser(add_help=False)
141 parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
142 _update_parser(parser, _arguments)
145 def _print_error_message(cli_err, verbose=False):
146 errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
147 if cli_err.importance == 1:
148 errmsg = magenta(errmsg)
149 elif cli_err.importance == 2:
150 errmsg = yellow(errmsg)
151 elif cli_err.importance > 2:
154 if verbose and cli_err.details is not None and len(cli_err.details) > 0:
155 print(': %s'%cli_err.details)
159 def get_command_group(unparsed):
160 groups = _arguments['config'].get_groups()
161 for grp_candidate in unparsed:
162 if grp_candidate in groups:
163 unparsed.remove(grp_candidate)
167 def load_command(group, unparsed, reload_package=False):
168 global candidate_command_terms
169 candidate_command_terms = [group] + unparsed
170 pkg = load_group_package(group, reload_package)
172 #From all possible parsed commands, chose the first match in user string
173 final_cmd = _commands.get_command(group)
174 for term in unparsed:
175 cmd = final_cmd.get_subcmd(term)
178 unparsed.remove(cmd.name)
182 """Load only group names and descriptions"""
183 global allow_no_commands
184 allow_no_commands = True#load only descriptions
185 for grp in _arguments['config'].get_groups():
186 load_group_package(grp)
187 allow_no_commands = False
189 def load_group_package(group, reload_package=False):
190 spec_pkg = _arguments['config'].value.get(group, 'cli')
193 for location in cmd_spec_locations:
194 location += spec_pkg if location == '' else ('.'+spec_pkg)
196 package = __import__(location, fromlist=['API_DESCRIPTION'])
201 for grp, descr in package.API_DESCRIPTION.items():
202 _commands.add_command(grp, descr)
204 raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
206 def print_commands(prefix=None, full_depth=False):
207 cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
209 for subcmd in cmd_list:
210 if subcmd.sublen() > 0:
211 sublen_str = '( %s more terms ... )'%subcmd.sublen()
212 cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
214 cmds[subcmd.name] = subcmd.help
217 print_dict(cmds, ident=12)
219 _commands.pretty_print()
221 def setup_logging(silent=False, debug=False, verbose=False, include=False):
222 """handle logging for clients package"""
224 def add_handler(name, level, prefix=''):
225 h = logging.StreamHandler()
226 fmt = logging.Formatter(prefix + '%(message)s')
228 logger = logging.getLogger(name)
230 logger.setLevel(level)
233 add_handler('', logging.CRITICAL)
235 add_handler('requests', logging.INFO, prefix='* ')
236 add_handler('clients.send', logging.DEBUG, prefix='> ')
237 add_handler('clients.recv', logging.DEBUG, prefix='< ')
239 add_handler('requests', logging.INFO, prefix='* ')
240 add_handler('clients.send', logging.INFO, prefix='> ')
241 add_handler('clients.recv', logging.INFO, prefix='< ')
243 add_handler('clients.recv', logging.INFO)
245 add_handler('', logging.WARNING)
252 exe = basename(argv[0])
253 parser = _init_parser(exe)
254 parsed, unparsed = parse_known_args(parser)
255 _history = History(_arguments['config'].get('history', 'file'))
256 _history.add(' '.join([exe]+argv[1:]))
257 _debug = _arguments['debug'].value
258 _help = _arguments['help'].value
259 _verbose = _arguments['verbose'].value
260 if _arguments['version'].value:
263 group = get_command_group(unparsed)
267 print_commands(full_depth=_debug)
270 cmd = load_command(group, unparsed)
271 if _help or not cmd.is_command:
272 if cmd.has_description:
273 parser.description = cmd.help
276 parser.description = _commands.get_closest_ancestor_command(cmd.path).help
278 parser.description = ' '
279 parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
281 cli = cmd.get_class()
282 parser.prog += cli.syntax
283 _update_parser(parser, cli().arguments)
285 parser.prog += '[...]'
288 #Shuuuut, we now have to load one more level just to see what is missing
289 global allow_subclass_signatures
290 allow_subclass_signatures = True
291 load_command(group, cmd.path.split('_')[1:], reload_package=True)
293 print_commands(cmd.path, full_depth=_debug)
296 setup_logging(silent=_arguments['silent'].value, debug=_debug, verbose=_verbose,
297 include=_arguments['include'].value)
298 cli = cmd.get_class()
299 executable = cli(_arguments)
300 _update_parser(parser, executable.arguments)
301 parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
302 parsed, new_unparsed = parse_known_args(parser)
303 unparsed = [term for term in unparsed if term in new_unparsed]
305 ret = executable.main(*unparsed)
307 except TypeError as e:
308 if e.args and e.args[0].startswith('main()'):
313 except CLIError as err:
316 _print_error_message(err, verbose=_verbose)