Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ db8d1766

History | View | Annotate | Download (13.6 kB)

1
# Copyright 2012-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.command
33

    
34
import logging
35
from sys import argv, exit, stdout
36
from os.path import basename
37
from inspect import getargspec
38

    
39
from kamaki.cli.argument import ArgumentParseManager
40
from kamaki.cli.history import History
41
from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
42
from kamaki.cli.errors import CLIError
43

    
44
_help = False
45
_debug = False
46
_verbose = False
47
_colors = False
48

    
49

    
50
def _construct_command_syntax(cls):
51
        spec = getargspec(cls.main.im_func)
52
        args = spec.args[1:]
53
        n = len(args) - len(spec.defaults or ())
54
        required = ' '.join('<%s>' % x\
55
            .replace('____', '[:')\
56
            .replace('___', ':')\
57
            .replace('__', ']').\
58
            replace('_', ' ') for x in args[:n])
59
        optional = ' '.join('[%s]' % x\
60
            .replace('____', '[:')\
61
            .replace('___', ':')\
62
            .replace('__', ']').\
63
            replace('_', ' ') for x in args[n:])
64
        cls.syntax = ' '.join(x for x in [required, optional] if x)
65
        if spec.varargs:
66
            cls.syntax += ' <%s ...>' % spec.varargs
67

    
68

    
69
def _get_cmd_tree_from_spec(spec, cmd_tree_list):
70
    for tree in cmd_tree_list:
71
        if tree.name == spec:
72
            return tree
73
    return None
74

    
75

    
76
_best_match = []
77

    
78

    
79
def _num_of_matching_terms(basic_list, attack_list):
80
    if not attack_list:
81
        return len(basic_list)
82

    
83
    matching_terms = 0
84
    for i, term in enumerate(basic_list):
85
        try:
86
            if term != attack_list[i]:
87
                break
88
        except IndexError:
89
            break
90
        matching_terms += 1
91
    return matching_terms
92

    
93

    
94
def _update_best_match(name_terms, prefix=[]):
95
    if prefix:
96
        pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
97
    else:
98
        pref_list = []
99

    
100
    num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
101
    global _best_match
102
    if not prefix:
103
        _best_match = []
104

    
105
    if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
106
        if len(_best_match) < num_of_matching_terms:
107
            _best_match = name_terms[:num_of_matching_terms]
108
        return True
109
    return False
110

    
111

    
112
def command(cmd_tree, prefix='', descedants_depth=1):
113
    """Load a class as a command
114
        spec_cmd0_cmd1 will be command spec cmd0
115
        @cmd_tree is initialized in cmd_spec file and is the structure
116
            where commands are loaded. Var name should be _commands
117
        @param prefix if given, load only commands prefixed with prefix,
118
        @param descedants_depth is the depth of the tree descedants of the
119
            prefix command. It is used ONLY if prefix and if prefix is not
120
            a terminal command
121
    """
122

    
123
    def wrap(cls):
124
        cls_name = cls.__name__
125

    
126
        if not cmd_tree:
127
            if _debug:
128
                print('Warning: command %s found but not loaded' % cls_name)
129
            return cls
130

    
131
        name_terms = cls_name.split('_')
132
        if not _update_best_match(name_terms, prefix):
133
            if _debug:
134
                print('Warning: %s failed to update_best_match' % cls_name)
135
            return None
136

    
137
        global _best_match
138
        max_len = len(_best_match) + descedants_depth
139
        if len(name_terms) > max_len:
140
            partial = '_'.join(name_terms[:max_len])
141
            if not cmd_tree.has_command(partial):  # add partial path
142
                cmd_tree.add_command(partial)
143
            if _debug:
144
                print('Warning: %s failed max_len test' % cls_name)
145
            return None
146

    
147
        cls.description, sep, cls.long_description\
148
        = cls.__doc__.partition('\n')
149
        _construct_command_syntax(cls)
150

    
151
        cmd_tree.add_command(cls_name, cls.description, cls)
152
        return cls
153
    return wrap
154

    
155

    
156
def get_cmd_terms():
157
    global command
158
    return [term for term in command.func_defaults[0]\
159
        if not term.startswith('-')]
160

    
161
cmd_spec_locations = [
162
    'kamaki.cli.commands',
163
    'kamaki.commands',
164
    'kamaki.cli',
165
    'kamaki',
166
    '']
167

    
168

    
169
def _setup_logging(silent=False, debug=False, verbose=False, include=False):
170
    """handle logging for clients package"""
171

    
172
    def add_handler(name, level, prefix=''):
173
        h = logging.StreamHandler()
174
        fmt = logging.Formatter(prefix + '%(message)s')
175
        h.setFormatter(fmt)
176
        logger = logging.getLogger(name)
177
        logger.addHandler(h)
178
        logger.setLevel(level)
179

    
180
    if silent:
181
        add_handler('', logging.CRITICAL)
182
        return
183

    
184
    if debug:
185
        add_handler('requests', logging.INFO, prefix='* ')
186
        add_handler('clients.send', logging.DEBUG, prefix='> ')
187
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
188
        add_handler('kamaki', logging.DEBUG, prefix='[DEBUG]: ')
189
    elif verbose:
190
        add_handler('requests', logging.INFO, prefix='* ')
191
        add_handler('clients.send', logging.INFO, prefix='> ')
192
        add_handler('clients.recv', logging.INFO, prefix='< ')
193
        add_handler('kamaki', logging.INFO, prefix='[INFO]: ')
194
    elif include:
195
        add_handler('clients.recv', logging.INFO)
196
    add_handler('kamaki', logging.WARNING, prefix='[WARNING]: ')
197

    
198

    
199
def _init_session(arguments):
200
    global _help
201
    _help = arguments['help'].value
202
    global _debug
203
    _debug = arguments['debug'].value
204
    global _verbose
205
    _verbose = arguments['verbose'].value
206
    global _colors
207
    _colors = arguments['config'].get('global', 'colors')
208
    if not (stdout.isatty() and _colors == 'on'):
209
        from kamaki.cli.utils import remove_colors
210
        remove_colors()
211
    _silent = arguments['silent'].value
212
    _include = arguments['include'].value
213
    _setup_logging(_silent, _debug, _verbose, _include)
214

    
215

    
216
def get_command_group(unparsed, arguments):
217
    groups = arguments['config'].get_groups()
218
    for term in unparsed:
219
        if term.startswith('-'):
220
            continue
221
        if term in groups:
222
            unparsed.remove(term)
223
            return term
224
        return None
225
    return None
226

    
227

    
228
def _load_spec_module(spec, arguments, module):
229
    spec_name = arguments['config'].get(spec, 'cli')
230
    if spec_name is None:
231
        return None
232
    pkg = None
233
    for location in cmd_spec_locations:
234
        location += spec_name if location == '' else '.%s' % spec_name
235
        try:
236
            pkg = __import__(location, fromlist=[module])
237
            return pkg
238
        except ImportError:
239
            continue
240
    return pkg
241

    
242

    
243
def _groups_help(arguments):
244
    global _debug
245
    descriptions = {}
246
    for spec in arguments['config'].get_groups():
247
        pkg = _load_spec_module(spec, arguments, '_commands')
248
        if pkg:
249
            cmds = None
250
            try:
251
                cmds = [
252
                    cmd for cmd in getattr(pkg, '_commands')\
253
                    if arguments['config'].get(cmd.name, 'cli')
254
                ]
255
            except AttributeError:
256
                if _debug:
257
                    print('Warning: No description for %s' % spec)
258
            try:
259
                for cmd in cmds:
260
                    descriptions[cmd.name] = cmd.description
261
            except TypeError:
262
                if _debug:
263
                    print('Warning: no cmd specs in module %s' % spec)
264
        elif _debug:
265
            print('Warning: Loading of %s cmd spec failed' % spec)
266
    print('\nOptions:\n - - - -')
267
    print_dict(descriptions)
268

    
269

    
270
def _print_subcommands_help(cmd):
271
    printout = {}
272
    for subcmd in cmd.get_subcommands():
273
        spec, sep, print_path = subcmd.path.partition('_')
274
        printout[print_path.replace('_', ' ')] = subcmd.description
275
    if printout:
276
        print('\nOptions:\n - - - -')
277
        print_dict(printout)
278

    
279

    
280
def _update_parser_help(parser, cmd):
281
    global _best_match
282
    parser.syntax = parser.syntax.split('<')[0]
283
    parser.syntax += ' '.join(_best_match)
284

    
285
    if cmd.is_command:
286
        cls = cmd.get_class()
287
        parser.syntax += ' ' + cls.syntax
288
        parser.update_arguments(cls().arguments)
289
        # arguments = cls().arguments
290
        # update_arguments(parser, arguments)
291
    else:
292
        parser.syntax += ' <...>'
293
    if cmd.has_description:
294
        parser.parser.description = cmd.help
295

    
296

    
297
def _print_error_message(cli_err):
298
    errmsg = '%s' % cli_err
299
    if cli_err.importance == 1:
300
        errmsg = magenta(errmsg)
301
    elif cli_err.importance == 2:
302
        errmsg = yellow(errmsg)
303
    elif cli_err.importance > 2:
304
        errmsg = red(errmsg)
305
    stdout.write(errmsg)
306
    print_list(cli_err.details)
307

    
308

    
309
def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
310
    matched = [term for term in unparsed if not term.startswith('-')]
311
    while matched:
312
        try:
313
            return cmd_tree.get_command('_'.join(matched))
314
        except KeyError:
315
            matched = matched[:-1]
316
    return None
317

    
318

    
319
def _exec_cmd(instance, cmd_args, help_method):
320
    try:
321
        return instance.main(*cmd_args)
322
    except TypeError as err:
323
        if err.args and err.args[0].startswith('main()'):
324
            print(magenta('Syntax error'))
325
            if _debug:
326
                raise err
327
            if _verbose:
328
                print(unicode(err))
329
            help_method()
330
        else:
331
            raise
332
    return 1
333

    
334

    
335
def set_command_params(parameters):
336
    """Add a parameters list to a command
337

338
    :param paramters: (list of str) a list of parameters
339
    """
340
    global command
341
    def_params = list(command.func_defaults)
342
    def_params[0] = parameters
343
    command.func_defaults = tuple(def_params)
344

    
345

    
346
#def one_cmd(parser, unparsed, arguments):
347
def one_cmd(parser):
348
    group = get_command_group(list(parser.unparsed), parser.arguments)
349
    if not group:
350
        parser.parser.print_help()
351
        _groups_help(parser.arguments)
352
        exit(0)
353

    
354
    nonargs = [term for term in parser.unparsed if not term.startswith('-')]
355
    set_command_params(nonargs)
356

    
357
    global _best_match
358
    _best_match = []
359

    
360
    spec_module = _load_spec_module(group, parser.arguments, '_commands')
361

    
362
    cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
363

    
364
    if _best_match:
365
        cmd = cmd_tree.get_command('_'.join(_best_match))
366
    else:
367
        cmd = _get_best_match_from_cmd_tree(cmd_tree, parser.unparsed)
368
        _best_match = cmd.path.split('_')
369
    if cmd is None:
370
        if _debug or _verbose:
371
            print('Unexpected error: failed to load command')
372
        exit(1)
373

    
374
    _update_parser_help(parser, cmd)
375

    
376
    if _help or not cmd.is_command:
377
        parser.parser.print_help()
378
        _print_subcommands_help(cmd)
379
        exit(0)
380

    
381
    cls = cmd.get_class()
382
    executable = cls(parser.arguments)
383
    parser.update_arguments(executable.arguments)
384
    #parsed, unparsed = parse_known_args(parser, executable.arguments)
385
    for term in _best_match:
386
        parser.unparsed.remove(term)
387
    _exec_cmd(executable, parser.unparsed, parser.parser.print_help)
388

    
389

    
390
def _load_all_commands(cmd_tree, arguments):
391
    _config = arguments['config']
392
    for spec in [spec for spec in _config.get_groups()\
393
            if _config.get(spec, 'cli')]:
394
        try:
395
            spec_module = _load_spec_module(spec, arguments, '_commands')
396
            spec_commands = getattr(spec_module, '_commands')
397
        except AttributeError:
398
            if _debug:
399
                print('Warning: No valid description for %s' % spec)
400
            continue
401
        for spec_tree in spec_commands:
402
            if spec_tree.name == spec:
403
                cmd_tree.add_tree(spec_tree)
404
                break
405

    
406

    
407
def run_shell(exe_string, parser):
408
    from command_shell import _init_shell
409
    shell = _init_shell(exe_string, parser)
410
    _load_all_commands(shell.cmd_tree, parser.arguments)
411
    shell.run(parser)
412

    
413

    
414
def main():
415
    try:
416
        exe = basename(argv[0])
417
        parser = ArgumentParseManager(exe)
418

    
419
        if parser.arguments['version'].value:
420
            exit(0)
421

    
422
        _init_session(parser.arguments)
423

    
424
        if parser.unparsed:
425
            _history = History(
426
                parser.arguments['config'].get('history', 'file'))
427
            _history.add(' '.join([exe] + argv[1:]))
428
            one_cmd(parser)
429
        elif _help:
430
            parser.parser.print_help()
431
            _groups_help(parser.arguments)
432
        else:
433
            run_shell(exe, parser)
434
    except CLIError as err:
435
        _print_error_message(err)
436
        if _debug:
437
            raise err
438
        exit(1)
439
    except Exception as er:
440
        print('Unknown Error: %s' % er)
441
        if _debug:
442
            raise
443
        exit(1)