summaryrefslogtreecommitdiffstats
path: root/func/overlord/command.py
blob: 7fb7de423f8974e219793900b5f98166114421d5 (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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# -*- Mode: Python; test-case-name: test_command -*-
# vi:si:et:sw=4:sts=4:ts=4

# This file is released under the standard PSF license.
#
#  from MOAP - https://thomas.apestaart.org/moap/trac
#    written by Thomas Vander Stichele (thomas at apestaart dot org)
#

"""
Command class.
"""

import optparse
import sys

from func.config import read_config, CONFIG_FILE
from func.commonconfig import CMConfig

class CommandHelpFormatter(optparse.IndentedHelpFormatter):
    """
    I format the description as usual, but add an overview of commands
    after it if there are any, formatted like the options.
    """
    _commands = None

    def addCommand(self, name, description):
        if self._commands is None:
            self._commands = {}
        self._commands[name] = description

    ### override parent method
    def format_description(self, description):
        # textwrap doesn't allow for a way to preserve double newlines
        # to separate paragraphs, so we do it here.
        blocks = description.split('\n\n')
        rets = []

        for block in blocks:
            rets.append(optparse.IndentedHelpFormatter.format_description(self,
                block))
        ret = "\n".join(rets)
        if self._commands:
            commandDesc = []
            commandDesc.append("commands:")
            keys = self._commands.keys()
            keys.sort()
            length = 0
            for key in keys:
                if len(key) > length:
                    length = len(key)
            for name in keys:
                format = "  %-" + "%d" % length + "s  %s"
                commandDesc.append(format % (name, self._commands[name]))
            ret += "\n" + "\n".join(commandDesc) + "\n"
        return ret

class CommandOptionParser(optparse.OptionParser):
    """
    I parse options as usual, but I explicitly allow setting stdout
    so that our print_help() method (invoked by default with -h/--help)
    defaults to writing there.
    """
    _stdout = sys.stdout

    def set_stdout(self, stdout):
        self._stdout = stdout

    # we're overriding the built-in file, but we need to since this is
    # the signature from the base class
    __pychecker__ = 'no-shadowbuiltin'
    def print_help(self, file=None):
        # we are overriding a parent method so we can't do anything about file
        __pychecker__ = 'no-shadowbuiltin'
        if file is None:
            file = self._stdout
        file.write(self.format_help())

class Command:
    """
    I am a class that handles a command for a program.
    Commands can be nested underneath a command for further processing.

    @cvar name:        name of the command, lowercase
    @cvar aliases:     list of alternative lowercase names recognized
    @type aliases:     list of str
    @cvar usage:       short one-line usage string;
                       %command gets expanded to a sub-command or [commands]
                       as appropriate
    @cvar summary:     short one-line summary of the command
    @cvar description: longer paragraph explaining the command
    @cvar subCommands: dict of name -> commands below this command
    @type subCommands: dict of str  -> L{Command}
    """
    name = None
    aliases = None
    usage = None
    summary = None
    description = None
    parentCommand = None
    subCommands = None
    subCommandClasses = None
    aliasedSubCommands = None

    def __init__(self, parentCommand=None, stdout=sys.stdout,
        stderr=sys.stderr):
        """
        Create a new command instance, with the given parent.
        Allows for redirecting stdout and stderr if needed.
        This redirection will be passed on to child commands.
        """
        if not self.name:
            self.name = str(self.__class__).split('.')[-1].lower()
        self.stdout = stdout
        self.stderr = stderr
        self.parentCommand = parentCommand

        self.config = read_config(CONFIG_FILE, CMConfig)

        # create subcommands if we have them
        self.subCommands = {}
        self.aliasedSubCommands = {}
        if self.subCommandClasses:
            for C in self.subCommandClasses:
                c = C(self, stdout=stdout, stderr=stderr)
                self.subCommands[c.name] = c
                if c.aliases:
                    for alias in c.aliases:
                        self.aliasedSubCommands[alias] = c

        # create our formatter and add subcommands if we have them
        formatter = CommandHelpFormatter()
        if self.subCommands:
            for name, command in self.subCommands.items():
                formatter.addCommand(name, command.summary or
                    command.description)

        # expand %command for the bottom usage
        usage = self.usage or self.name
        if usage.find("%command") > -1:
            usage = usage.split("%command")[0] + '[command]'
        usages = [usage, ]

        # FIXME: abstract this into getUsage that takes an optional
        # parentCommand on where to stop recursing up
        # useful for implementing subshells

        # walk the tree up for our usage
        c = self.parentCommand
        while c:
            usage = c.usage or c.name
            if usage.find(" %command") > -1:
                usage = usage.split(" %command")[0]
            usages.append(usage)
            c = c.parentCommand
        usages.reverse()
        usage = " ".join(usages)

        # create our parser
        description = self.description or self.summary
        self.parser = CommandOptionParser(
            usage=usage, description=description,
            formatter=formatter)
        self.parser.set_stdout(self.stdout)
        self.parser.disable_interspersed_args()

        # allow subclasses to add options
        self.addOptions()

    def addOptions(self):
        """
        Override me to add options to the parser.
        """
        pass

    def do(self, args):
        """
        Override me to implement the functionality of the command.
        """
        pass

    def parse(self, argv):
        """
        Parse the given arguments and act on them.

        @rtype:   int
        @returns: an exit code
        """
        self.options, args = self.parser.parse_args(argv)

        # FIXME: make handleOptions not take options, since we store it
        # in self.options now
        ret = self.handleOptions(self.options)
        if ret:
            return ret

        # handle pleas for help
        if args and args[0] == 'help':
            self.debug('Asked for help, args %r' % args)

            # give help on current command if only 'help' is passed
            if len(args) == 1:
                self.outputHelp()
                return 0

            # complain if we were asked for help on a subcommand, but we don't
            # have any
            if not self.subCommands:
                self.stderr.write('No subcommands defined.')
                self.parser.print_usage(file=self.stderr)
                self.stderr.write(
                    "Use --help to get more information about this command.\n")
                return 1

            # rewrite the args the other way around;
            # help doap becomes doap help so it gets deferred to the doap
            # command
            args = [args[1], args[0]]


        # if we have args that we need to deal with, do it now
        # before we start looking for subcommands
        self.handleArguments(args)

        # if we don't have subcommands, defer to our do() method
        if not self.subCommands:
            ret = self.do(args)

            # if everything's fine, we return 0
            if not ret:
                ret = 0

            return ret


        # if we do have subcommands, defer to them
        try:
            command = args[0]
        except IndexError:
            self.parser.print_usage(file=self.stderr)
            self.stderr.write(
                "Use --help to get a list of commands.\n")
            return 1

        if command in self.subCommands.keys():
            return self.subCommands[command].parse(args[1:])

        if self.aliasedSubCommands:
            if command in self.aliasedSubCommands.keys():
                return self.aliasedSubCommands[command].parse(args[1:])

        self.stderr.write("Unknown command '%s'.\n" % command)
        return 1

    def outputHelp(self):
        """
        Output help information.
        """
        self.parser.print_help(file=self.stderr)

    def outputUsage(self):
        """
        Output usage information.
        Used when the options or arguments were missing or wrong.
        """
        self.parser.print_usage(file=self.stderr)

    def handleOptions(self, options):
        """
        Handle the parsed options.
        """
        pass

    def handleArguments(self, arguments):
        """
        Handle the parsed arguments.
        """
        pass

    def getRootCommand(self):
        """
        Return the top-level command, which is typically the program.
        """
        c = self
        while c.parentCommand:
            c = c.parentCommand
        return c