Mercurial > hg > CommandParser
annotate commandparser/command.py @ 11:03db23600c1f
bump version
author | Jeff Hammel <jhammel@mozilla.com> |
---|---|
date | Mon, 02 Apr 2012 10:42:13 -0700 |
parents | a3599e4db9a4 |
children | e0a3148e67a8 |
rev | line source |
---|---|
1 | 1 """ |
2 a command-line interface to the command line, a la pythonpaste | |
3 """ | |
4 | |
5 import inspect | |
6 import os | |
7 import sys | |
8 from optparse import OptionParser | |
9 from pprint import pprint | |
10 | |
9 | 11 try: |
12 import json | |
13 except ImportError: | |
14 import simplejson as json | |
15 | |
10 | 16 # BBB python 2.4 |
17 cleandoc = getattr(inspect, 'cleandoc', lambda x: x.strip()) | |
18 | |
2
d36032625794
add __all__ and whitespace cleanup
Jeff Hammel <jhammel@mozilla.com>
parents:
1
diff
changeset
|
19 __all__ = ['Undefined', 'CommandParser'] |
d36032625794
add __all__ and whitespace cleanup
Jeff Hammel <jhammel@mozilla.com>
parents:
1
diff
changeset
|
20 |
1 | 21 class Undefined(object): |
9 | 22 def __init__(self, default): |
23 self.default=default | |
1 | 24 |
25 class CommandParser(OptionParser): | |
26 # TODO: add `help` command | |
27 | |
28 def __init__(self, _class, description=None): | |
29 self._class = _class | |
30 self.commands = {} | |
8
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
31 init = self.command(_class.__init__) |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
32 self.init_args = init['args'] |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
33 command_str = ' '.join(self.init_args + ['command']) |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
34 usage = '%prog [options]' + ' %s [command-options]' % (command_str) |
1 | 35 description = description or _class.__doc__ |
36 OptionParser.__init__(self, usage=usage, description=description) | |
37 commands = [ getattr(_class, i) for i in dir(_class) | |
38 if not i.startswith('_') ] | |
39 commands = [ method for method in commands | |
40 if hasattr(method, '__call__') ] | |
41 for _command in commands: | |
42 c = self.command(_command) | |
43 self.commands[c['name']] = c | |
44 | |
45 # get class options | |
46 self.command2parser(init, self) | |
47 self.disable_interspersed_args() | |
48 | |
49 def add_option(self, *args, **kwargs): | |
50 kwargs['default'] = Undefined(kwargs.get('default')) | |
51 OptionParser.add_option(self, *args, **kwargs) | |
52 | |
53 def print_help(self): | |
3 | 54 |
1 | 55 OptionParser.print_help(self) |
3 | 56 |
1 | 57 # short descriptions for commands |
58 command_descriptions = [dict(name=i, | |
59 description=self.commands[i]['doc'].strip().split('\n',1)[0]) | |
60 for i in self.commands.keys()] | |
61 command_descriptions.append(dict(name='help', description='print help for a given command')) | |
62 command_descriptions.sort(key=lambda x: x['name']) | |
63 max_len = max([len(i['name']) for i in command_descriptions]) | |
64 description = "Commands: \n%s" % ('\n'.join([' %s%s %s' % (description['name'], ' ' * (max_len - len(description['name'])), description['description']) | |
65 for description in command_descriptions])) | |
66 | |
67 print | |
68 print description | |
69 | |
70 def parse(self, args=sys.argv[1:]): | |
71 """global parse step""" | |
3 | 72 |
1 | 73 self.options, args = self.parse_args(args) |
74 | |
75 # help/sanity check -- should probably be separated | |
76 if not len(args): | |
77 self.print_help() | |
78 sys.exit(0) | |
79 if args[0] == 'help': | |
80 if len(args) == 2: | |
81 if args[1] == 'help': | |
82 self.print_help() | |
83 elif args[1] in self.commands: | |
84 name = args[1] | |
85 commandparser = self.command2parser(name) | |
86 commandparser.print_help() | |
87 else: | |
88 self.error("No command '%s'" % args[1]) | |
89 else: | |
90 self.print_help() | |
91 sys.exit(0) | |
8
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
92 required = len(self.init_args) + 1 # command |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
93 if len(args) < required: |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
94 self.print_usage() |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
95 sys.exit(1) |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
96 self.command_args = args[:len(self.init_args)] |
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
97 args = args[len(self.init_args):] |
1 | 98 command = args[0] |
99 if command not in self.commands: | |
100 self.error("No command '%s'" % command) | |
101 return command, args[1:] | |
102 | |
103 def invoke(self, args=sys.argv[1:]): | |
104 """ | |
105 invoke | |
106 """ | |
107 | |
108 # parse | |
8
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
109 name, args = self.parse(args) |
1 | 110 |
111 # setup | |
112 options = {} | |
113 dotfile = os.path.join(os.environ['HOME'], '.' + self.get_prog_name()) | |
114 if os.path.exists(dotfile): | |
115 f = file(dotfile) | |
116 for line in f.readlines(): | |
117 line = line.strip() | |
118 if not line: | |
119 continue | |
120 if ':' in line: | |
121 key, value = [i.strip() | |
122 for i in line.split(':', 1)] | |
123 options[key] = value | |
124 else: | |
125 print >> sys.stderr, "Bad option line: " + line | |
126 for key, value in self.options.__dict__.items(): | |
127 if isinstance(value, Undefined): | |
128 if key in options: | |
129 continue | |
130 options[key] = value.default | |
131 else: | |
132 options[key] = value | |
8
109627b7db9f
add ability to have mandatory class arguments
Jeff Hammel <jhammel@mozilla.com>
parents:
5
diff
changeset
|
133 _object = self._class(*self.command_args, **options) |
1 | 134 |
135 # command specific args | |
136 command = self.commands[name] | |
137 commandparser = self.command2parser(name) | |
138 command_options, command_args = commandparser.parse_args(args) | |
139 if len(command_args) < len(command['args']): | |
140 commandparser.error("Not enough arguments given") | |
141 if len(command_args) != len(command['args']) and not command['varargs']: | |
142 commandparser.error("Too many arguments given") | |
143 | |
144 # invoke the command | |
145 retval = getattr(_object, name)(*command_args, **command_options.__dict__) | |
146 if isinstance(retval, basestring): | |
147 print retval | |
148 elif retval is None: | |
149 pass | |
150 elif isinstance(retval, list): | |
151 for i in retval: | |
152 print i | |
153 elif isinstance(retval, dict): | |
154 try: | |
155 print json.dumps(retval, indent=2, sort_keys=True) | |
156 except: | |
157 pprint(retval) | |
158 else: | |
159 pprint(retval) | |
160 return retval | |
161 | |
162 def command(self, function): | |
163 name = function.func_name | |
164 if function.__doc__: | |
10 | 165 doc = cleandoc(function.__doc__) |
1 | 166 else: |
167 doc = '' | |
10 | 168 args, varargs, varkw, defaults = inspect.getargspec(function) |
1 | 169 if defaults: |
10 | 170 args = args[1:-len(defaults)] |
171 optional = dict(zip(args[-len(defaults):], defaults)) | |
1 | 172 else: |
10 | 173 args = args[1:] |
1 | 174 optional = None |
5 | 175 command = {'doc': doc, |
176 'name': name, | |
177 'args': args, | |
178 'optional': optional, | |
10 | 179 'varargs': varargs |
5 | 180 } |
1 | 181 return command |
182 | |
183 def commandargs2str(self, command): | |
184 if isinstance(command, basestring): | |
185 command = self.commands[command] | |
186 retval = [] | |
187 retval.extend(['<%s>' % arg for arg in command['args']]) | |
188 varargs = command['varargs'] | |
189 if varargs: | |
190 retval.append('<%s> [%s] [...]' % (varargs, varargs)) | |
191 if command['optional']: | |
192 retval.append('[options]') | |
193 return ' '.join(retval) | |
194 | |
195 def doc2arghelp(self, docstring, decoration='-', delimeter=':'): | |
196 """ | |
197 Parse a docstring and get at the section describing arguments | |
198 - decoration: decoration character | |
199 - delimeter: delimter character | |
3 | 200 |
1 | 201 Yields a tuple of the stripped docstring and the arguments help |
202 dictionary | |
203 """ | |
204 lines = [ i.strip() for i in docstring.split('\n') ] | |
205 argdict = {} | |
206 doc = [] | |
207 option = None | |
208 for line in lines: | |
209 if not line and option: # blank lines terminate [?] | |
210 break | |
211 if line.startswith(decoration) and delimeter in line: | |
212 name, description = line.split(delimeter, 1) | |
213 name = name.lstrip(decoration).strip() | |
214 description = description.strip() | |
215 argdict[name] = [ description ] | |
216 option = name | |
217 else: | |
218 if option: | |
219 argdict[name].append(line) | |
220 else: | |
221 doc.append(line) | |
222 argdict = dict([(key, ' '.join(value)) | |
223 for key, value in argdict.items()]) | |
224 return ('\n'.join(doc), argdict) | |
225 | |
226 def command2parser(self, command, parser=None): | |
227 if isinstance(command, basestring): | |
228 command = self.commands[command] | |
229 doc, argdict = self.doc2arghelp(command['doc']) | |
230 if parser is None: | |
231 parser = OptionParser('%%prog %s %s' % (command['name'], self.commandargs2str(command)), | |
232 description=doc, add_help_option=False) | |
233 if command['optional']: | |
234 for key, value in command['optional'].items(): | |
235 help = argdict.get(key, '') | |
236 if value is True: | |
237 parser.add_option('--no-%s' % key, dest=key, | |
238 action='store_false', default=True, | |
239 help=help) | |
240 elif value is False: | |
241 parser.add_option('--%s' % key, action='store_true', | |
242 default=False, help=help) | |
4 | 243 elif isinstance(value, int): |
244 help += ' [DEFAULT: %s]' % value | |
245 parser.add_option('--%s' % key, help=help, | |
246 type='int', default=value) | |
1 | 247 elif type(value) in set([type(()), type([])]): |
248 if value: | |
249 help += ' [DEFAULT: %s]' % value | |
250 parser.add_option('--%s' % key, action='append', | |
251 default=list(value), | |
252 help=help) | |
253 else: | |
254 if value is not None: | |
255 help += ' [DEFAULT: %s]' % value | |
256 parser.add_option('--%s' % key, help=help, default=value) | |
3 | 257 |
1 | 258 return parser |