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