Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ c17b8bc0

History | View | Annotate | Download (13.5 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
    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.syntax = parser.syntax.split('<')[0]
280
    parser.syntax += ' '.join(_best_match)
281

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

    
293

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

    
305

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

    
315

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

    
331

    
332
def set_command_params(parameters):
333
    """Add a parameters list to a command
334

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

    
342

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

    
351
    nonargs = [term for term in parser.unparsed if not term.startswith('-')]
352
    set_command_params(nonargs)
353

    
354
    global _best_match
355
    _best_match = []
356

    
357
    spec_module = _load_spec_module(group, parser.arguments, '_commands')
358

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

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

    
371
    _update_parser_help(parser, cmd)
372

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

    
378
    cls = cmd.get_class()
379
    executable = cls(parser.arguments)
380
    parser.update_arguments(executable.arguments)
381
    #parsed, unparsed = parse_known_args(parser, executable.arguments)
382
    for term in _best_match:
383
        parser.unparsed.remove(term)
384
    _exec_cmd(executable, parser.unparsed, parser.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, parser):
405
    from command_shell import _init_shell
406
    shell = _init_shell(exe_string, parser)
407
    _load_all_commands(shell.cmd_tree, parser.arguments)
408
    shell.run(parser)
409

    
410

    
411
def main():
412
    try:
413
        exe = basename(argv[0])
414
        parser = ArgumentParseManager(exe)
415

    
416
        if parser.arguments['version'].value:
417
            exit(0)
418

    
419
        _init_session(parser.arguments)
420

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