Revision fd5db045 kamaki/cli/__init__.py

b/kamaki/cli/__init__.py
1 1
#!/usr/bin/env python
2

  
3 2
# Copyright 2011-2012 GRNET S.A. All rights reserved.
4 3
#
5 4
# Redistribution and use in source and binary forms, with or
......
7 6
# conditions are met:
8 7
#
9 8
#   1. Redistributions of source code must retain the above
10
#	  copyright notice, this list of conditions and the following
11
#	  disclaimer.
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
12 11
#
13 12
#   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.
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 16
#
18 17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
......
47 46
from sys import exit, stdout, stderr, argv
48 47

  
49 48
try:
50
	from collections import OrderedDict
49
    from collections import OrderedDict
51 50
except ImportError:
52
	from ordereddict import OrderedDict
51
    from ordereddict import OrderedDict
53 52

  
54 53
#from kamaki import clients
55
from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
56
from .utils import bold, magenta, red, yellow, print_list, print_dict, remove_colors
57
from .command_tree import CommandTree
58
from .argument import _arguments, parse_known_args
59
from .history import History
54
from kamaki.cli.errors import CLIError, CLISyntaxError,\
55
    CLICmdIncompleteError, CLICmdSpecError
56
from kamaki.cli.utils import bold, magenta, red, yellow,\
57
    print_list, print_dict, remove_colors
58
from kamaki.cli.command_tree import CommandTree
59
from kamaki.cli.argument import _arguments, parse_known_args
60
from kamaki.cli.history import History
60 61

  
61 62
cmd_spec_locations = [
62
	'kamaki.cli.commands',
63
	'kamaki.commands',
64
	'kamaki.cli',
65
	'kamaki',
66
	'']
67
_commands = CommandTree(name='kamaki', description='A command line tool for poking clouds')
68

  
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
63
    'kamaki.cli.commands',
64
    'kamaki.commands',
65
    'kamaki.cli',
66
    'kamaki',
67
    '']
68
_commands = CommandTree(name='kamaki',
69
    description='A command line tool for poking clouds')
70

  
71
# If empty, all commands are loaded, if not empty, only commands in this list
72
# e.g. [store, lele, list, lolo] is good to load store_list but not list_store
73
# First arg should always refer to a group
72 74
candidate_command_terms = []
73 75
allow_no_commands = False
74 76
allow_all_commands = False
75 77
allow_subclass_signatures = False
76 78

  
77
def _allow_class_in_cmd_tree(cls):
78
	global allow_all_commands
79
	if allow_all_commands:
80
		return True
81
	global allow_no_commands 
82
	if allow_no_commands:
83
		return False
84

  
85
	term_list = cls.__name__.split('_')
86
	global candidate_command_terms
87
	index = 0
88
	for term in candidate_command_terms:
89
		try:
90
			index += 1 if term_list[index] == term else 0
91
		except IndexError: #Whole term list matched!
92
			return True
93
	if allow_subclass_signatures:
94
		if index == len(candidate_command_terms) and len(term_list) > index:
95
			try: #is subterm already in _commands?
96
				_commands.get_command('_'.join(term_list[:index+1]))
97
			except KeyError: #No, so it must be placed there
98
				return True
99
		return False
100

  
101
	return True if index == len(term_list) else False
102

  
103
def command():
104
	"""Class decorator that registers a class as a CLI command"""
105

  
106
	def decorator(cls):
107
		"""Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
108

  
109
		if not _allow_class_in_cmd_tree(cls):
110
			return cls
111 79

  
112
		cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
80
def _allow_class_in_cmd_tree(cls):
81
    global allow_all_commands
82
    if allow_all_commands:
83
        return True
84
    global allow_no_commands
85
    if allow_no_commands:
86
        return False
87

  
88
    term_list = cls.__name__.split('_')
89
    global candidate_command_terms
90
    index = 0
91
    for term in candidate_command_terms:
92
        try:
93
            index += 1 if term_list[index] == term else 0
94
        except IndexError:  # Whole term list matched!
95
            return True
96
    if allow_subclass_signatures:
97
        if index == len(candidate_command_terms) and len(term_list) > index:
98
            try:  # is subterm already in _commands?
99
                _commands.get_command('_'.join(term_list[:index + 1]))
100
            except KeyError:  # No, so it must be placed there
101
                return True
102
        return False
103

  
104
    return True if index == len(term_list) else False
113 105

  
114
		# Generate a syntax string based on main's arguments
115
		spec = getargspec(cls.main.im_func)
116
		args = spec.args[1:]
117
		n = len(args) - len(spec.defaults or ())
118
		required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
119
			replace('_', ' ') for x in args[:n])
120
		optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
121
			replace('_', ' ') for x in args[n:])
122
		cls.syntax = ' '.join(x for x in [required, optional] if x)
123
		if spec.varargs:
124
			cls.syntax += ' <%s ...>' % spec.varargs
125 106

  
126
		#store each term, one by one, first
127
		_commands.add_command(cls.__name__, cls.description, cls)
107
def command():
108
    """Class decorator that registers a class as a CLI command"""
109

  
110
    def decorator(cls):
111
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
112

  
113
        if not _allow_class_in_cmd_tree(cls):
114
            return cls
115

  
116
        cls.description, sep, cls.long_description\
117
            = cls.__doc__.partition('\n')
118

  
119
        # Generate a syntax string based on main's arguments
120
        spec = getargspec(cls.main.im_func)
121
        args = spec.args[1:]
122
        n = len(args) - len(spec.defaults or ())
123
        required = ' '.join('<%s>' % x\
124
            .replace('____', '[:')\
125
            .replace('___', ':')\
126
            .replace('__', ']').\
127
            replace('_', ' ') for x in args[:n])
128
        optional = ' '.join('[%s]' % x\
129
            .replace('____', '[:')\
130
            .replace('___', ':')\
131
            .replace('__', ']').\
132
            replace('_', ' ') for x in args[n:])
133
        cls.syntax = ' '.join(x for x in [required, optional] if x)
134
        if spec.varargs:
135
            cls.syntax += ' <%s ...>' % spec.varargs
136

  
137
        # store each term, one by one, first
138
        _commands.add_command(cls.__name__, cls.description, cls)
139

  
140
        return cls
141
    return decorator
128 142

  
129
		return cls
130
	return decorator
131 143

  
132 144
def _update_parser(parser, arguments):
133
	for name, argument in arguments.items():
134
		try:
135
			argument.update_parser(parser, name)
136
		except ArgumentError:
137
			pass
145
    for name, argument in arguments.items():
146
        try:
147
            argument.update_parser(parser, name)
148
        except ArgumentError:
149
            pass
150

  
138 151

  
139 152
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)
143
	return parser
153
    parser = ArgumentParser(add_help=False)
154
    parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
155
    _update_parser(parser, _arguments)
156
    return parser
157

  
144 158

  
145 159
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:
152
		errmsg = red(errmsg)
153
	stdout.write(errmsg)
154
	if cli_err.details is not None and len(cli_err.details) > 0:
155
		print(': %s'%cli_err.details)
156
	else:
157
		print()
160
    errmsg = '%s (%s)' % (cli_err, cli_err.status if cli_err.status else ' ')
161
    if cli_err.importance == 1:
162
        errmsg = magenta(errmsg)
163
    elif cli_err.importance == 2:
164
        errmsg = yellow(errmsg)
165
    elif cli_err.importance > 2:
166
        errmsg = red(errmsg)
167
    stdout.write(errmsg)
168
    if cli_err.details is not None and len(cli_err.details) > 0:
169
        print(': %s' % cli_err.details)
170
    else:
171
        print()
172

  
158 173

  
159 174
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)
164
			return grp_candidate
165
	return None
175
    groups = _arguments['config'].get_groups()
176
    for grp_candidate in unparsed:
177
        if grp_candidate in groups:
178
            unparsed.remove(grp_candidate)
179
            return grp_candidate
180
    return None
181

  
166 182

  
167 183
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)
171

  
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)
176
		if cmd is not None:
177
			final_cmd = cmd
178
			unparsed.remove(cmd.name)
179
	return final_cmd
184
    global candidate_command_terms
185
    candidate_command_terms = [group] + unparsed
186
    pkg = load_group_package(group, reload_package)
187

  
188
    #From all possible parsed commands, chose the first match in user string
189
    final_cmd = _commands.get_command(group)
190
    for term in unparsed:
191
        cmd = final_cmd.get_subcmd(term)
192
        if cmd is not None:
193
            final_cmd = cmd
194
            unparsed.remove(cmd.name)
195
    return final_cmd
196

  
180 197

  
181 198
def shallow_load():
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
199
    """Load only group names and descriptions"""
200
    global allow_no_commands
201
    allow_no_commands = True  # load only descriptions
202
    for grp in _arguments['config'].get_groups():
203
        load_group_package(grp)
204
    allow_no_commands = False
205

  
188 206

  
189 207
def load_group_package(group, reload_package=False):
190
	spec_pkg = _arguments['config'].value.get(group, 'cli')
191
	if spec_pkg is None:
192
		return None
193
	for location in cmd_spec_locations:
194
		location += spec_pkg if location == '' else ('.'+spec_pkg)
195
		try:
196
			package = __import__(location, fromlist=['API_DESCRIPTION'])
197
		except ImportError:
198
			continue
199
		if reload_package:
200
			reload(package)
201
		for grp, descr in package.API_DESCRIPTION.items():
202
			_commands.add_command(grp, descr)
203
		return package
204
	raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
208
    spec_pkg = _arguments['config'].value.get(group, 'cli')
209
    if spec_pkg is None:
210
        return None
211
    for location in cmd_spec_locations:
212
        location += spec_pkg if location == '' else ('.' + spec_pkg)
213
        try:
214
            package = __import__(location, fromlist=['API_DESCRIPTION'])
215
        except ImportError:
216
            continue
217
        if reload_package:
218
            reload(package)
219
        for grp, descr in package.API_DESCRIPTION.items():
220
            _commands.add_command(grp, descr)
221
        return package
222
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed' % spec_pkg)
223

  
205 224

  
206 225
def print_commands(prefix=None, full_depth=False):
207
	cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
208
	cmds = {}
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
213
		else:
214
			cmds[subcmd.name] = subcmd.help
215
	if len(cmds) > 0:
216
		print('\nOptions:')
217
		print_dict(cmds, ident=12)
218
	if full_depth:
219
		_commands.pretty_print()
226
    cmd_list = _commands.get_groups() if prefix is None\
227
        else _commands.get_subcommands(prefix)
228
    cmds = {}
229
    for subcmd in cmd_list:
230
        if subcmd.sublen() > 0:
231
            sublen_str = '( %s more terms ... )' % subcmd.sublen()
232
            cmds[subcmd.name] = [subcmd.help, sublen_str]\
233
                if subcmd.has_description else subcmd_str
234
        else:
235
            cmds[subcmd.name] = subcmd.help
236
    if len(cmds) > 0:
237
        print('\nOptions:')
238
        print_dict(cmds, ident=12)
239
    if full_depth:
240
        _commands.pretty_print()
241

  
220 242

  
221 243
def setup_logging(silent=False, debug=False, verbose=False, include=False):
222
	"""handle logging for clients package"""
223

  
224
	def add_handler(name, level, prefix=''):
225
		h = logging.StreamHandler()
226
		fmt = logging.Formatter(prefix + '%(message)s')
227
		h.setFormatter(fmt)
228
		logger = logging.getLogger(name)
229
		logger.addHandler(h)
230
		logger.setLevel(level)
231

  
232
	if silent:
233
		add_handler('', logging.CRITICAL)
234
	elif debug:
235
		add_handler('requests', logging.INFO, prefix='* ')
236
		add_handler('clients.send', logging.DEBUG, prefix='> ')
237
		add_handler('clients.recv', logging.DEBUG, prefix='< ')
238
	elif verbose:
239
		add_handler('requests', logging.INFO, prefix='* ')
240
		add_handler('clients.send', logging.INFO, prefix='> ')
241
		add_handler('clients.recv', logging.INFO, prefix='< ')
242
	elif include:
243
		add_handler('clients.recv', logging.INFO)
244
	else:
245
		add_handler('', logging.WARNING)
244
    """handle logging for clients package"""
245

  
246
    def add_handler(name, level, prefix=''):
247
        h = logging.StreamHandler()
248
        fmt = logging.Formatter(prefix + '%(message)s')
249
        h.setFormatter(fmt)
250
        logger = logging.getLogger(name)
251
        logger.addHandler(h)
252
        logger.setLevel(level)
253

  
254
    if silent:
255
        add_handler('', logging.CRITICAL)
256
    elif debug:
257
        add_handler('requests', logging.INFO, prefix='* ')
258
        add_handler('clients.send', logging.DEBUG, prefix='> ')
259
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
260
    elif verbose:
261
        add_handler('requests', logging.INFO, prefix='* ')
262
        add_handler('clients.send', logging.INFO, prefix='> ')
263
        add_handler('clients.recv', logging.INFO, prefix='< ')
264
    elif include:
265
        add_handler('clients.recv', logging.INFO)
266
    else:
267
        add_handler('', logging.WARNING)
268

  
246 269

  
247 270
def _exec_cmd(instance, cmd_args, help_method):
248
	try:
249
		return instance.main(*cmd_args)
250
	except TypeError as err:
251
		if err.args and err.args[0].startswith('main()'):
252
			print(magenta('Syntax error'))
253
			if instance.get_argument('verbose'):
254
				print(unicode(err))
255
			help_method()
256
		else:
257
			raise
258
	except CLIError as err:
259
		if instance.get_argument('debug'):
260
			raise
261
		_print_error_message(err)
262
	return 1
271
    try:
272
        return instance.main(*cmd_args)
273
    except TypeError as err:
274
        if err.args and err.args[0].startswith('main()'):
275
            print(magenta('Syntax error'))
276
            if instance.get_argument('verbose'):
277
                print(unicode(err))
278
            help_method()
279
        else:
280
            raise
281
    except CLIError as err:
282
        if instance.get_argument('debug'):
283
            raise
284
        _print_error_message(err)
285
    return 1
286

  
263 287

  
264 288
def one_command():
265
	_debug = False
266
	_help = False
267
	_verbose = False
268
	try:
269
		exe = basename(argv[0])
270
		parser = _init_parser(exe)
271
		parsed, unparsed = parse_known_args(parser, _arguments)
272
		_colors = _arguments['config'].get('global', 'colors')
273
		if _colors!='on':
274
			remove_colors()
275
		_history = History(_arguments['config'].get('history', 'file'))
276
		_history.add(' '.join([exe]+argv[1:]))
277
		_debug = _arguments['debug'].value
278
		_help = _arguments['help'].value
279
		_verbose = _arguments['verbose'].value
280
		if _arguments['version'].value:
281
			exit(0)
282

  
283
		group = get_command_group(unparsed)
284
		if group is None:
285
			parser.print_help()
286
			shallow_load()
287
			print_commands(full_depth=_debug)
288
			exit(0)
289

  
290
		cmd = load_command(group, unparsed)
291
		if _help or not cmd.is_command:
292
			if cmd.has_description:
293
				parser.description = cmd.help 
294
			else:
295
				try:
296
					parser.description = _commands.get_closest_ancestor_command(cmd.path).help
297
				except KeyError:
298
					parser.description = ' '
299
			parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
300
			if cmd.is_command:
301
				cli = cmd.get_class()
302
				parser.prog += cli.syntax
303
				_update_parser(parser, cli().arguments)
304
			else:
305
				parser.prog += '[...]'
306
			parser.print_help()
307

  
308
			#Shuuuut, we now have to load one more level just to see what is missing
309
			global allow_subclass_signatures 
310
			allow_subclass_signatures = True
311
			load_command(group, cmd.path.split('_')[1:], reload_package=True)
312

  
313
			print_commands(cmd.path, full_depth=_debug)
314
			exit(0)
315

  
316
		setup_logging(silent=_arguments['silent'].value, debug=_debug, verbose=_verbose,
317
			include=_arguments['include'].value)
318
		cli = cmd.get_class()
319
		executable = cli(_arguments)
320
		_update_parser(parser, executable.arguments)
321
		parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
322
		parsed, new_unparsed = parse_known_args(parser, _arguments)
323
		unparsed = [term for term in unparsed if term in new_unparsed]
324
		ret = _exec_cmd(executable, unparsed, parser.print_help)
325
		exit(ret)
326
	except CLIError as err:
327
		if _debug:
328
			raise
329
		_print_error_message(err)
330
		exit(1)
331

  
332
from command_shell import _fix_arguments ,Shell
289
    _debug = False
290
    _help = False
291
    _verbose = False
292
    try:
293
        exe = basename(argv[0])
294
        parser = _init_parser(exe)
295
        parsed, unparsed = parse_known_args(parser, _arguments)
296
        _colors = _arguments['config'].get('global', 'colors')
297
        if _colors != 'on':
298
            remove_colors()
299
        _history = History(_arguments['config'].get('history', 'file'))
300
        _history.add(' '.join([exe] + argv[1:]))
301
        _debug = _arguments['debug'].value
302
        _help = _arguments['help'].value
303
        _verbose = _arguments['verbose'].value
304
        if _arguments['version'].value:
305
            exit(0)
306

  
307
        group = get_command_group(unparsed)
308
        if group is None:
309
            parser.print_help()
310
            shallow_load()
311
            print_commands(full_depth=_debug)
312
            exit(0)
313

  
314
        cmd = load_command(group, unparsed)
315
        if _help or not cmd.is_command:
316
            if cmd.has_description:
317
                parser.description = cmd.help
318
            else:
319
                try:
320
                    parser.description\
321
                        = _commands.get_closest_ancestor_command(cmd.path).help
322
                except KeyError:
323
                    parser.description = ' '
324
            parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
325
            if cmd.is_command:
326
                cli = cmd.get_class()
327
                parser.prog += cli.syntax
328
                _update_parser(parser, cli().arguments)
329
            else:
330
                parser.prog += '[...]'
331
            parser.print_help()
332

  
333
            # load one more level just to see what is missing
334
            global allow_subclass_signatures
335
            allow_subclass_signatures = True
336
            load_command(group, cmd.path.split('_')[1:], reload_package=True)
337

  
338
            print_commands(cmd.path, full_depth=_debug)
339
            exit(0)
340

  
341
        setup_logging(silent=_arguments['silent'].value,
342
            debug=_debug,
343
            verbose=_verbose,
344
            include=_arguments['include'].value)
345
        cli = cmd.get_class()
346
        executable = cli(_arguments)
347
        _update_parser(parser, executable.arguments)
348
        parser.prog = '%s %s %s'\
349
            % (exe, cmd.path.replace('_', ' '), cli.syntax)
350
        parsed, new_unparsed = parse_known_args(parser, _arguments)
351
        unparsed = [term for term in unparsed if term in new_unparsed]
352
        ret = _exec_cmd(executable, unparsed, parser.print_help)
353
        exit(ret)
354
    except CLIError as err:
355
        if _debug:
356
            raise
357
        _print_error_message(err)
358
        exit(1)
359

  
360
from command_shell import _fix_arguments, Shell
361

  
333 362

  
334 363
def _start_shell():
335
	shell = Shell()
336
	shell.set_prompt(basename(argv[0]))
337
	from kamaki import __version__ as version
338
	shell.greet(version)
339
	shell.do_EOF = shell.do_exit
340
	return shell
364
    shell = Shell()
365
    shell.set_prompt(basename(argv[0]))
366
    from kamaki import __version__ as version
367
    shell.greet(version)
368
    shell.do_EOF = shell.do_exit
369
    return shell
370

  
341 371

  
342 372
def run_shell():
343
	_fix_arguments()
344
	shell = _start_shell()
345
	_config = _arguments['config']
346
	_config.value = None
347
	for grp in _config.get_groups():
348
		global allow_all_commands
349
		allow_all_commands = True
350
		load_group_package(grp)
351
	setup_logging(silent=_arguments['silent'].value, debug=_arguments['debug'].value,
352
		verbose=_arguments['verbose'].value, include=_arguments['include'].value)
353
	shell.cmd_tree = _commands
354
	shell.run()
373
    _fix_arguments()
374
    shell = _start_shell()
375
    _config = _arguments['config']
376
    _config.value = None
377
    for grp in _config.get_groups():
378
        global allow_all_commands
379
        allow_all_commands = True
380
        load_group_package(grp)
381
    setup_logging(silent=_arguments['silent'].value,
382
        debug=_arguments['debug'].value,
383
        verbose=_arguments['verbose'].value,
384
        include=_arguments['include'].value)
385
    shell.cmd_tree = _commands
386
    shell.run()
387

  
355 388

  
356 389
def main():
357 390

  
358
	if len(argv) <= 1:
359
		run_shell()
360
	else:
361
		one_command()
391
    if len(argv) <= 1:
392
        run_shell()
393
    else:
394
        one_command()

Also available in: Unified diff