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