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