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
|
########################## LICENCE ###############################
##
## Copyright (c) 2010, Michele Simionato
## All rights reserved.
##
## Redistributions of source code must retain the above copyright
## notice, this list of conditions and the following disclaimer.
## Redistributions in bytecode form must reproduce the above copyright
## notice, this list of conditions and the following disclaimer in
## the documentation and/or other materials provided with the
## distribution.
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
## DAMAGE.
"""
CLAP, the smart and simple Command Line Arguments Parser.
See clap/doc.html for the documentation.
"""
import optparse, re, sys, string
class RegexContainer(object):
"""
A regular expression container for named regexps.
All its regular attributes must be valid regular expression templates.
Notice that the termination character $ must be doubled in
order to avoid confusion with the template syntax.
"""
def __init__(self):
self._dic = {} # a dictionary {name: interpolated regex pattern}
def __setattr__(self, name, value):
if not name.startswith('_'): # regular attribute
templ = string.Template('(?P<' + name + '>' + value + ')')
pattern = templ.substitute(self._dic)
object.__setattr__(self, name, re.compile(pattern))
self._dic[name] = pattern
else: # private or special attribute
object.__setattr__(self, name, value)
rx = RegexContainer() # a few regular expressions to parse the usage string
rx.argument = r'[a-zA-Z]\w*'
rx.arguments = '(?:\s+$argument)*'
rx.ellipsis = r'\.\.\.'
rx.prog = r'%prog$arguments\s*$ellipsis?'
rx.enddef = r'\n[ \t]*\n|$$'
rx.lines = r'.*?'
rx.short = r'\w'
rx.long = r'[-\w]+'
rx.default = r'[^:]*'
rx.help = r'.*'
rx.usage = r'(?is)\s*usage:$lines$enddef' # case-insensitive multiline rx
rx.optiondef = r'\s*-$short\s*,\s*--$long\s*=\s*$default:\s*$help'
rx.flagdef = r'\s*-$short\s*,\s*--$long\s*:\s*$help'
def parse_usage(txt):
"An utility to extract the expected arguments and the rest argument, if any"
match = rx.prog.match(txt)
if match is None:
ParsingError.raise_(txt)
expected_args = match.group('arguments').split()
if match.group('ellipsis'):
return expected_args[:-1], match.group('argument')
else:
return expected_args, ''
class ParsingError(Exception):
@classmethod
def raise_(cls, usage):
raise cls("""Wrong format for the usage message.\n\n%s\n
It should be '%%prog arguments ... [options]""" % usage)
def make_get_default_values(defaults):
# the purpose of this trick is to allow the idiom
# if not arg: OptionParser.exit()
def __nonzero__(self):
"True if at least one option is set to a non-trivial value"
for k, v in vars(self).iteritems():
if v and v != defaults[k]: return True
return False
Values = type('Values', (optparse.Values, object),
dict(__nonzero__=__nonzero__))
return lambda : Values(defaults)
optionstring = None # singleton
class OptionParser(object):
"""
There should be only one instance of it.
Attributes: all_options, expected_args, rest_arg, p
"""
def __init__(self, doc):
"Populate the option parser."
global optionstring
assert doc is not None, \
"Missing usage string (maybe __doc__ is None)"
optionstring = doc.replace('%prog', sys.argv[0])
# parse the doc
match = rx.usage.search(doc)
if not match:
raise ParsingError("Could not find the option definitions")
optlines = match.group("lines").splitlines()
prog = optlines[0] # first line
match = rx.prog.search(prog)
if not match:
ParsingError.raise_(prog)
self.expected_args, self.rest_arg = parse_usage(match.group())
self.p = optparse.OptionParser(prog)
# manage the default values
df = self.p.defaults
for a in self.expected_args:
df[a] = None
if self.rest_arg:
df[self.rest_arg] = []
self.p.get_default_values = make_get_default_values(df)
# parse the options
for line in optlines[1:]:
# check if the line is an option definition
match_option = rx.optiondef.match(line)
if match_option:
action = 'store'
short, long_, help, default=match_option.group(
"short", "long", "help", "default")
else: # check if the line is a flag definition
match_flag = rx.flagdef.match(line)
if match_flag:
action = 'store_true'
short, long_, help, default=match_flag.group(
"short", "long", "help") + (False,)
else: # cannot parse the definition correctly
continue
# add the options
long_ = long_.replace('-', '_')
self.p.add_option("-" + short, "--" + long_,
action=action, help=help, default=default)
# skip the help option, which destination is None
self.all_options = [o for o in self.p.option_list if o.dest]
def parse_args(self, arglist=None):
"""
Parse the received arguments and returns an ``optparse.Values``
object containing both the options and the positional arguments.
"""
option, args = self.p.parse_args(arglist)
n_expected_args = len(self.expected_args)
n_received_args = len(args)
if (n_received_args < n_expected_args) or (
n_received_args > n_expected_args and not self.rest_arg):
raise ParsingError(
'Received %d arguments %s, expected %d %s' %
(n_received_args, args, n_expected_args, self.expected_args))
for name, value in zip(self.expected_args, args):
setattr(option, name, value)
if self.rest_arg:
setattr(option, self.rest_arg, args[n_expected_args:])
return option
@classmethod
def exit(cls, msg=None):
exit(msg)
def call(func, args=None, doc=None):
"""
Magically calls func by passing to it the command lines arguments,
parsed according to the docstring of func.
"""
if args is None:
args = sys.argv[1:]
if doc is None:
doc = func.__doc__
try:
arg = OptionParser(doc).parse_args(args)
except ParsingError, e:
print 'ParsingError:', e
OptionParser.exit()
return func(**vars(arg))
def exit(msg=None):
if msg is None:
msg = optionstring
raise SystemExit(msg)
|