summaryrefslogtreecommitdiff
path: root/alot/commands/__init__.py
blob: e30769bb4c3bfea18ef287f16295cd4b8b5f6c01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# Copyright (C) 2011-2012  Patrick Totzke <patricktotzke@gmail.com>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file

import argparse
import glob
import logging
import os
import re

from ..settings.const   import settings
from ..helper           import split_commandstring

class Command:

    """base class for commands"""
    repeatable = False

    def __init__(self):
        self.prehook = None
        self.posthook = None
        self.undoable = False
        self.help = self.__doc__

    def apply(self, ui):
        """code that gets executed when this command is applied"""
        pass


class CommandCanceled(Exception):
    """ Exception triggered when an interactive command has been cancelled
    """
    pass


COMMANDS = {
    'search': {},
    'envelope': {},
    'bufferlist': {},
    'taglist': {},
    'namedqueries': {},
    'thread': {},
    'global': {},
}


def lookup_command(cmdname, mode):
    """
    returns commandclass, argparser and forced parameters used to construct
    a command for `cmdname` when called in `mode`.

    :param cmdname: name of the command to look up
    :type cmdname: str
    :param mode: mode identifier
    :type mode: str
    :rtype: (:class:`Command`, :class:`~argparse.ArgumentParser`,
            dict(str->dict))
    """
    if cmdname in COMMANDS[mode]:
        return COMMANDS[mode][cmdname]
    elif cmdname in COMMANDS['global']:
        return COMMANDS['global'][cmdname]
    else:
        return None, None, None


def lookup_parser(cmdname, mode):
    """
    returns the :class:`CommandArgumentParser` used to construct a
    command for `cmdname` when called in `mode`.
    """
    return lookup_command(cmdname, mode)[1]


class CommandParseError(Exception):

    """could not parse commandline string"""
    pass


class CommandArgumentParser(argparse.ArgumentParser):

    """
    :class:`~argparse.ArgumentParser` that raises :class:`CommandParseError`
    instead of printing to `sys.stderr`"""
    def exit(self, message):
        raise CommandParseError(message)

    def error(self, message):
        raise CommandParseError(message)


class registerCommand:
    """
    Decorator used to register a :class:`Command` as
    handler for command `name` in `mode` so that it
    can be looked up later using :func:`lookup_command`.

    Consider this example that shows how a :class:`Command` class
    definition is decorated to register it as handler for
    'save' in mode 'thread' and add boolean and string arguments::

    .. code-block::

        @registerCommand('thread', 'save', arguments=[
            (['--all'], {'action': 'store_true', 'help':'save all'}),
            (['path'], {'nargs':'?', 'help':'path to save to'})],
            help='save attachment(s)')
        class SaveAttachmentCommand(Command):
            pass

    """
    def __init__(self, mode, name, help=None, usage=None,
                 forced=None, arguments=None):
        """
        :param mode: mode identifier
        :type mode: str
        :param name: command name to register as
        :type name: str
        :param help: help string summarizing what this command does
        :type help: str
        :param usage: overides the auto generated usage string
        :type usage: str
        :param forced: keyword parameter used for commands constructor
        :type forced: dict (str->str)
        :param arguments: list of arguments given as pairs (args, kwargs)
                          accepted by
                          :meth:`argparse.ArgumentParser.add_argument`.
        :type arguments: list of (list of str, dict (str->str)
        """
        self.mode = mode
        self.name = name
        self.help = help
        self.usage = usage
        self.forced = forced or {}
        self.arguments = arguments or []

    def __call__(self, klass):
        helpstring = self.help or klass.__doc__
        argparser = CommandArgumentParser(description=helpstring,
                                          usage=self.usage,
                                          prog=self.name, add_help=False)
        for args, kwargs in self.arguments:
            argparser.add_argument(*args, **kwargs)
        COMMANDS[self.mode][self.name] = (klass, argparser, self.forced)
        return klass


def commandfactory(cmdline, mode='global'):
    """
    parses `cmdline` and constructs a :class:`Command`.

    :param cmdline: command line to interpret
    :type cmdline: str
    :param mode: mode identifier
    :type mode: str
    """
    # split commandname and parameters
    if not cmdline:
        return None
    logging.debug('mode:%s got commandline "%s"', mode, cmdline)
    # allow to shellescape without a space after '!'
    if cmdline.startswith('!'):
        cmdline = 'shellescape \'%s\'' % cmdline[1:]
    cmdline = re.sub(r'"(.*)"', r'"\\"\1\\""', cmdline)
    try:
        args = split_commandstring(cmdline)
    except ValueError as e:
        raise CommandParseError(str(e))
    logging.debug('ARGS: %s', args)
    cmdname = args[0]
    args = args[1:]

    # unfold aliases
    # TODO: read from settingsmanager

    # get class, argparser and forced parameter
    (cmdclass, parser, forcedparms) = lookup_command(cmdname, mode)
    if cmdclass is None:
        msg = 'unknown command: %s' % cmdname
        logging.debug(msg)
        raise CommandParseError(msg)

    parms = vars(parser.parse_args(args))
    parms.update(forcedparms)

    logging.debug('cmd parms %s', parms)

    # create Command
    cmd = cmdclass(**parms)

    # set pre and post command hooks
    get_hook = settings.get_hook
    cmd.prehook = get_hook('pre_%s_%s' % (mode, cmdname)) or \
        get_hook('pre_global_%s' % cmdname)
    cmd.posthook = get_hook('post_%s_%s' % (mode, cmdname)) or \
        get_hook('post_global_%s' % cmdname)

    return cmd


pyfiles = glob.glob1(os.path.dirname(__file__), '*.py')
__all__ = list(filename[:-3] for filename in pyfiles)