1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """
17 The module contains routines to parse command arguments and map them to
18 the command handler's positonal and keyword arguments.
19
20 Mapping is done in two stages: 1) parse arguments into positional
21 arguments and options; 2) adapt them to the specific command handler
22 according to the command properties.
23 """
24
25 import re
26 from types import BooleanType, UnicodeType
27 from operator import itemgetter
28
29 from errors import DefinitionError, CommandError
30
31
32
33 ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
34 OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
35
36
37
38
39 KEY_ENCODING = 'UTF-8'
40
41
42
43 USAGE_PATTERN = 'Usage: %s %s'
44
46 """
47 Simple yet effective and sufficient in most cases parser which
48 parses command arguments and returns them as two lists.
49
50 First list represents positional arguments as (argument, position),
51 and second representing options as (key, value, position) tuples,
52 where position is a (start, end) span tuple of where it was found in
53 the string.
54
55 Options may be given in --long or -short format. As --option=value
56 or --option value or -option value. Keys without values will get
57 None as value.
58
59 Arguments and option values that contain spaces may be given as 'one
60 two three' or "one two three"; that is between single or double
61 quotes.
62 """
63 args, opts = [], []
64
65 def intersects_opts((given_start, given_end)):
66 """
67 Check if given span intersects with any of options.
68 """
69 for key, value, (start, end) in opts:
70 if given_start >= start and given_end <= end:
71 return True
72 return False
73
74 def intersects_args((given_start, given_end)):
75 """
76 Check if given span intersects with any of arguments.
77 """
78 for arg, (start, end) in args:
79 if given_start >= start and given_end <= end:
80 return True
81 return False
82
83 for match in re.finditer(OPT_PATTERN, arguments):
84 if match:
85 key = match.group('key')
86 value = match.group('value') or None
87 position = match.span()
88 opts.append((key, value, position))
89
90 for match in re.finditer(ARG_PATTERN, arguments):
91 if match:
92 body = match.group('body')
93 position = match.span()
94 args.append((body, position))
95
96
97
98
99 for arg, position in args[:]:
100 if intersects_opts(position):
101 args.remove((arg, position))
102
103
104
105
106 for key, value, position in opts[:]:
107 if intersects_args(position):
108 opts.remove((key, value, position))
109
110 return args, opts
111
113 """
114 Adapt args and opts got from the parser to a specific handler by
115 means of arguments specified on command definition. That is
116 transform them to *args and **kwargs suitable for passing to a
117 command handler.
118
119 Dashes (-) in the option names will be converted to underscores. So
120 you can map --one-more-option to a one_more_option=None.
121
122 If the initial value of a keyword argument is a boolean (False in
123 most cases) - then this option will be treated as a switch, that is
124 an option which does not take an argument. If a switch is followed
125 by an argument - then this argument will be treated just like a
126 normal positional argument.
127 """
128 spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
129 norm_kwargs = dict(spec_kwargs)
130
131
132
133
134
135
136
137 if command.raw:
138 if arguments:
139 spec_fix = 1 if command.source else 0
140 spec_len = len(spec_args) - spec_fix
141 arguments_end = len(arguments) - 1
142
143
144
145
146
147
148 for key, value, (start, end) in opts[:spec_len]:
149 if value:
150 end -= len(value) + 1
151 args.append((arguments[start:end], (start, end)))
152 args.append((value, (end, end + len(value) + 1)))
153 else:
154 args.append((arguments[start:end], (start, end)))
155
156
157
158
159 args.sort(key=itemgetter(1))
160
161 if spec_len > 1:
162 try:
163 stopper, (start, end) = args[spec_len - 2]
164 except IndexError:
165 raise CommandError("Missing arguments", command)
166
167
168
169
170
171 raw = arguments[end:]
172 raw = raw.strip() or None
173
174 if not raw and not command.empty:
175 raise CommandError("Missing arguments", command)
176
177
178
179
180 args = args[:spec_len - 1]
181 opts = []
182
183 args.append((raw, (end, arguments_end)))
184 else:
185
186
187
188
189 args = [(arguments, (0, arguments_end))]
190 opts = []
191 else:
192 if command.empty:
193 args.append((None, (0, 0)))
194 else:
195 raise CommandError("Missing arguments", command)
196
197
198
199
200
201 for index, (key, value, position) in enumerate(opts):
202 if '-' in key:
203 opts[index] = (key.replace('-', '_'), value, position)
204
205
206
207
208 if command.expand:
209 expanded = []
210 for spec_key, spec_value in norm_kwargs.iteritems():
211 letter = spec_key[0] if len(spec_key) > 1 else None
212 if letter and letter not in expanded:
213 for index, (key, value, position) in enumerate(opts):
214 if key == letter:
215 expanded.append(letter)
216 opts[index] = (spec_key, value, position)
217 break
218
219
220
221 for index, (key, value, position) in enumerate(opts):
222 if isinstance(norm_kwargs.get(key), BooleanType):
223 opts[index] = (key, True, position)
224 if value:
225 args.append((value, position))
226
227
228
229 args.sort(key=itemgetter(1))
230 opts.sort(key=itemgetter(2))
231
232
233
234 args = map(lambda (arg, position): arg, args)
235 opts = map(lambda (key, value, position): (key, value), opts)
236
237
238
239
240 if command.extra:
241 if not var_args:
242 spec_fix = 1 if not command.source else 2
243 spec_len = len(spec_args) - spec_fix
244 extra = args[spec_len:]
245 args = args[:spec_len]
246 args.append(extra)
247 else:
248 raise DefinitionError("Can not have both, extra and *args")
249
250
251
252
253 spec_fix = 1 if command.source else 0
254 spec_len = len(spec_args) - spec_fix
255 if len(args) > spec_len:
256 if command.overlap:
257 overlapped = args[spec_len:]
258 args = args[:spec_len]
259 for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs):
260 opts.append((spec_key, arg))
261 else:
262 raise CommandError("Excessive arguments", command)
263
264
265
266 for key, value in opts:
267 initial = norm_kwargs.get(key)
268 if isinstance(initial, BooleanType):
269 if not isinstance(value, BooleanType):
270 raise CommandError("%s: Switch can not take an argument" % key, command)
271
272
273
274 for index, (key, value) in enumerate(opts):
275 if isinstance(key, UnicodeType):
276 opts[index] = (key.encode(KEY_ENCODING), value)
277
278
279
280 if command.source:
281 args.insert(0, arguments)
282
283
284
285 return tuple(args), dict(opts)
286
288 """
289 Extract handler's arguments specification and wrap them in a
290 human-readable format usage information. If complete is given - then
291 USAGE_PATTERN will be used to render the specification completly.
292 """
293 spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
294
295
296
297
298 sp_source = spec_args.pop(0) if command.source else None
299 sp_extra = spec_args.pop() if command.extra else None
300
301 kwargs = []
302 letters = []
303
304 for key, value in spec_kwargs:
305 letter = key[0]
306 key = key.replace('_', '-')
307
308 if isinstance(value, BooleanType):
309 value = str()
310 else:
311 value = '=%s' % value
312
313 if letter not in letters:
314 kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
315 letters.append(letter)
316 else:
317 kwargs.append('--%s%s' % (key, value))
318
319 usage = str()
320 args = str()
321
322 if command.raw:
323 spec_len = len(spec_args) - 1
324 if spec_len:
325 args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
326 args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
327 else:
328 if spec_args:
329 args += '<%s>' % ', '.join(spec_args)
330 if var_args or sp_extra:
331 args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra)
332
333 usage += args
334
335 if kwargs or var_kwargs:
336 if kwargs:
337 usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
338 if var_kwargs:
339 usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
340
341
342
343 if len(command.names) > 1:
344 names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
345 else:
346 names = command.first_name
347
348 return USAGE_PATTERN % (names, usage) if complete else usage
349