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