Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 7dbd52f5

History | View | Annotate | Download (13.4 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 _arguments, parse_known_args, update_arguments
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
    elif debug:
183
        add_handler('requests', logging.INFO, prefix='* ')
184
        add_handler('clients.send', logging.DEBUG, prefix='> ')
185
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
186
    elif verbose:
187
        add_handler('requests', logging.INFO, prefix='* ')
188
        add_handler('clients.send', logging.INFO, prefix='> ')
189
        add_handler('clients.recv', logging.INFO, prefix='< ')
190
    elif include:
191
        add_handler('clients.recv', logging.INFO)
192
    else:
193
        add_handler('', logging.WARNING)
194

    
195

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

    
212

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

    
224

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

    
239

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

    
266

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

    
276

    
277
def _update_parser_help(parser, cmd):
278
    global _best_match
279
    parser.prog = parser.prog.split('<')[0]
280
    parser.prog += ' '.join(_best_match)
281

    
282
    if cmd.is_command:
283
        cls = cmd.get_class()
284
        parser.prog += ' ' + cls.syntax
285
        arguments = cls().arguments
286
        update_arguments(parser, arguments)
287
    else:
288
        parser.prog += ' <...>'
289
    if cmd.has_description:
290
        parser.description = cmd.help
291

    
292

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

    
304

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

    
314

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

    
330

    
331
def set_command_param(param, value):
332
    if param == 'prefix':
333
        pos = 0
334
    elif param == 'descedants_depth':
335
        pos = 1
336
    else:
337
        return
338
    global command
339
    def_params = list(command.func_defaults)
340
    def_params[pos] = value
341
    command.func_defaults = tuple(def_params)
342

    
343

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

    
351
    set_command_param(
352
        'prefix',
353
        [term for term in unparsed if not term.startswith('-')]
354
    )
355
    global _best_match
356
    _best_match = []
357

    
358
    spec_module = _load_spec_module(group, arguments, '_commands')
359

    
360
    cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
361

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

    
372
    _update_parser_help(parser, cmd)
373

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

    
379
    cls = cmd.get_class()
380
    executable = cls(arguments)
381
    parsed, unparsed = parse_known_args(parser, executable.arguments)
382
    for term in _best_match:
383
        unparsed.remove(term)
384
    _exec_cmd(executable, unparsed, parser.print_help)
385

    
386

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

    
403

    
404
def run_shell(exe_string, arguments):
405
    from command_shell import _init_shell
406
    shell = _init_shell(exe_string, arguments)
407
    _load_all_commands(shell.cmd_tree, arguments)
408
    shell.run(arguments)
409

    
410

    
411
from kamaki.cli.argument import ArgumentParseManager
412

    
413

    
414
def main():
415
    try:
416
        exe = basename(argv[0])
417
        parser = ArgumentParseManager(exe)
418
        parsed, unparsed = parse_known_args(parser.parser, parser.arguments)
419

    
420
        if _arguments['version'].value:
421
            exit(0)
422

    
423
        _init_session(_arguments)
424

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