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