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 AND CONTRIBUTORS ``AS
20# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
23# <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
24# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
28# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
30# OF THE POSSIBILITY OF SUCH DAMAGE.
31#
32"""Command-line options parser.
33With the help of an options spec string, easily parse command-line options.
34
35An options spec is made up of two parts, separated by a line with two dashes.
36The first part is the synopsis of the command and the second one specifies
37options, one per line.
38
39Each non-empty line in the synopsis gives a set of options that can be used
40together.
41
42Option flags must be at the begining of the line and multiple flags are
43separated by commas. Usually, options have a short, one character flag, and a
44longer one, but the short one can be omitted.
45
46Long option flags are used as the option's key for the OptDict produced when
47parsing options.
48
49When the flag definition is ended with an equal sign, the option takes
50one string as an argument, and that string will be converted to an
51integer when possible. Otherwise, the option does not take an argument
52and corresponds to a boolean flag that is true when the option is
53given on the command line.
54
55The option's description is found at the right of its flags definition, after
56one or more spaces. The description ends at the end of the line. If the
57description contains text enclosed in square brackets, the enclosed text will
58be used as the option's default value.
59
60Options can be put in different groups. Options in the same group must be on
61consecutive lines. Groups are formed by inserting a line that begins with a
62space. The text on that line will be output after an empty line.
63"""
64
65from __future__ import absolute_import
66import sys, os, textwrap, getopt, re, struct
67
68try:
69    import fcntl
70except ImportError:
71    fcntl = None
72
73try:
74    import termios
75except ImportError:
76    termios = None
77
78
79def _invert(v, invert):
80    if invert:
81        return not v
82    return v
83
84
85def _remove_negative_kv(k, v):
86    if k.startswith('no-') or k.startswith('no_'):
87        return k[3:], not v
88    return k,v
89
90
91class OptDict(object):
92    """Dictionary that exposes keys as attributes.
93
94    Keys can be set or accessed with a "no-" or "no_" prefix to negate the
95    value.
96    """
97    def __init__(self, aliases):
98        self._opts = {}
99        self._aliases = aliases
100
101    def _unalias(self, k):
102        k, reinvert = _remove_negative_kv(k, False)
103        k, invert = self._aliases[k]
104        return k, invert ^ reinvert
105
106    def __setitem__(self, k, v):
107        k, invert = self._unalias(k)
108        self._opts[k] = _invert(v, invert)
109
110    def __getitem__(self, k):
111        k, invert = self._unalias(k)
112        return _invert(self._opts[k], invert)
113
114    def __getattr__(self, k):
115        return self[k]
116
117
118def _default_onabort(msg):
119    sys.exit(97)
120
121
122def _intify(v):
123    try:
124        vv = int(v or '')
125        if str(vv) == v:
126            return vv
127    except ValueError:
128        pass
129    return v
130
131
132if not fcntl and termios:
133    def _tty_width():
134        return 70
135else:
136    def _tty_width():
137        s = struct.pack("HHHH", 0, 0, 0, 0)
138        try:
139            s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
140        except IOError:
141            return 70
142        ysize, xsize, ypix, xpix = struct.unpack('HHHH', s)
143        return xsize or 70
144
145
146class Options:
147    """Option parser.
148    When constructed, a string called an option spec must be given. It
149    specifies the synopsis and option flags and their description.  For more
150    information about option specs, see the docstring at the top of this file.
151
152    Two optional arguments specify an alternative parsing function and an
153    alternative behaviour on abort (after having output the usage string).
154
155    By default, the parser function is getopt.gnu_getopt, and the abort
156    behaviour is to exit the program.
157    """
158    def __init__(self, optspec, optfunc=getopt.gnu_getopt,
159                 onabort=_default_onabort):
160        self.optspec = optspec
161        self._onabort = onabort
162        self.optfunc = optfunc
163        self._aliases = {}
164        self._shortopts = 'h?'
165        self._longopts = ['help', 'usage']
166        self._hasparms = {}
167        self._defaults = {}
168        self._usagestr = self._gen_usage()  # this also parses the optspec
169
170    def _gen_usage(self):
171        out = []
172        lines = self.optspec.strip().split('\n')
173        lines.reverse()
174        first_syn = True
175        while lines:
176            l = lines.pop()
177            if l == '--': break
178            out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
179            first_syn = False
180        out.append('\n')
181        last_was_option = False
182        while lines:
183            l = lines.pop()
184            if l.startswith(' '):
185                out.append('%s%s\n' % (last_was_option and '\n' or '',
186                                       l.lstrip()))
187                last_was_option = False
188            elif l:
189                (flags,extra) = (l + ' ').split(' ', 1)
190                extra = extra.strip()
191                if flags.endswith('='):
192                    flags = flags[:-1]
193                    has_parm = 1
194                else:
195                    has_parm = 0
196                g = re.search(r'\[([^\]]*)\]$', extra)
197                if g:
198                    defval = _intify(g.group(1))
199                else:
200                    defval = None
201                flagl = flags.split(',')
202                flagl_nice = []
203                flag_main, invert_main = _remove_negative_kv(flagl[0], False)
204                self._defaults[flag_main] = _invert(defval, invert_main)
205                for _f in flagl:
206                    f,invert = _remove_negative_kv(_f, 0)
207                    self._aliases[f] = (flag_main, invert_main ^ invert)
208                    self._hasparms[f] = has_parm
209                    if f == '#':
210                        self._shortopts += '0123456789'
211                        flagl_nice.append('-#')
212                    elif len(f) == 1:
213                        self._shortopts += f + (has_parm and ':' or '')
214                        flagl_nice.append('-' + f)
215                    else:
216                        f_nice = re.sub(r'\W', '_', f)
217                        self._aliases[f_nice] = (flag_main,
218                                                 invert_main ^ invert)
219                        self._longopts.append(f + (has_parm and '=' or ''))
220                        self._longopts.append('no-' + f)
221                        flagl_nice.append('--' + _f)
222                flags_nice = ', '.join(flagl_nice)
223                if has_parm:
224                    flags_nice += ' ...'
225                prefix = '    %-20s  ' % flags_nice
226                argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
227                                                initial_indent=prefix,
228                                                subsequent_indent=' '*28))
229                out.append(argtext + '\n')
230                last_was_option = True
231            else:
232                out.append('\n')
233                last_was_option = False
234        return ''.join(out).rstrip() + '\n'
235
236    def usage(self, msg=""):
237        """Print usage string to stderr and abort."""
238        sys.stderr.write(self._usagestr)
239        if msg:
240            sys.stderr.write(msg)
241        e = self._onabort and self._onabort(msg) or None
242        if e:
243            raise e
244
245    def fatal(self, msg):
246        """Print an error message to stderr and abort with usage string."""
247        msg = '\nerror: %s\n' % msg
248        return self.usage(msg)
249
250    def parse(self, args):
251        """Parse a list of arguments and return (options, flags, extra).
252
253        In the returned tuple, "options" is an OptDict with known options,
254        "flags" is a list of option flags that were used on the command-line,
255        and "extra" is a list of positional arguments.
256        """
257        try:
258            (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
259        except getopt.GetoptError as e:
260            self.fatal(e)
261
262        opt = OptDict(aliases=self._aliases)
263
264        for k,v in self._defaults.items():
265            opt[k] = v
266
267        for (k,v) in flags:
268            k = k.lstrip('-')
269            if k in ('h', '?', 'help', 'usage'):
270                self.usage()
271            if (self._aliases.get('#') and
272                  k in ('0','1','2','3','4','5','6','7','8','9')):
273                v = int(k)  # guaranteed to be exactly one digit
274                k, invert = self._aliases['#']
275                opt['#'] = v
276            else:
277                k, invert = opt._unalias(k)
278                if not self._hasparms[k]:
279                    assert(v == '')
280                    v = (opt._opts.get(k) or 0) + 1
281                else:
282                    v = _intify(v)
283            opt[k] = _invert(v, invert)
284        return (opt,flags,extra)
285