Trimming more stuff out.
[certmaster.git] / certmaster / overlord / command.py
1 # -*- Mode: Python; test-case-name: test_command -*-
2 # vi:si:et:sw=4:sts=4:ts=4
3
4 # This file is released under the standard PSF license.
5 #
6 # from MOAP - https://thomas.apestaart.org/moap/trac
7 # written by Thomas Vander Stichele (thomas at apestaart dot org)
8 #
9
10 """
11 Command class.
12 """
13
14 import optparse
15 import sys
16
17 from func.config import read_config, CONFIG_FILE
18 from func.commonconfig import CMConfig
19
20 class CommandHelpFormatter(optparse.IndentedHelpFormatter):
21 """
22 I format the description as usual, but add an overview of commands
23 after it if there are any, formatted like the options.
24 """
25 _commands = None
26
27 def addCommand(self, name, description):
28 if self._commands is None:
29 self._commands = {}
30 self._commands[name] = description
31
32 ### override parent method
33 def format_description(self, description):
34 # textwrap doesn't allow for a way to preserve double newlines
35 # to separate paragraphs, so we do it here.
36 blocks = description.split('\n\n')
37 rets = []
38
39 for block in blocks:
40 rets.append(optparse.IndentedHelpFormatter.format_description(self,
41 block))
42 ret = "\n".join(rets)
43 if self._commands:
44 commandDesc = []
45 commandDesc.append("commands:")
46 keys = self._commands.keys()
47 keys.sort()
48 length = 0
49 for key in keys:
50 if len(key) > length:
51 length = len(key)
52 for name in keys:
53 format = " %-" + "%d" % length + "s %s"
54 commandDesc.append(format % (name, self._commands[name]))
55 ret += "\n" + "\n".join(commandDesc) + "\n"
56 return ret
57
58 class CommandOptionParser(optparse.OptionParser):
59 """
60 I parse options as usual, but I explicitly allow setting stdout
61 so that our print_help() method (invoked by default with -h/--help)
62 defaults to writing there.
63 """
64 _stdout = sys.stdout
65
66 def set_stdout(self, stdout):
67 self._stdout = stdout
68
69 # we're overriding the built-in file, but we need to since this is
70 # the signature from the base class
71 __pychecker__ = 'no-shadowbuiltin'
72 def print_help(self, file=None):
73 # we are overriding a parent method so we can't do anything about file
74 __pychecker__ = 'no-shadowbuiltin'
75 if file is None:
76 file = self._stdout
77 file.write(self.format_help())
78
79 class Command:
80 """
81 I am a class that handles a command for a program.
82 Commands can be nested underneath a command for further processing.
83
84 @cvar name: name of the command, lowercase
85 @cvar aliases: list of alternative lowercase names recognized
86 @type aliases: list of str
87 @cvar usage: short one-line usage string;
88 %command gets expanded to a sub-command or [commands]
89 as appropriate
90 @cvar summary: short one-line summary of the command
91 @cvar description: longer paragraph explaining the command
92 @cvar subCommands: dict of name -> commands below this command
93 @type subCommands: dict of str -> L{Command}
94 """
95 name = None
96 aliases = None
97 usage = None
98 summary = None
99 description = None
100 parentCommand = None
101 subCommands = None
102 subCommandClasses = None
103 aliasedSubCommands = None
104
105 def __init__(self, parentCommand=None, stdout=sys.stdout,
106 stderr=sys.stderr):
107 """
108 Create a new command instance, with the given parent.
109 Allows for redirecting stdout and stderr if needed.
110 This redirection will be passed on to child commands.
111 """
112 if not self.name:
113 self.name = str(self.__class__).split('.')[-1].lower()
114 self.stdout = stdout
115 self.stderr = stderr
116 self.parentCommand = parentCommand
117
118 self.config = read_config(CONFIG_FILE, CMConfig)
119
120 # create subcommands if we have them
121 self.subCommands = {}
122 self.aliasedSubCommands = {}
123 if self.subCommandClasses:
124 for C in self.subCommandClasses:
125 c = C(self, stdout=stdout, stderr=stderr)
126 self.subCommands[c.name] = c
127 if c.aliases:
128 for alias in c.aliases:
129 self.aliasedSubCommands[alias] = c
130
131 # create our formatter and add subcommands if we have them
132 formatter = CommandHelpFormatter()
133 if self.subCommands:
134 for name, command in self.subCommands.items():
135 formatter.addCommand(name, command.summary or
136 command.description)
137
138 # expand %command for the bottom usage
139 usage = self.usage or self.name
140 if usage.find("%command") > -1:
141 usage = usage.split("%command")[0] + '[command]'
142 usages = [usage, ]
143
144 # FIXME: abstract this into getUsage that takes an optional
145 # parentCommand on where to stop recursing up
146 # useful for implementing subshells
147
148 # walk the tree up for our usage
149 c = self.parentCommand
150 while c:
151 usage = c.usage or c.name
152 if usage.find(" %command") > -1:
153 usage = usage.split(" %command")[0]
154 usages.append(usage)
155 c = c.parentCommand
156 usages.reverse()
157 usage = " ".join(usages)
158
159 # create our parser
160 description = self.description or self.summary
161 self.parser = CommandOptionParser(
162 usage=usage, description=description,
163 formatter=formatter)
164 self.parser.set_stdout(self.stdout)
165 self.parser.disable_interspersed_args()
166
167 # allow subclasses to add options
168 self.addOptions()
169
170 def addOptions(self):
171 """
172 Override me to add options to the parser.
173 """
174 pass
175
176 def do(self, args):
177 """
178 Override me to implement the functionality of the command.
179 """
180 pass
181
182 def parse(self, argv):
183 """
184 Parse the given arguments and act on them.
185
186 @rtype: int
187 @returns: an exit code
188 """
189 self.options, args = self.parser.parse_args(argv)
190
191 # FIXME: make handleOptions not take options, since we store it
192 # in self.options now
193 ret = self.handleOptions(self.options)
194 if ret:
195 return ret
196
197 # handle pleas for help
198 if args and args[0] == 'help':
199 self.debug('Asked for help, args %r' % args)
200
201 # give help on current command if only 'help' is passed
202 if len(args) == 1:
203 self.outputHelp()
204 return 0
205
206 # complain if we were asked for help on a subcommand, but we don't
207 # have any
208 if not self.subCommands:
209 self.stderr.write('No subcommands defined.')
210 self.parser.print_usage(file=self.stderr)
211 self.stderr.write(
212 "Use --help to get more information about this command.\n")
213 return 1
214
215 # rewrite the args the other way around;
216 # help doap becomes doap help so it gets deferred to the doap
217 # command
218 args = [args[1], args[0]]
219
220
221 # if we have args that we need to deal with, do it now
222 # before we start looking for subcommands
223 self.handleArguments(args)
224
225 # if we don't have subcommands, defer to our do() method
226 if not self.subCommands:
227 ret = self.do(args)
228
229 # if everything's fine, we return 0
230 if not ret:
231 ret = 0
232
233 return ret
234
235
236 # if we do have subcommands, defer to them
237 try:
238 command = args[0]
239 except IndexError:
240 self.parser.print_usage(file=self.stderr)
241 self.stderr.write(
242 "Use --help to get a list of commands.\n")
243 return 1
244
245 if command in self.subCommands.keys():
246 return self.subCommands[command].parse(args[1:])
247
248 if self.aliasedSubCommands:
249 if command in self.aliasedSubCommands.keys():
250 return self.aliasedSubCommands[command].parse(args[1:])
251
252 self.stderr.write("Unknown command '%s'.\n" % command)
253 return 1
254
255 def outputHelp(self):
256 """
257 Output help information.
258 """
259 self.parser.print_help(file=self.stderr)
260
261 def outputUsage(self):
262 """
263 Output usage information.
264 Used when the options or arguments were missing or wrong.
265 """
266 self.parser.print_usage(file=self.stderr)
267
268 def handleOptions(self, options):
269 """
270 Handle the parsed options.
271 """
272 pass
273
274 def handleArguments(self, arguments):
275 """
276 Handle the parsed arguments.
277 """
278 pass
279
280 def getRootCommand(self):
281 """
282 Return the top-level command, which is typically the program.
283 """
284 c = self
285 while c.parentCommand:
286 c = c.parentCommand
287 return c