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, CLICmdSpecError
57 from .config import Config #TO BE REMOVED
58 from .utils import bold, magenta, red, yellow, print_list, print_dict
59 from .command_tree import CommandTree
60 from argument import _arguments, parse_known_args
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):
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 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')
191 for location in cmd_spec_locations:
192 location += spec_pkg if location == '' else ('.'+spec_pkg)
194 package = __import__(location, fromlist=['API_DESCRIPTION'])
199 for grp, descr in package.API_DESCRIPTION.items():
200 _commands.add_command(grp, descr)
202 raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
204 def print_commands(prefix=None, full_depth=False):
205 cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
207 for subcmd in cmd_list:
208 if subcmd.sublen() > 0:
209 sublen_str = '( %s more terms ... )'%subcmd.sublen()
210 cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
212 cmds[subcmd.name] = subcmd.help
215 print_dict(cmds, ident=12)
217 _commands.pretty_print()
224 exe = basename(argv[0])
225 parser = _init_parser(exe)
226 parsed, unparsed = parse_known_args(parser)
227 _debug = _arguments['debug'].value
228 _help = _arguments['help'].value
229 _verbose = _arguments['verbose'].value
230 if _arguments['version'].value:
233 group = get_command_group(unparsed)
237 print_commands(full_depth=_verbose)
240 cmd = load_command(group, unparsed)
241 if _help or not cmd.is_command:
242 if cmd.has_description:
243 parser.description = cmd.help
246 parser.description = _commands.get_closest_ancestor_command(cmd.path).help
248 parser.description = ' '
249 parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
251 cli = cmd.get_class()
252 parser.prog += cli.syntax
253 _update_parser(parser, cli().arguments)
255 parser.prog += '[...]'
258 #Shuuuut, we now have to load one more level just to see what is missing
259 global allow_subclass_signatures
260 allow_subclass_signatures = True
261 load_command(group, cmd.path.split('_')[1:], reload_package=True)
263 print_commands(cmd.path, full_depth=_verbose)
266 cli = cmd.get_class()
267 executable = cli(_arguments)
268 _update_parser(parser, executable.arguments)
269 parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
270 parse_known_args(parser)
272 ret = executable.main(*unparsed)
274 except TypeError as e:
275 if e.args and e.args[0].startswith('main()'):
280 except CLIError as err:
283 _print_error_message(err)