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