Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 276 lines (237 sloc) 10.038 kB
b0b2171 @apenwarr options.py: don't crash given semi-invalid optspecs.
authored
1 # Copyright 2010-2012 Avery Pennarun and options.py contributors.
8953dc8 @apenwarr options.py: relicense to 2-clause BSD license.
authored
2 # All rights reserved.
3 #
4 # (This license applies to this file but not necessarily the other files in
5 # this package.)
02bd2b5 @apenwarr options.py: get rid of end-of-line whitespace.
authored
6 #
8953dc8 @apenwarr options.py: relicense to 2-clause BSD license.
authored
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
02bd2b5 @apenwarr options.py: get rid of end-of-line whitespace.
authored
10 #
8953dc8 @apenwarr options.py: relicense to 2-clause BSD license.
authored
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
02bd2b5 @apenwarr options.py: get rid of end-of-line whitespace.
authored
13 #
8953dc8 @apenwarr options.py: relicense to 2-clause BSD license.
authored
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in
16 # the documentation and/or other materials provided with the
17 # distribution.
02bd2b5 @apenwarr options.py: get rid of end-of-line whitespace.
authored
18 #
8953dc8 @apenwarr options.py: relicense to 2-clause BSD license.
authored
19 # THIS SOFTWARE IS PROVIDED BY AVERY PENNARUN ``AS IS'' AND ANY
20 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
23 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 #
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
31 """Command-line options parser.
32 With the help of an options spec string, easily parse command-line options.
67087b3 @lelutin options.py: update docstrings and detail optspec
lelutin authored
33
34 An options spec is made up of two parts, separated by a line with two dashes.
35 The first part is the synopsis of the command and the second one specifies
36 options, one per line.
37
38 Each non-empty line in the synopsis gives a set of options that can be used
39 together.
40
41 Option flags must be at the begining of the line and multiple flags are
42 separated by commas. Usually, options have a short, one character flag, and a
43 longer one, but the short one can be omitted.
44
45 Long option flags are used as the option's key for the OptDict produced when
46 parsing options.
47
7f94324 @rlbdv Mention that option values will be converted to integers when possible.
rlbdv authored
48 When the flag definition is ended with an equal sign, the option takes
49 one string as an argument, and that string will be converted to an
50 integer when possible. Otherwise, the option does not take an argument
51 and corresponds to a boolean flag that is true when the option is
52 given on the command line.
67087b3 @lelutin options.py: update docstrings and detail optspec
lelutin authored
53
54 The option's description is found at the right of its flags definition, after
55 one or more spaces. The description ends at the end of the line. If the
56 description contains text enclosed in square brackets, the enclosed text will
57 be used as the option's default value.
58
59 Options can be put in different groups. Options in the same group must be on
60 consecutive lines. Groups are formed by inserting a line that begins with a
61 space. The text on that line will be output after an empty line.
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
62 """
4048d43 @apenwarr options.py: get the real tty width for word wrapping purposes.
authored
63 import sys, os, textwrap, getopt, re, struct
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
64
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
65
66 def _invert(v, invert):
67 if invert:
68 return not v
69 return v
70
71
72 def _remove_negative_kv(k, v):
73 if k.startswith('no-') or k.startswith('no_'):
74 return k[3:], not v
75 return k,v
76
77
78 class OptDict(object):
67087b3 @lelutin options.py: update docstrings and detail optspec
lelutin authored
79 """Dictionary that exposes keys as attributes.
80
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
81 Keys can be set or accessed with a "no-" or "no_" prefix to negate the
67087b3 @lelutin options.py: update docstrings and detail optspec
lelutin authored
82 value.
83 """
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
84 def __init__(self, aliases):
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
85 self._opts = {}
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
86 self._aliases = aliases
87
88 def _unalias(self, k):
89 k, reinvert = _remove_negative_kv(k, False)
90 k, invert = self._aliases[k]
91 return k, invert ^ reinvert
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
92
93 def __setitem__(self, k, v):
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
94 k, invert = self._unalias(k)
95 self._opts[k] = _invert(v, invert)
b0c1c31 options.py: differentiate unset and set-to-negative options.
Brandon Low authored
96
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
97 def __getitem__(self, k):
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
98 k, invert = self._unalias(k)
99 return _invert(self._opts[k], invert)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
100
101 def __getattr__(self, k):
102 return self[k]
103
104
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
105 def _default_onabort(msg):
106 sys.exit(97)
107
108
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
109 def _intify(v):
110 try:
111 vv = int(v or '')
112 if str(vv) == v:
113 return vv
114 except ValueError:
115 pass
116 return v
117
118
4048d43 @apenwarr options.py: get the real tty width for word wrapping purposes.
authored
119 def _atoi(v):
120 try:
121 return int(v or 0)
122 except ValueError:
123 return 0
124
125
126 def _tty_width():
127 s = struct.pack("HHHH", 0, 0, 0, 0)
128 try:
129 import fcntl, termios
130 s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
131 except (IOError, ImportError):
132 return _atoi(os.environ.get('WIDTH')) or 70
133 (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
241a33f @apenwarr options.py: don't die if tty width is set to 0.
authored
134 return xsize or 70
4048d43 @apenwarr options.py: get the real tty width for word wrapping purposes.
authored
135
136
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
137 class Options:
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
138 """Option parser.
67087b3 @lelutin options.py: update docstrings and detail optspec
lelutin authored
139 When constructed, a string called an option spec must be given. It
140 specifies the synopsis and option flags and their description. For more
141 information about option specs, see the docstring at the top of this file.
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
142
143 Two optional arguments specify an alternative parsing function and an
144 alternative behaviour on abort (after having output the usage string).
145
146 By default, the parser function is getopt.gnu_getopt, and the abort
147 behaviour is to exit the program.
148 """
581e9df @lelutin options: remove unused 'exe' parameter
lelutin authored
149 def __init__(self, optspec, optfunc=getopt.gnu_getopt,
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
150 onabort=_default_onabort):
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
151 self.optspec = optspec
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
152 self._onabort = onabort
f168a50 @apenwarr options: allow user to specify an alternative to getopt.gnu_getopt.
authored
153 self.optfunc = optfunc
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
154 self._aliases = {}
155 self._shortopts = 'h?'
c078168 @apenwarr options.py: make --usage just print the usage message.
authored
156 self._longopts = ['help', 'usage']
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
157 self._hasparms = {}
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
158 self._defaults = {}
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
159 self._usagestr = self._gen_usage() # this also parses the optspec
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
160
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
161 def _gen_usage(self):
162 out = []
163 lines = self.optspec.strip().split('\n')
164 lines.reverse()
165 first_syn = True
166 while lines:
167 l = lines.pop()
168 if l == '--': break
169 out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
170 first_syn = False
171 out.append('\n')
1959252 @apenwarr options.py: remove extra newlines in usage string.
authored
172 last_was_option = False
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
173 while lines:
174 l = lines.pop()
175 if l.startswith(' '):
1959252 @apenwarr options.py: remove extra newlines in usage string.
authored
176 out.append('%s%s\n' % (last_was_option and '\n' or '',
177 l.lstrip()))
178 last_was_option = False
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
179 elif l:
b0b2171 @apenwarr options.py: don't crash given semi-invalid optspecs.
authored
180 (flags,extra) = (l + ' ').split(' ', 1)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
181 extra = extra.strip()
182 if flags.endswith('='):
183 flags = flags[:-1]
184 has_parm = 1
185 else:
186 has_parm = 0
ef0bbbb @apenwarr options.py: handle optspecs that include inline square brackets.
authored
187 g = re.search(r'\[([^\]]*)\]$', extra)
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
188 if g:
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
189 defval = _intify(g.group(1))
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
190 else:
191 defval = None
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
192 flagl = flags.split(',')
193 flagl_nice = []
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
194 flag_main, invert_main = _remove_negative_kv(flagl[0], False)
195 self._defaults[flag_main] = _invert(defval, invert_main)
ec8c8dc @apenwarr options.py: generate usage string correctly for no-* options.
authored
196 for _f in flagl:
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
197 f,invert = _remove_negative_kv(_f, 0)
198 self._aliases[f] = (flag_main, invert_main ^ invert)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
199 self._hasparms[f] = has_parm
c899f42 @apenwarr options.py: add support for '-#' style compression options.
authored
200 if f == '#':
201 self._shortopts += '0123456789'
202 flagl_nice.append('-#')
203 elif len(f) == 1:
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
204 self._shortopts += f + (has_parm and ':' or '')
205 flagl_nice.append('-' + f)
206 else:
b0c1c31 options.py: differentiate unset and set-to-negative options.
Brandon Low authored
207 f_nice = re.sub(r'\W', '_', f)
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
208 self._aliases[f_nice] = (flag_main,
209 invert_main ^ invert)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
210 self._longopts.append(f + (has_parm and '=' or ''))
73c29d1 @apenwarr Automatically handle "--no-" prefix on long options.
authored
211 self._longopts.append('no-' + f)
ec8c8dc @apenwarr options.py: generate usage string correctly for no-* options.
authored
212 flagl_nice.append('--' + _f)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
213 flags_nice = ', '.join(flagl_nice)
214 if has_parm:
215 flags_nice += ' ...'
216 prefix = ' %-20s ' % flags_nice
4048d43 @apenwarr options.py: get the real tty width for word wrapping purposes.
authored
217 argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
218 initial_indent=prefix,
219 subsequent_indent=' '*28))
220 out.append(argtext + '\n')
1959252 @apenwarr options.py: remove extra newlines in usage string.
authored
221 last_was_option = True
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
222 else:
223 out.append('\n')
1959252 @apenwarr options.py: remove extra newlines in usage string.
authored
224 last_was_option = False
567f487 @apenwarr Add an options.fatal() function and use it.
authored
225 return ''.join(out).rstrip() + '\n'
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
226
227 def usage(self, msg=""):
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
228 """Print usage string to stderr and abort."""
f00d352 @apenwarr bup.options: remove reference to bup.helpers.
authored
229 sys.stderr.write(self._usagestr)
5a9bc0a @apenwarr options.py: o.fatal(): print error after, not before, usage message.
authored
230 if msg:
231 sys.stderr.write(msg)
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
232 e = self._onabort and self._onabort(msg) or None
233 if e:
234 raise e
567f487 @apenwarr Add an options.fatal() function and use it.
authored
235
5a9bc0a @apenwarr options.py: o.fatal(): print error after, not before, usage message.
authored
236 def fatal(self, msg):
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
237 """Print an error message to stderr and abort with usage string."""
5a9bc0a @apenwarr options.py: o.fatal(): print error after, not before, usage message.
authored
238 msg = '\nerror: %s\n' % msg
9a31a8c @lelutin lib/options: Add an onabort argument to Options()
lelutin authored
239 return self.usage(msg)
240
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
241 def parse(self, args):
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
242 """Parse a list of arguments and return (options, flags, extra).
243
244 In the returned tuple, "options" is an OptDict with known options,
245 "flags" is a list of option flags that were used on the command-line,
246 and "extra" is a list of positional arguments.
247 """
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
248 try:
f168a50 @apenwarr options: allow user to specify an alternative to getopt.gnu_getopt.
authored
249 (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
250 except getopt.GetoptError, e:
567f487 @apenwarr Add an options.fatal() function and use it.
authored
251 self.fatal(e)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
252
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
253 opt = OptDict(aliases=self._aliases)
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
254
255 for k,v in self._defaults.iteritems():
256 opt[k] = v
c76370f @lelutin lib/bup/options: Add docstrings
lelutin authored
257
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
258 for (k,v) in flags:
b0c1c31 options.py: differentiate unset and set-to-negative options.
Brandon Low authored
259 k = k.lstrip('-')
c078168 @apenwarr options.py: make --usage just print the usage message.
authored
260 if k in ('h', '?', 'help', 'usage'):
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
261 self.usage()
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
262 if (self._aliases.get('#') and
c899f42 @apenwarr options.py: add support for '-#' style compression options.
authored
263 k in ('0','1','2','3','4','5','6','7','8','9')):
264 v = int(k) # guaranteed to be exactly one digit
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
265 k, invert = self._aliases['#']
c899f42 @apenwarr options.py: add support for '-#' style compression options.
authored
266 opt['#'] = v
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
267 else:
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
268 k, invert = opt._unalias(k)
73c29d1 @apenwarr Automatically handle "--no-" prefix on long options.
authored
269 if not self._hasparms[k]:
270 assert(v == '')
b0c1c31 options.py: differentiate unset and set-to-negative options.
Brandon Low authored
271 v = (opt._opts.get(k) or 0) + 1
73c29d1 @apenwarr Automatically handle "--no-" prefix on long options.
authored
272 else:
a57b472 @apenwarr options.py: support for putting default values in [square brackets].
authored
273 v = _intify(v)
a40b7d7 @apenwarr options.py: clean up handling of --no-* options.
authored
274 opt[k] = _invert(v, invert)
52632e3 @apenwarr Completely revamped option parsing based on git-shell-style.
authored
275 return (opt,flags,extra)
Something went wrong with that request. Please try again.