]> arthur.barton.de Git - bup.git/blob - lib/bup/options.py
Use absolute_import from the __future__ everywhere
[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 ``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 #
31 """Command-line options parser.
32 With the help of an options spec string, easily parse command-line options.
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
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.
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.
62 """
63
64 from __future__ import absolute_import
65 import sys, os, textwrap, getopt, re, struct
66
67 try:
68     import fcntl
69 except ImportError:
70     fcntl = None
71
72 try:
73     import termios
74 except ImportError:
75     termios = None
76
77
78 def _invert(v, invert):
79     if invert:
80         return not v
81     return v
82
83
84 def _remove_negative_kv(k, v):
85     if k.startswith('no-') or k.startswith('no_'):
86         return k[3:], not v
87     return k,v
88
89
90 class OptDict(object):
91     """Dictionary that exposes keys as attributes.
92
93     Keys can be set or accessed with a "no-" or "no_" prefix to negate the
94     value.
95     """
96     def __init__(self, aliases):
97         self._opts = {}
98         self._aliases = aliases
99
100     def _unalias(self, k):
101         k, reinvert = _remove_negative_kv(k, False)
102         k, invert = self._aliases[k]
103         return k, invert ^ reinvert
104
105     def __setitem__(self, k, v):
106         k, invert = self._unalias(k)
107         self._opts[k] = _invert(v, invert)
108
109     def __getitem__(self, k):
110         k, invert = self._unalias(k)
111         return _invert(self._opts[k], invert)
112
113     def __getattr__(self, k):
114         return self[k]
115
116
117 def _default_onabort(msg):
118     sys.exit(97)
119
120
121 def _intify(v):
122     try:
123         vv = int(v or '')
124         if str(vv) == v:
125             return vv
126     except ValueError:
127         pass
128     return v
129
130
131 def _atoi(v):
132     try:
133         return int(v or 0)
134     except ValueError:
135         return 0
136
137
138 if not fcntl and termios:
139     def _tty_width():
140         return 70
141 else:
142     def _tty_width():
143         s = struct.pack("HHHH", 0, 0, 0, 0)
144         try:
145             s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
146         except IOError:
147             return 70
148         ysize, xsize, ypix, xpix = struct.unpack('HHHH', s)
149         return xsize or 70
150
151
152 class Options:
153     """Option parser.
154     When constructed, a string called an option spec must be given. It
155     specifies the synopsis and option flags and their description.  For more
156     information about option specs, see the docstring at the top of this file.
157
158     Two optional arguments specify an alternative parsing function and an
159     alternative behaviour on abort (after having output the usage string).
160
161     By default, the parser function is getopt.gnu_getopt, and the abort
162     behaviour is to exit the program.
163     """
164     def __init__(self, optspec, optfunc=getopt.gnu_getopt,
165                  onabort=_default_onabort):
166         self.optspec = optspec
167         self._onabort = onabort
168         self.optfunc = optfunc
169         self._aliases = {}
170         self._shortopts = 'h?'
171         self._longopts = ['help', 'usage']
172         self._hasparms = {}
173         self._defaults = {}
174         self._usagestr = self._gen_usage()  # this also parses the optspec
175
176     def _gen_usage(self):
177         out = []
178         lines = self.optspec.strip().split('\n')
179         lines.reverse()
180         first_syn = True
181         while lines:
182             l = lines.pop()
183             if l == '--': break
184             out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
185             first_syn = False
186         out.append('\n')
187         last_was_option = False
188         while lines:
189             l = lines.pop()
190             if l.startswith(' '):
191                 out.append('%s%s\n' % (last_was_option and '\n' or '',
192                                        l.lstrip()))
193                 last_was_option = False
194             elif l:
195                 (flags,extra) = (l + ' ').split(' ', 1)
196                 extra = extra.strip()
197                 if flags.endswith('='):
198                     flags = flags[:-1]
199                     has_parm = 1
200                 else:
201                     has_parm = 0
202                 g = re.search(r'\[([^\]]*)\]$', extra)
203                 if g:
204                     defval = _intify(g.group(1))
205                 else:
206                     defval = None
207                 flagl = flags.split(',')
208                 flagl_nice = []
209                 flag_main, invert_main = _remove_negative_kv(flagl[0], False)
210                 self._defaults[flag_main] = _invert(defval, invert_main)
211                 for _f in flagl:
212                     f,invert = _remove_negative_kv(_f, 0)
213                     self._aliases[f] = (flag_main, invert_main ^ invert)
214                     self._hasparms[f] = has_parm
215                     if f == '#':
216                         self._shortopts += '0123456789'
217                         flagl_nice.append('-#')
218                     elif len(f) == 1:
219                         self._shortopts += f + (has_parm and ':' or '')
220                         flagl_nice.append('-' + f)
221                     else:
222                         f_nice = re.sub(r'\W', '_', f)
223                         self._aliases[f_nice] = (flag_main,
224                                                  invert_main ^ invert)
225                         self._longopts.append(f + (has_parm and '=' or ''))
226                         self._longopts.append('no-' + f)
227                         flagl_nice.append('--' + _f)
228                 flags_nice = ', '.join(flagl_nice)
229                 if has_parm:
230                     flags_nice += ' ...'
231                 prefix = '    %-20s  ' % flags_nice
232                 argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
233                                                 initial_indent=prefix,
234                                                 subsequent_indent=' '*28))
235                 out.append(argtext + '\n')
236                 last_was_option = True
237             else:
238                 out.append('\n')
239                 last_was_option = False
240         return ''.join(out).rstrip() + '\n'
241
242     def usage(self, msg=""):
243         """Print usage string to stderr and abort."""
244         sys.stderr.write(self._usagestr)
245         if msg:
246             sys.stderr.write(msg)
247         e = self._onabort and self._onabort(msg) or None
248         if e:
249             raise e
250
251     def fatal(self, msg):
252         """Print an error message to stderr and abort with usage string."""
253         msg = '\nerror: %s\n' % msg
254         return self.usage(msg)
255
256     def parse(self, args):
257         """Parse a list of arguments and return (options, flags, extra).
258
259         In the returned tuple, "options" is an OptDict with known options,
260         "flags" is a list of option flags that were used on the command-line,
261         and "extra" is a list of positional arguments.
262         """
263         try:
264             (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
265         except getopt.GetoptError as e:
266             self.fatal(e)
267
268         opt = OptDict(aliases=self._aliases)
269
270         for k,v in self._defaults.items():
271             opt[k] = v
272
273         for (k,v) in flags:
274             k = k.lstrip('-')
275             if k in ('h', '?', 'help', 'usage'):
276                 self.usage()
277             if (self._aliases.get('#') and
278                   k in ('0','1','2','3','4','5','6','7','8','9')):
279                 v = int(k)  # guaranteed to be exactly one digit
280                 k, invert = self._aliases['#']
281                 opt['#'] = v
282             else:
283                 k, invert = opt._unalias(k)
284                 if not self._hasparms[k]:
285                     assert(v == '')
286                     v = (opt._opts.get(k) or 0) + 1
287                 else:
288                     v = _intify(v)
289             opt[k] = _invert(v, invert)
290         return (opt,flags,extra)