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 base64 import b64encode
47 from os.path import abspath, basename, exists
48 from sys import exit, stdout, stderr, argv
51 from collections import OrderedDict
53 from ordereddict import OrderedDict
55 #from kamaki import clients
56 from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError
57 from .config import Config #TO BE REMOVED
58 from .utils import bold, magenta, red, yellow, CommandTree, print_list, print_dict
59 from argument import _arguments, parse_known_args
61 cmd_spec_locations = [
62 'kamaki.cli.commands',
67 _commands = CommandTree(description='A command line tool for poking clouds')
69 #If empty, all commands are loaded, if not empty, only commands in this list
70 #e.g. [store, lele, list, lolo] is good to load store_list but not list_store
71 #First arg should always refer to a group
72 candidate_command_terms = []
73 do_no_load_commands = False
74 put_subclass_signatures_in_commands = False
76 def _put_subclass_signatures_in_commands(cls):
77 global candidate_command_terms
79 part_name = '_'.join(candidate_command_terms)
81 empty, same, rest = cls.__name__.partition(part_name)
87 _commands.add_path(cls.__name__, (cls.__doc__.partition('\n'))[0])
89 rest_terms = rest[1:].split('_')
90 new_name = part_name+'_'+rest_terms[0]
91 desc = cls.__doc__.partition('\n')[0] if new_name == cls.__name__ else ''
92 _commands.add_path(new_name, desc)
96 def _put_class_path_in_commands(cls):
97 #Maybe I should apologise for the globals, but they are used in a smart way, so...
98 global candidate_command_terms
99 term_list = cls.__name__.split('_')
102 if len(candidate_command_terms) > 0:
103 #This is the case of a one-command execution: discard if not requested
104 if term_list[0] != candidate_command_terms[0]:
107 for term in term_list:
108 #check if the term is requested by user
109 if term not in candidate_command_terms[i:]:
111 i = 1+candidate_command_terms.index(term)
112 #now, put the term in the tree
113 if term not in tmp_tree.get_command_names():
114 tmp_tree.add_command(term)
115 tmp_tree = tmp_tree.get_command(term)
117 #Just insert everything in the tree
118 for term in term_list:
119 if term not in tmp_tree.get_command_names():
120 tmp_tree.add_command(term)
121 tmp_tree = tmp_tree.get_command()
125 """Class decorator that registers a class as a CLI command"""
128 """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
129 global do_no_load_commands
130 if do_no_load_commands:
133 global put_subclass_signatures_in_commands
134 if put_subclass_signatures_in_commands:
135 _put_subclass_signatures_in_commands(cls)
138 if not _put_class_path_in_commands(cls):
141 cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
143 # Generate a syntax string based on main's arguments
144 spec = getargspec(cls.main.im_func)
146 n = len(args) - len(spec.defaults or ())
147 required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
148 replace('_', ' ') for x in args[:n])
149 optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
150 replace('_', ' ') for x in args[n:])
151 cls.syntax = ' '.join(x for x in [required, optional] if x)
153 cls.syntax += ' <%s ...>' % spec.varargs
155 #store each term, one by one, first
156 _commands.add_command(cls.__name__, cls.description, cls)
160 def _update_parser(parser, arguments):
162 for name, argument in arguments.items():
163 argument.update_parser(parser, name)
164 except ArgumentError:
167 def _init_parser(exe):
168 parser = ArgumentParser(add_help=False)
169 parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
170 _update_parser(parser, _arguments)
173 def _print_error_message(cli_err):
174 errmsg = '%s'%unicode(cli_err) +' (%s)'%cli_err.status if cli_err.status else ' '
175 if cli_err.importance == 1:
176 errmsg = magenta(errmsg)
177 elif cli_err.importance == 2:
178 errmsg = yellow(errmsg)
179 elif cli_err.importance > 2:
182 if cli_err.details is not None and len(cli_err.details) > 0:
183 print(': %s'%cli_err.details)
187 def _expand_cmd(cmd_prefix, unparsed):
188 if len(unparsed) == 0:
190 prefix = (cmd_prefix+'_') if len(cmd_prefix) > 0 else ''
191 for term in _commands.list(cmd_prefix):
193 unparsed.remove(term)
199 def _retrieve_cmd(unparsed):
201 cur_cmd = _expand_cmd('', unparsed)
202 while cur_cmd is not None:
204 cur_cmd = _expand_cmd(cur_cmd, unparsed)
206 print(bold('Command groups:'))
207 print_list(_commands.get_groups(), ident=14)
211 return _commands.get_class(cmd_str)
212 except CLICmdIncompleteError:
213 print(bold('%s:'%cmd_str))
214 print_list(_commands.list(cmd_str))
217 def get_command_group(unparsed):
218 groups = _arguments['config'].get_groups()
219 for grp_candidate in unparsed:
220 if grp_candidate in groups:
221 unparsed.remove(grp_candidate)
225 def _order_in_list(list1, list2):
227 for i,term in enumerate(list1):
228 order += len(list2)*i*list2.index(term)
231 def load_command(group, unparsed, reload_package=False):
232 global candidate_command_terms
233 candidate_command_terms = [group] + unparsed
234 pkg = load_group_package(group, reload_package)
236 #From all possible parsed commands, chose one
239 next_names = _commands.get_command_names(final_cmd)
240 while len(next_names) > 0:
241 if len(next_names) == 1:
242 final_cmd+='_'+next_names[0]
243 else:#choose the first in user string
245 pos = unparsed.index(next_names[0])
249 for i, name in enumerate(next_names[1:]):
250 tmp_index = unparsed.index(name)
254 final_cmd+='_'+next_names[choice]
255 next_names = _commands.get_command_names(final_cmd)
259 """Load only group names and descriptions"""
260 global do_no_load_commands
261 do_no_load_commands = True#load only descriptions
262 for grp in _arguments['config'].get_groups():
263 load_group_package(grp)
264 do_no_load_commands = False
266 def load_group_package(group, reload_package=False):
267 spec_pkg = _arguments['config'].value.get(group, 'cli')
268 for location in cmd_spec_locations:
269 location += spec_pkg if location == '' else ('.'+spec_pkg)
271 package = __import__(location, fromlist=['API_DESCRIPTION'])
276 for grp, descr in package.API_DESCRIPTION.items():
277 _commands.add_command(grp, descr)
279 raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
281 def print_commands(prefix=[], full_tree=False):
282 cmd = _commands.get_command(prefix)
283 grps = {' . ':cmd.description} if cmd.is_command else {}
284 for grp in cmd.get_command_names():
285 grps[grp] = cmd.get_description(grp)
287 print_dict(grps, ident=12)
289 _commands.print_tree(level=-1)
295 exe = basename(argv[0])
296 parser = _init_parser(exe)
297 parsed, unparsed = parse_known_args(parser)
298 _debug = _arguments['debug'].value
299 _help = _arguments['help'].value
300 if _arguments['version'].value:
303 group = get_command_group(unparsed)
307 print_commands(full_tree=_arguments['verbose'].value)
311 command_path = load_command(group, unparsed)
312 cli = _commands.get_class(command_path)
313 if cli is None or _help: #Not a complete command or help
314 parser.description = _commands.closest_description(command_path)
315 parser.prog = '%s %s '%(exe, command_path.replace('_', ' '))
317 parser.prog += '<...>'
319 parser.prog += cli.syntax
320 _update_parser(parser, cli().arguments)
323 #Shuuuut, we now have to load one more level just to see what is missing
324 global put_subclass_signatures_in_commands
325 put_subclass_signatures_in_commands = True
326 load_command(group, command_path.split('_')[1:], reload_package=True)
328 print_commands(command_path, full_tree=_arguments['verbose'].value)
332 cmd = cli(_arguments)
333 _update_parser(parser, cmd.arguments)
334 parser.prog = '%s %s %s'%(exe, command_path.replace('_', ' '), cli.syntax)
335 parsed, unparsed = parse_known_args(parser)
336 for term in command_path.split('_'):
337 unparsed.remove(term)
339 ret = cmd.main(*unparsed)
341 except TypeError as e:
342 if e.args and e.args[0].startswith('main()'):
347 except CLIError as err:
350 _print_error_message(err)