]> arthur.barton.de Git - bup.git/blob - lib/bup/options.py
get: adjust for python 3 and test there
[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 if not fcntl and termios:
133     def _tty_width():
134         return 70
135 else:
136     def _tty_width():
137         s = struct.pack("HHHH", 0, 0, 0, 0)
138         try:
139             s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
140         except IOError:
141             return 70
142         ysize, xsize, ypix, xpix = struct.unpack('HHHH', s)
143         return xsize or 70
144
145
146 class Options:
147     """Option parser.
148     When constructed, a string called an option spec must be given. It
149     specifies the synopsis and option flags and their description.  For more
150     information about option specs, see the docstring at the top of this file.
151
152     Two optional arguments specify an alternative parsing function and an
153     alternative behaviour on abort (after having output the usage string).
154
155     By default, the parser function is getopt.gnu_getopt, and the abort
156     behaviour is to exit the program.
157     """
158     def __init__(self, optspec, optfunc=getopt.gnu_getopt,
159                  onabort=_default_onabort):
160         self.optspec = optspec
161         self._onabort = onabort
162         self.optfunc = optfunc
163         self._aliases = {}
164         self._shortopts = 'h?'
165         self._longopts = ['help', 'usage']
166         self._hasparms = {}
167         self._defaults = {}
168         self._usagestr = self._gen_usage()  # this also parses the optspec
169
170     def _gen_usage(self):
171         out = []
172         lines = self.optspec.strip().split('\n')
173         lines.reverse()
174         first_syn = True
175         while lines:
176             l = lines.pop()
177             if l == '--': break
178             out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
179             first_syn = False
180         out.append('\n')
181         last_was_option = False
182         while lines:
183             l = lines.pop()
184             if l.startswith(' '):
185                 out.append('%s%s\n' % (last_was_option and '\n' or '',
186                                        l.lstrip()))
187                 last_was_option = False
188             elif l:
189                 (flags,extra) = (l + ' ').split(' ', 1)
190                 extra = extra.strip()
191                 if flags.endswith('='):
192                     flags = flags[:-1]
193                     has_parm = 1
194                 else:
195                     has_parm = 0
196                 g = re.search(r'\[([^\]]*)\]$', extra)
197                 if g:
198                     defval = _intify(g.group(1))
199                 else:
200                     defval = None
201                 flagl = flags.split(',')
202                 flagl_nice = []
203                 flag_main, invert_main = _remove_negative_kv(flagl[0], False)
204                 self._defaults[flag_main] = _invert(defval, invert_main)
205                 for _f in flagl:
206                     f,invert = _remove_negative_kv(_f, 0)
207                     self._aliases[f] = (flag_main, invert_main ^ invert)
208                     self._hasparms[f] = has_parm
209                     if f == '#':
210                         self._shortopts += '0123456789'
211                         flagl_nice.append('-#')
212                     elif len(f) == 1:
213                         self._shortopts += f + (has_parm and ':' or '')
214                         flagl_nice.append('-' + f)
215                     else:
216                         f_nice = re.sub(r'\W', '_', f)
217                         self._aliases[f_nice] = (flag_main,
218                                                  invert_main ^ invert)
219                         self._longopts.append(f + (has_parm and '=' or ''))
220                         self._longopts.append('no-' + f)
221                         flagl_nice.append('--' + _f)
222                 flags_nice = ', '.join(flagl_nice)
223                 if has_parm:
224                     flags_nice += ' ...'
225                 prefix = '    %-20s  ' % flags_nice
226                 argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
227                                                 initial_indent=prefix,
228                                                 subsequent_indent=' '*28))
229                 out.append(argtext + '\n')
230                 last_was_option = True
231             else:
232                 out.append('\n')
233                 last_was_option = False
234         return ''.join(out).rstrip() + '\n'
235
236     def usage(self, msg=""):
237         """Print usage string to stderr and abort."""
238         sys.stderr.write(self._usagestr)
239         if msg:
240             sys.stderr.write(msg)
241         e = self._onabort and self._onabort(msg) or None
242         if e:
243             raise e
244
245     def fatal(self, msg):
246         """Print an error message to stderr and abort with usage string."""
247         msg = '\nerror: %s\n' % msg
248         return self.usage(msg)
249
250     def parse(self, args):
251         """Parse a list of arguments and return (options, flags, extra).
252
253         In the returned tuple, "options" is an OptDict with known options,
254         "flags" is a list of option flags that were used on the command-line,
255         and "extra" is a list of positional arguments.
256         """
257         try:
258             (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
259         except getopt.GetoptError as e:
260             self.fatal(e)
261
262         opt = OptDict(aliases=self._aliases)
263
264         for k,v in self._defaults.items():
265             opt[k] = v
266
267         for (k,v) in flags:
268             k = k.lstrip('-')
269             if k in ('h', '?', 'help', 'usage'):
270                 self.usage()
271             if (self._aliases.get('#') and
272                   k in ('0','1','2','3','4','5','6','7','8','9')):
273                 v = int(k)  # guaranteed to be exactly one digit
274                 k, invert = self._aliases['#']
275                 opt['#'] = v
276             else:
277                 k, invert = opt._unalias(k)
278                 if not self._hasparms[k]:
279                     assert(v == '')
280                     v = (opt._opts.get(k) or 0) + 1
281                 else:
282                     v = _intify(v)
283             opt[k] = _invert(v, invert)
284         return (opt,flags,extra)