3 ### Subcommand dispatch
5 ### (c) 2013 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of Chopwood: a password-changing service.
12 ### Chopwood is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU Affero General Public License as
14 ### published by the Free Software Foundation; either version 3 of the
15 ### License, or (at your option) any later version.
17 ### Chopwood is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU Affero General Public License for more details.
22 ### You should have received a copy of the GNU Affero General Public
23 ### License along with Chopwood; if not, see
24 ### <http://www.gnu.org/licenses/>.
26 from __future__
import with_statement
29 from cStringIO
import StringIO
32 from output
import OUT
35 ### We've built enough infrastructure now: it's time to move on to user
38 ### Everything is done in terms of `subcommands'. A subcommand has a name, a
39 ### set of `contexts' in which it's active (see below), a description (for
40 ### help), a function, and a bunch of parameters. There are a few different
41 ### kinds of parameters, but the basic idea is that they have names and
42 ### values. When we invoke a subcommand, we'll pass the parameter values as
43 ### keyword arguments to the function.
45 ### We have a fair number of different interfaces to provide: there's an
46 ### administration interface for adding and removing new users and accounts;
47 ### there's a GNU Userv interface for local users to change their passwords;
48 ### there's an SSH interface for remote users and for acting as a remote
49 ### service; and there's a CGI interface. To make life a little more
50 ### confusing, sets of commands don't map one-to-one with these various
51 ### interfaces: for example, the remote-user SSH interface is (basically) the
52 ### same as the Userv interface, and the CGI interface offers two distinct
53 ### command sets depending on whether the user has authenticated.
55 ### We call these various command sets `contexts'. To be useful, a
56 ### subcommand must be active within at least one context. Command lookup
57 ### takes place with a specific context in mind, and command names only need
58 ### be unique within a particular context. Commands from a different context
59 ### are simply unavailable.
61 ### When it comes to parameters, we have simple positional arguments, and
62 ### fancy options. Positional arguments are called this because on the
63 ### command line they're only distinguished by their order. Like Lisp
64 ### functions, a subcommand has some of mandatory formal arguments, followed
65 ### by some optional arguments, and finally maybe a `rest' argument which
66 ### gobbles up any remaining actual arguments as a list. To make things more
67 ### fun, we also have options, which conform to the usual Unix command-line
70 ### Finally, there's a global set of options, always read from the command
71 ### line, which affects stuff like which configuration file to use, and can
72 ### also be useful in testing and debugging.
74 ###--------------------------------------------------------------------------
77 ## The global options. This will carry the option values once they've been
81 class Parameter (object):
83 Base class for parameters.
85 Currently only stores the parameter's name, which does double duty as the
86 name of the handler function's keyword argument which will receive this
87 parameter's value, and the parameter name in the CGI interface from which
90 def __init__(me
, name
):
93 class Opt (Parameter
):
95 An option, i.e., one which is presented as an option-flag in command-line
98 The SHORT and LONG strings are the option flags for this parameter. The
99 SHORT string should be a single `-' followed by a single character (usually
100 a letter. The LONG string should be a pair `--' followed by a name
101 (usually words, joined with hyphens).
103 The HELP is presented to the user as a description of the option.
105 The ARGNAME may be either `None' to indicate that this is a simple boolean
106 switch (the value passed to the handler function will be `True' or
107 `False'), or a string (conventionally in uppercase, used as a metasyntactic
108 variable in the generated usage synopsis) to indicate that the option takes
109 a general string argument (passed literally to the handler function).
111 def __init__(me
, name
, short
, long, help, argname
= None):
112 Parameter
.__init__(me
, name
)
118 class Arg (Parameter
):
120 A (positional) argument. Nothing much to do here.
122 The parameter name, converted to upper case, is used as a metasyntactic
123 variable in the generated usage synopsis.
127 ###--------------------------------------------------------------------------
130 class Subcommand (object):
134 Many interesting things about the subcommand are made available as
138 The subcommand name. Used to look the command up (see
139 the `lookup_subcommand' method of `SubcommandOptionParser'), and in
140 usage and help messages.
143 A set (coerced from any iterable provided to the constructor) of
144 contexts in which this subcommand is available.
147 A description of the subcommand, provided if the user requests
151 The handler function, invoked to actually carry out the subcommand.
154 A list of `Opt' objects, used to build the option parser.
156 `params', `oparams', `rparam'
157 `Arg' objects for the positional parameters. `params' is a list of
158 mandatory parameters; `oparams' is a list of optional parameters; and
159 `rparam' is either an `Arg' for the `rest' parameter, or `None' if
160 there is no `rest' parameter.
163 def __init__(me
, name
, contexts
, desc
, func
, opts
= [],
164 params
= [], oparams
= [], rparam
= None):
166 Initialize a subcommand object. The constructors arguments are used to
167 initialize attributes on the object; see the class docstring for details.
170 me
.contexts
= set(contexts
)
179 """Generate a suitable usage summary for the subcommand."""
181 ## Cache the summary in an attribute.
182 try: return me
._usage
183 except AttributeError: pass
185 ## Gather up a list of switches and options with arguments.
190 if o
.short
: u
.append('[%s %s]' %
(o
.short
, o
.argname
.upper()))
191 else: u
.append('%s=%s' %
(o
.long, o
.argname
.upper()))
193 if o
.short
: sw
.append(o
.short
[1])
194 else: u
.append(o
.long)
196 ## Generate the usage message.
197 me
._usage
= ' '.join(
198 [me
.name
] + # The command name.
199 (sw
and ['[-%s]' %
''.join(sorted(sw
))] or []) +
200 # Switches, in order.
201 sorted(u
) + # Options with arguments, and
202 # options without short names.
203 [p
.name
.upper() for p
in me
.params
] +
204 # Required arguments, in order.
205 ['[%s]' % p
.name
.upper() for p
in me
.oparams
] +
206 # Optional arguments, in order.
207 (me
.rparam
and ['[%s ...]' % me
.rparam
.name
.upper()] or []))
208 # The `rest' argument, if present.
215 Make and return an `OptionParser' object for this subcommand.
217 This is used for dispatching through a command-line interface, and for
218 generating subcommand-specific help.
220 op
= OP
.OptionParser(usage
= 'usage: %%prog %s' % me
.usage(),
221 description
= me
.desc
)
223 op
.add_option(o
.short
, o
.long, dest
= o
.name
, help = o
.help,
224 action
= o
.argname
and 'store' or 'store_true',
228 def cmdline(me
, args
):
230 Invoke the subcommand given a list ARGS of command-line arguments.
233 ## Parse any options.
235 opts
, args
= op
.parse_args(args
)
237 ## Count up the remaining positional arguments supplied, and how many
238 ## mandatory and optional arguments we want.
241 nop
= len(me
.oparams
)
243 ## Complain if there's a mismatch.
244 if na
< np
or (not me
.rparam
and na
> np
+ nop
):
245 raise U
.ExpectedError
, (400, 'Wrong number of arguments')
247 ## Now we want to gather the parameters into a dictionary.
250 ## First, work through the various options. The option parser tends to
251 ## define attributes for omitted options with the value `None': we leave
252 ## this out of the keywords dictionary so that the subcommand can provide
253 ## its own default values.
255 try: v
= getattr(opts
, o
.name
)
256 except AttributeError: pass
258 if v
is not None: kw
[o
.name
] = v
260 ## Next, assign values from positional arguments to the corresponding
262 for a
, p
in zip(args
, me
.params
+ me
.oparams
):
265 ## If we have a `rest' parameter then set it to any arguments which
266 ## haven't yet been consumed.
268 kw
[me
.rparam
.name
] = na
> np
+ nop
and args
[np
+ nop
:] or []
270 ## Call the handler function.
273 ###--------------------------------------------------------------------------
274 ### Option parsing with subcommands.
276 class SubcommandOptionParser (OP
.OptionParser
, object):
278 A subclass of `OptionParser' with some additional knowledge about
281 The current context is maintained in the `context' attribute, which can be
282 freely assigned by the client. The initial value is chosen as the first in
283 the CONTEXTS list, which is otherwise only used to set up the `help'
287 def __init__(me
, usage
= '%prog [-OPTIONS] COMMAND [ARGS ...]',
288 contexts
= ['cli'], commands
= [], *args
, **kw
):
290 Constructor for the options parser. As for the superclass, but with an
291 additional argument CONTEXTS used for initializing the `help' command.
293 super(SubcommandOptionParser
, me
).__init__(usage
= usage
, *args
, **kw
)
296 ## We must turn of the `interspersed arguments' feature: otherwise we'll
297 ## eat the subcommand's arguments.
298 me
.disable_interspersed_args()
299 me
.context
= list(contexts
)[0]
301 ## Provide a default `help' command.
303 me
.addsubcmd(Subcommand(
306 desc
= 'Show help for %prog, or for the COMMANDs.',
307 rparam
= Arg('commands')))
308 for sub
in commands
: me
.addsubcmd(sub
)
310 def addsubcmd(me
, sub
):
311 """Add a subcommand to the main map."""
312 for c
in sub
.contexts
:
313 me
._cmds
[sub
.name
, c
] = sub
315 def print_help(me
, file = None, *args
, **kw
):
317 Print a help message. This augments the superclass behaviour by printing
318 synopses for the available subcommands.
320 if file is None: file = SYS
.stdout
321 super(SubcommandOptionParser
, me
).print_help(file = file, *args
, **kw
)
322 file.write('\nCommands:\n')
323 for sub
in sorted(set(me
._cmds
.values()), key
= lambda c
: c
.name
):
324 if sub
.desc
is None or me
.context
not in sub
.contexts
: continue
325 file.write('\t%s\n' % sub
.usage())
327 def cmd_help(me
, commands
= []):
329 A default `help' command. With arguments, print help about those;
330 otherwise just print help on the main program, as for `--help'.
334 me
.print_help(file = s
)
337 for name
in commands
:
340 c
= me
.lookup_subcommand(name
)
341 c
.mkoptparse().print_help(file = s
)
342 OUT
.write(s
.getvalue())
344 def lookup_subcommand(me
, name
, exactp
= False, context
= None):
346 Find the subcommand with the given NAME in the CONTEXT (default the
347 current context). Unless EXACTP, accept a command for which NAME is an
348 unambiguous prefix. Return the subcommand object, or raise an
349 appropriate `ExpectedError'.
352 if context
is None: context
= me
.context
354 ## See if we can find an exact match.
355 try: c
= me
._cmds
[name
, context
]
356 except KeyError: pass
359 ## No. Maybe we'll find a prefix match.
362 for c
in set(me
._cmds
.values()):
363 if context
in c
.contexts
and \
364 c
.name
.startswith(name
):
367 ## See what we came up with.
369 raise U
.ExpectedError
, (404, "Unknown command `%s'" % name
)
371 raise U
.ExpectedError
, (
373 ("Ambiguous command `%s': could be any of %s" %
374 (name
, ', '.join("`%s'" % c
.name
for c
in match
))))
378 def dispatch(me
, context
, args
):
380 Invoke the appropriate subcommand, indicated by ARGS, within the CONTEXT.
383 if not args
: raise U
.ExpectedError
, (400, "Missing command")
385 c
= me
.lookup_subcommand(args
[0])
388 ###--------------------------------------------------------------------------
389 ### Registry of subcommands.
391 ## Our list of commands. We'll attach this to the options parser when we're
395 def subcommand(name
, contexts
, desc
, cls
= Subcommand
, *args
, **kw
):
396 """Decorator for defining subcommands."""
398 COMMANDS
.append(cls(name
, contexts
, desc
, func
, *args
, **kw
))
401 ###----- That's all, folks --------------------------------------------------