]> arthur.barton.de Git - bup.git/blob - lib/bup/options.py
Merge remote branch 'origin/master' into meta
[bup.git] / lib / bup / options.py
1 """Command-line options parser.
2 With the help of an options spec string, easily parse command-line options.
3
4 An options spec is made up of two parts, separated by a line with two dashes.
5 The first part is the synopsis of the command and the second one specifies
6 options, one per line.
7
8 Each non-empty line in the synopsis gives a set of options that can be used
9 together.
10
11 Option flags must be at the begining of the line and multiple flags are
12 separated by commas. Usually, options have a short, one character flag, and a
13 longer one, but the short one can be omitted.
14
15 Long option flags are used as the option's key for the OptDict produced when
16 parsing options.
17
18 When the flag definition is ended with an equal sign, the option takes one
19 string as an argument. Otherwise, the option does not take an argument and
20 corresponds to a boolean flag that is true when the option is given on the
21 command line.
22
23 The option's description is found at the right of its flags definition, after
24 one or more spaces. The description ends at the end of the line. If the
25 description contains text enclosed in square brackets, the enclosed text will
26 be used as the option's default value.
27
28 Options can be put in different groups. Options in the same group must be on
29 consecutive lines. Groups are formed by inserting a line that begins with a
30 space. The text on that line will be output after an empty line.
31 """
32 import sys, os, textwrap, getopt, re, struct
33
34 class OptDict:
35     """Dictionary that exposes keys as attributes.
36
37     Keys can bet set or accessed with a "no-" or "no_" prefix to negate the
38     value.
39     """
40     def __init__(self):
41         self._opts = {}
42
43     def __setitem__(self, k, v):
44         if k.startswith('no-') or k.startswith('no_'):
45             k = k[3:]
46             v = not v
47         self._opts[k] = v
48
49     def __getitem__(self, k):
50         if k.startswith('no-') or k.startswith('no_'):
51             return not self._opts[k[3:]]
52         return self._opts[k]
53
54     def __getattr__(self, k):
55         return self[k]
56
57
58 def _default_onabort(msg):
59     sys.exit(97)
60
61
62 def _intify(v):
63     try:
64         vv = int(v or '')
65         if str(vv) == v:
66             return vv
67     except ValueError:
68         pass
69     return v
70
71
72 def _atoi(v):
73     try:
74         return int(v or 0)
75     except ValueError:
76         return 0
77
78
79 def _remove_negative_kv(k, v):
80     if k.startswith('no-') or k.startswith('no_'):
81         return k[3:], not v
82     return k,v
83
84 def _remove_negative_k(k):
85     return _remove_negative_kv(k, None)[0]
86
87
88 def _tty_width():
89     s = struct.pack("HHHH", 0, 0, 0, 0)
90     try:
91         import fcntl, termios
92         s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
93     except (IOError, ImportError):
94         return _atoi(os.environ.get('WIDTH')) or 70
95     (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
96     return xsize or 70
97
98
99 class Options:
100     """Option parser.
101     When constructed, a string called an option spec must be given. It
102     specifies the synopsis and option flags and their description.  For more
103     information about option specs, see the docstring at the top of this file.
104
105     Two optional arguments specify an alternative parsing function and an
106     alternative behaviour on abort (after having output the usage string).
107
108     By default, the parser function is getopt.gnu_getopt, and the abort
109     behaviour is to exit the program.
110     """
111     def __init__(self, optspec, optfunc=getopt.gnu_getopt,
112                  onabort=_default_onabort):
113         self.optspec = optspec
114         self._onabort = onabort
115         self.optfunc = optfunc
116         self._aliases = {}
117         self._shortopts = 'h?'
118         self._longopts = ['help']
119         self._hasparms = {}
120         self._defaults = {}
121         self._usagestr = self._gen_usage()
122
123     def _gen_usage(self):
124         out = []
125         lines = self.optspec.strip().split('\n')
126         lines.reverse()
127         first_syn = True
128         while lines:
129             l = lines.pop()
130             if l == '--': break
131             out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
132             first_syn = False
133         out.append('\n')
134         last_was_option = False
135         while lines:
136             l = lines.pop()
137             if l.startswith(' '):
138                 out.append('%s%s\n' % (last_was_option and '\n' or '',
139                                        l.lstrip()))
140                 last_was_option = False
141             elif l:
142                 (flags, extra) = l.split(' ', 1)
143                 extra = extra.strip()
144                 if flags.endswith('='):
145                     flags = flags[:-1]
146                     has_parm = 1
147                 else:
148                     has_parm = 0
149                 g = re.search(r'\[([^\]]*)\]$', extra)
150                 if g:
151                     defval = g.group(1)
152                 else:
153                     defval = None
154                 flagl = flags.split(',')
155                 flagl_nice = []
156                 for _f in flagl:
157                     f,dvi = _remove_negative_kv(_f, _intify(defval))
158                     self._aliases[f] = _remove_negative_k(flagl[0])
159                     self._hasparms[f] = has_parm
160                     self._defaults[f] = dvi
161                     if len(f) == 1:
162                         self._shortopts += f + (has_parm and ':' or '')
163                         flagl_nice.append('-' + f)
164                     else:
165                         f_nice = re.sub(r'\W', '_', f)
166                         self._aliases[f_nice] = _remove_negative_k(flagl[0])
167                         self._longopts.append(f + (has_parm and '=' or ''))
168                         self._longopts.append('no-' + f)
169                         flagl_nice.append('--' + _f)
170                 flags_nice = ', '.join(flagl_nice)
171                 if has_parm:
172                     flags_nice += ' ...'
173                 prefix = '    %-20s  ' % flags_nice
174                 argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
175                                                 initial_indent=prefix,
176                                                 subsequent_indent=' '*28))
177                 out.append(argtext + '\n')
178                 last_was_option = True
179             else:
180                 out.append('\n')
181                 last_was_option = False
182         return ''.join(out).rstrip() + '\n'
183
184     def usage(self, msg=""):
185         """Print usage string to stderr and abort."""
186         sys.stderr.write(self._usagestr)
187         e = self._onabort and self._onabort(msg) or None
188         if e:
189             raise e
190
191     def fatal(self, s):
192         """Print an error message to stderr and abort with usage string."""
193         msg = 'error: %s\n' % s
194         sys.stderr.write(msg)
195         return self.usage(msg)
196
197     def parse(self, args):
198         """Parse a list of arguments and return (options, flags, extra).
199
200         In the returned tuple, "options" is an OptDict with known options,
201         "flags" is a list of option flags that were used on the command-line,
202         and "extra" is a list of positional arguments.
203         """
204         try:
205             (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
206         except getopt.GetoptError, e:
207             self.fatal(e)
208
209         opt = OptDict()
210
211         for k,v in self._defaults.iteritems():
212             k = self._aliases[k]
213             opt[k] = v
214
215         for (k,v) in flags:
216             k = k.lstrip('-')
217             if k in ('h', '?', 'help'):
218                 self.usage()
219             if k.startswith('no-'):
220                 k = self._aliases[k[3:]]
221                 v = 0
222             else:
223                 k = self._aliases[k]
224                 if not self._hasparms[k]:
225                     assert(v == '')
226                     v = (opt._opts.get(k) or 0) + 1
227                 else:
228                     v = _intify(v)
229             opt[k] = v
230         for (f1,f2) in self._aliases.iteritems():
231             opt[f1] = opt._opts.get(f2)
232         return (opt,flags,extra)