X-Git-Url: https://arthur.barton.de/cgi-bin/gitweb.cgi?p=bup.git;a=blobdiff_plain;f=lib%2Fbup%2Foptions.py;h=a7fd284b719cbf51a61ccc7f706ccbe61fa6495c;hp=c1a1455d476cddf3aed42db31f81f21d19b3cdc1;hb=bf67f94dd4f4096de4eee07a7dc377d6c889a016;hpb=b0c1c31e420d243b5644c4158cea149b3b9771fb diff --git a/lib/bup/options.py b/lib/bup/options.py index c1a1455..a7fd284 100644 --- a/lib/bup/options.py +++ b/lib/bup/options.py @@ -1,30 +1,172 @@ -import sys, textwrap, getopt, re +# Copyright 2010-2012 Avery Pennarun and options.py contributors. +# All rights reserved. +# +# (This license applies to this file but not necessarily the other files in +# this package.) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY AVERY PENNARUN AND CONTRIBUTORS ``AS +# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +# +"""Command-line options parser. +With the help of an options spec string, easily parse command-line options. -class OptDict: - def __init__(self): +An options spec is made up of two parts, separated by a line with two dashes. +The first part is the synopsis of the command and the second one specifies +options, one per line. + +Each non-empty line in the synopsis gives a set of options that can be used +together. + +Option flags must be at the begining of the line and multiple flags are +separated by commas. Usually, options have a short, one character flag, and a +longer one, but the short one can be omitted. + +Long option flags are used as the option's key for the OptDict produced when +parsing options. + +When the flag definition is ended with an equal sign, the option takes +one string as an argument, and that string will be converted to an +integer when possible. Otherwise, the option does not take an argument +and corresponds to a boolean flag that is true when the option is +given on the command line. + +The option's description is found at the right of its flags definition, after +one or more spaces. The description ends at the end of the line. If the +description contains text enclosed in square brackets, the enclosed text will +be used as the option's default value. + +Options can be put in different groups. Options in the same group must be on +consecutive lines. Groups are formed by inserting a line that begins with a +space. The text on that line will be output after an empty line. +""" + +from __future__ import absolute_import +import sys, os, textwrap, getopt, re, struct + +try: + import fcntl +except ImportError: + fcntl = None + +try: + import termios +except ImportError: + termios = None + + +def _invert(v, invert): + if invert: + return not v + return v + + +def _remove_negative_kv(k, v): + if k.startswith('no-') or k.startswith('no_'): + return k[3:], not v + return k,v + + +class OptDict(object): + """Dictionary that exposes keys as attributes. + + Keys can be set or accessed with a "no-" or "no_" prefix to negate the + value. + """ + def __init__(self, aliases): self._opts = {} + self._aliases = aliases + + def _unalias(self, k): + k, reinvert = _remove_negative_kv(k, False) + k, invert = self._aliases[k] + return k, invert ^ reinvert def __setitem__(self, k, v): - self._opts[k] = v + k, invert = self._unalias(k) + self._opts[k] = _invert(v, invert) def __getitem__(self, k): - return self._opts[k] + k, invert = self._unalias(k) + return _invert(self._opts[k], invert) def __getattr__(self, k): return self[k] +def _default_onabort(msg): + sys.exit(97) + + +def _intify(v): + try: + vv = int(v or '') + if str(vv) == v: + return vv + except ValueError: + pass + return v + + +if not fcntl and termios: + def _tty_width(): + return 70 +else: + def _tty_width(): + s = struct.pack("HHHH", 0, 0, 0, 0) + try: + s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s) + except IOError: + return 70 + ysize, xsize, ypix, xpix = struct.unpack('HHHH', s) + return xsize or 70 + + class Options: - def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt): - self.exe = exe + """Option parser. + When constructed, a string called an option spec must be given. It + specifies the synopsis and option flags and their description. For more + information about option specs, see the docstring at the top of this file. + + Two optional arguments specify an alternative parsing function and an + alternative behaviour on abort (after having output the usage string). + + By default, the parser function is getopt.gnu_getopt, and the abort + behaviour is to exit the program. + """ + def __init__(self, optspec, optfunc=getopt.gnu_getopt, + onabort=_default_onabort): self.optspec = optspec + self._onabort = onabort self.optfunc = optfunc self._aliases = {} self._shortopts = 'h?' - self._longopts = ['help'] + self._longopts = ['help', 'usage'] self._hasparms = {} - self._usagestr = self._gen_usage() - + self._defaults = {} + self._usagestr = self._gen_usage() # this also parses the optspec + def _gen_usage(self): out = [] lines = self.optspec.strip().split('\n') @@ -36,80 +178,107 @@ class Options: out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l)) first_syn = False out.append('\n') + last_was_option = False while lines: l = lines.pop() if l.startswith(' '): - out.append('\n%s\n' % l.lstrip()) + out.append('%s%s\n' % (last_was_option and '\n' or '', + l.lstrip())) + last_was_option = False elif l: - (flags, extra) = l.split(' ', 1) + (flags,extra) = (l + ' ').split(' ', 1) extra = extra.strip() if flags.endswith('='): flags = flags[:-1] has_parm = 1 else: has_parm = 0 + g = re.search(r'\[([^\]]*)\]$', extra) + if g: + defval = _intify(g.group(1)) + else: + defval = None flagl = flags.split(',') flagl_nice = [] - for f in flagl: - self._aliases[f] = flagl[0] + flag_main, invert_main = _remove_negative_kv(flagl[0], False) + self._defaults[flag_main] = _invert(defval, invert_main) + for _f in flagl: + f,invert = _remove_negative_kv(_f, 0) + self._aliases[f] = (flag_main, invert_main ^ invert) self._hasparms[f] = has_parm - if len(f) == 1: + if f == '#': + self._shortopts += '0123456789' + flagl_nice.append('-#') + elif len(f) == 1: self._shortopts += f + (has_parm and ':' or '') flagl_nice.append('-' + f) else: f_nice = re.sub(r'\W', '_', f) - self._aliases[f_nice] = flagl[0] - assert(not f.startswith('no-')) # supported implicitly + self._aliases[f_nice] = (flag_main, + invert_main ^ invert) self._longopts.append(f + (has_parm and '=' or '')) self._longopts.append('no-' + f) - flagl_nice.append('--' + f) + flagl_nice.append('--' + _f) flags_nice = ', '.join(flagl_nice) if has_parm: flags_nice += ' ...' prefix = ' %-20s ' % flags_nice - argtext = '\n'.join(textwrap.wrap(extra, width=70, + argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(), initial_indent=prefix, subsequent_indent=' '*28)) out.append(argtext + '\n') + last_was_option = True else: out.append('\n') + last_was_option = False return ''.join(out).rstrip() + '\n' - - def usage(self): + + def usage(self, msg=""): + """Print usage string to stderr and abort.""" sys.stderr.write(self._usagestr) - sys.exit(97) + if msg: + sys.stderr.write(msg) + e = self._onabort and self._onabort(msg) or None + if e: + raise e + + def fatal(self, msg): + """Print an error message to stderr and abort with usage string.""" + msg = '\nerror: %s\n' % msg + return self.usage(msg) - def fatal(self, s): - sys.stderr.write('error: %s\n' % s) - return self.usage() - def parse(self, args): + """Parse a list of arguments and return (options, flags, extra). + + In the returned tuple, "options" is an OptDict with known options, + "flags" is a list of option flags that were used on the command-line, + and "extra" is a list of positional arguments. + """ try: (flags,extra) = self.optfunc(args, self._shortopts, self._longopts) - except getopt.GetoptError, e: + except getopt.GetoptError as e: self.fatal(e) - opt = OptDict() + opt = OptDict(aliases=self._aliases) + + for k,v in self._defaults.items(): + opt[k] = v + for (k,v) in flags: k = k.lstrip('-') - if k in ('h', '?', 'help'): + if k in ('h', '?', 'help', 'usage'): self.usage() - if k.startswith('no-'): - k = self._aliases[k[3:]] - v = 0 + if (self._aliases.get('#') and + k in ('0','1','2','3','4','5','6','7','8','9')): + v = int(k) # guaranteed to be exactly one digit + k, invert = self._aliases['#'] + opt['#'] = v else: - k = self._aliases[k] + k, invert = opt._unalias(k) if not self._hasparms[k]: assert(v == '') v = (opt._opts.get(k) or 0) + 1 else: - try: - vv = int(v) - if str(vv) == v: - v = vv - except ValueError: - pass - opt[k] = v - for (f1,f2) in self._aliases.iteritems(): - opt[f1] = opt._opts.get(f2) + v = _intify(v) + opt[k] = _invert(v, invert) return (opt,flags,extra)