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