Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ d88ba587

History | View | Annotate | Download (13.3 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, init_parser,\
40
    update_arguments
41
from kamaki.cli.history import History
42
from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
43
from kamaki.cli.errors import CLIError
44

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

    
50

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

    
69

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

    
76

    
77
_best_match = []
78

    
79

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

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

    
94

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

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

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

    
112

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

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

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

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

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

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

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

    
156

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

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

    
169

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

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

    
181
    if silent:
182
        add_handler('', logging.CRITICAL)
183
    elif debug:
184
        add_handler('requests', logging.INFO, prefix='* ')
185
        add_handler('clients.send', logging.DEBUG, prefix='> ')
186
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
187
    elif verbose:
188
        add_handler('requests', logging.INFO, prefix='* ')
189
        add_handler('clients.send', logging.INFO, prefix='> ')
190
        add_handler('clients.recv', logging.INFO, prefix='< ')
191
    elif include:
192
        add_handler('clients.recv', logging.INFO)
193
    else:
194
        add_handler('', logging.WARNING)
195

    
196

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

    
213

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

    
225

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

    
240

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

    
267

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

    
277

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

    
283
    if cmd.is_command:
284
        cls = cmd.get_class()
285
        parser.prog += ' ' + cls.syntax
286
        arguments = cls().arguments
287
        update_arguments(parser, arguments)
288
    else:
289
        parser.prog += ' <...>'
290
    if cmd.has_description:
291
        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_param(param, value):
333
    if param == 'prefix':
334
        pos = 0
335
    elif param == 'descedants_depth':
336
        pos = 1
337
    else:
338
        return
339
    global command
340
    def_params = list(command.func_defaults)
341
    def_params[pos] = value
342
    command.func_defaults = tuple(def_params)
343

    
344

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

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

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

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

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

    
373
    _update_parser_help(parser, cmd)
374

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

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

    
387

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

    
404

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

    
411

    
412
def main():
413
    try:
414
        exe = basename(argv[0])
415
        parser = init_parser(exe, _arguments)
416
        parsed, unparsed = parse_known_args(parser, _arguments)
417

    
418
        if _arguments['version'].value:
419
            exit(0)
420

    
421
        _init_session(_arguments)
422

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