| 1 | # This program is free software; you can redistribute it and/or modify |
|---|
| 2 | # it under the terms of the GNU General Public License as published by |
|---|
| 3 | # the Free Software Foundation; either version 2 of the License, or |
|---|
| 4 | # (at your option) any later version. |
|---|
| 5 | # |
|---|
| 6 | # This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 7 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 8 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
|---|
| 9 | # |
|---|
| 10 | # You should have received a copy of the GNU General Public License along with |
|---|
| 11 | # this program; if not, write to the Free Software Foundation, Inc., |
|---|
| 12 | # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
|---|
| 13 | """ Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). |
|---|
| 14 | http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|---|
| 15 | |
|---|
| 16 | add an abstraction level to transparently import optik classes from optparse |
|---|
| 17 | (python >= 2.3) or the optik package. |
|---|
| 18 | It also defines three new types for optik/optparse command line parser : |
|---|
| 19 | |
|---|
| 20 | * regexp |
|---|
| 21 | argument of this type will be converted using re.compile |
|---|
| 22 | * csv |
|---|
| 23 | argument of this type will be converted using split(',') |
|---|
| 24 | * yn |
|---|
| 25 | argument of this type will be true if 'y' or 'yes', false if 'n' or 'no' |
|---|
| 26 | * named |
|---|
| 27 | argument of this type are in the form <NAME>=<VALUE> or <NAME>:<VALUE> |
|---|
| 28 | |
|---|
| 29 | """ |
|---|
| 30 | |
|---|
| 31 | import re |
|---|
| 32 | import sys |
|---|
| 33 | import time |
|---|
| 34 | from copy import copy |
|---|
| 35 | from os.path import exists |
|---|
| 36 | |
|---|
| 37 | try: |
|---|
| 38 | # python >= 2.3 |
|---|
| 39 | from optparse import OptionParser as BaseParser, Option as BaseOption, \ |
|---|
| 40 | OptionGroup, OptionValueError, OptionError, Values, HelpFormatter, \ |
|---|
| 41 | NO_DEFAULT |
|---|
| 42 | except ImportError: |
|---|
| 43 | # python < 2.3 |
|---|
| 44 | from optik import OptionParser as BaseParser, Option as BaseOption, \ |
|---|
| 45 | OptionGroup, OptionValueError, OptionError, Values, HelpFormatter |
|---|
| 46 | try: |
|---|
| 47 | from optik import NO_DEFAULT |
|---|
| 48 | except: |
|---|
| 49 | NO_DEFAULT = [] |
|---|
| 50 | |
|---|
| 51 | try: |
|---|
| 52 | from mx import DateTime |
|---|
| 53 | HAS_MX_DATETIME = True |
|---|
| 54 | except ImportError: |
|---|
| 55 | HAS_MX_DATETIME = False |
|---|
| 56 | |
|---|
| 57 | |
|---|
| 58 | OPTPARSE_FORMAT_DEFAULT = sys.version_info >= (2, 4) |
|---|
| 59 | |
|---|
| 60 | from logilab.common.textutils import get_csv |
|---|
| 61 | |
|---|
| 62 | def check_regexp(option, opt, value): |
|---|
| 63 | """check a regexp value by trying to compile it |
|---|
| 64 | return the compiled regexp |
|---|
| 65 | """ |
|---|
| 66 | if hasattr(value, 'pattern'): |
|---|
| 67 | return value |
|---|
| 68 | try: |
|---|
| 69 | return re.compile(value) |
|---|
| 70 | except ValueError: |
|---|
| 71 | raise OptionValueError( |
|---|
| 72 | "option %s: invalid regexp value: %r" % (opt, value)) |
|---|
| 73 | |
|---|
| 74 | def check_csv(option, opt, value): |
|---|
| 75 | """check a csv value by trying to split it |
|---|
| 76 | return the list of separated values |
|---|
| 77 | """ |
|---|
| 78 | if isinstance(value, (list, tuple)): |
|---|
| 79 | return value |
|---|
| 80 | try: |
|---|
| 81 | return get_csv(value) |
|---|
| 82 | except ValueError: |
|---|
| 83 | raise OptionValueError( |
|---|
| 84 | "option %s: invalid csv value: %r" % (opt, value)) |
|---|
| 85 | |
|---|
| 86 | def check_yn(option, opt, value): |
|---|
| 87 | """check a yn value |
|---|
| 88 | return true for yes and false for no |
|---|
| 89 | """ |
|---|
| 90 | if isinstance(value, int): |
|---|
| 91 | return bool(value) |
|---|
| 92 | if value in ('y', 'yes'): |
|---|
| 93 | return True |
|---|
| 94 | if value in ('n', 'no'): |
|---|
| 95 | return False |
|---|
| 96 | msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)" |
|---|
| 97 | raise OptionValueError(msg % (opt, value)) |
|---|
| 98 | |
|---|
| 99 | def check_named(option, opt, value): |
|---|
| 100 | """check a named value |
|---|
| 101 | return a dictionnary containing (name, value) associations |
|---|
| 102 | """ |
|---|
| 103 | if isinstance(value, dict): |
|---|
| 104 | return value |
|---|
| 105 | values = [] |
|---|
| 106 | for value in check_csv(option, opt, value): |
|---|
| 107 | if value.find('=') != -1: |
|---|
| 108 | values.append(value.split('=', 1)) |
|---|
| 109 | elif value.find(':') != -1: |
|---|
| 110 | values.append(value.split(':', 1)) |
|---|
| 111 | if values: |
|---|
| 112 | return dict(values) |
|---|
| 113 | msg = "option %s: invalid named value %r, should be <NAME>=<VALUE> or \ |
|---|
| 114 | <NAME>:<VALUE>" |
|---|
| 115 | raise OptionValueError(msg % (opt, value)) |
|---|
| 116 | |
|---|
| 117 | def check_password(option, opt, value): |
|---|
| 118 | """check a password value (can't be empty) |
|---|
| 119 | """ |
|---|
| 120 | # no actual checking, monkey patch if you want more |
|---|
| 121 | return value |
|---|
| 122 | |
|---|
| 123 | def check_file(option, opt, value): |
|---|
| 124 | """check a file value |
|---|
| 125 | return the filepath |
|---|
| 126 | """ |
|---|
| 127 | if exists(value): |
|---|
| 128 | return value |
|---|
| 129 | msg = "option %s: file %r does not exist" |
|---|
| 130 | raise OptionValueError(msg % (opt, value)) |
|---|
| 131 | |
|---|
| 132 | def check_date(option, opt, value): |
|---|
| 133 | """check a file value |
|---|
| 134 | return the filepath |
|---|
| 135 | """ |
|---|
| 136 | try: |
|---|
| 137 | return DateTime.strptime(value, "%Y/%m/%d") |
|---|
| 138 | except DateTime.Error : |
|---|
| 139 | raise OptionValueError( |
|---|
| 140 | "expected format of %s is yyyy/mm/dd" % opt) |
|---|
| 141 | |
|---|
| 142 | def check_color(option, opt, value): |
|---|
| 143 | """check a color value and returns it |
|---|
| 144 | /!\ does *not* check color labels (like 'red', 'green'), only |
|---|
| 145 | checks hexadecimal forms |
|---|
| 146 | """ |
|---|
| 147 | # Case (1) : color label, we trust the end-user |
|---|
| 148 | if re.match('[a-z0-9 ]+$', value, re.I): |
|---|
| 149 | return value |
|---|
| 150 | # Case (2) : only accepts hexadecimal forms |
|---|
| 151 | if re.match('#[a-f0-9]{6}', value, re.I): |
|---|
| 152 | return value |
|---|
| 153 | # Else : not a color label neither a valid hexadecimal form => error |
|---|
| 154 | msg = "option %s: invalid color : %r, should be either hexadecimal \ |
|---|
| 155 | value or predefinied color" |
|---|
| 156 | raise OptionValueError(msg % (opt, value)) |
|---|
| 157 | |
|---|
| 158 | import types |
|---|
| 159 | |
|---|
| 160 | class Option(BaseOption): |
|---|
| 161 | """override optik.Option to add some new option types |
|---|
| 162 | """ |
|---|
| 163 | TYPES = BaseOption.TYPES + ('regexp', 'csv', 'yn', 'named', 'password', |
|---|
| 164 | 'multiple_choice', 'file', 'font', 'color') |
|---|
| 165 | TYPE_CHECKER = copy(BaseOption.TYPE_CHECKER) |
|---|
| 166 | TYPE_CHECKER['regexp'] = check_regexp |
|---|
| 167 | TYPE_CHECKER['csv'] = check_csv |
|---|
| 168 | TYPE_CHECKER['yn'] = check_yn |
|---|
| 169 | TYPE_CHECKER['named'] = check_named |
|---|
| 170 | TYPE_CHECKER['multiple_choice'] = check_csv |
|---|
| 171 | TYPE_CHECKER['file'] = check_file |
|---|
| 172 | TYPE_CHECKER['color'] = check_color |
|---|
| 173 | TYPE_CHECKER['password'] = check_password |
|---|
| 174 | if HAS_MX_DATETIME: |
|---|
| 175 | TYPES += ('date',) |
|---|
| 176 | TYPE_CHECKER['date'] = check_date |
|---|
| 177 | |
|---|
| 178 | def _check_choice(self): |
|---|
| 179 | """FIXME: need to override this due to optik misdesign""" |
|---|
| 180 | if self.type in ("choice", "multiple_choice"): |
|---|
| 181 | if self.choices is None: |
|---|
| 182 | raise OptionError( |
|---|
| 183 | "must supply a list of choices for type 'choice'", self) |
|---|
| 184 | elif type(self.choices) not in (types.TupleType, types.ListType): |
|---|
| 185 | raise OptionError( |
|---|
| 186 | "choices must be a list of strings ('%s' supplied)" |
|---|
| 187 | % str(type(self.choices)).split("'")[1], self) |
|---|
| 188 | elif self.choices is not None: |
|---|
| 189 | raise OptionError( |
|---|
| 190 | "must not supply choices for type %r" % self.type, self) |
|---|
| 191 | BaseOption.CHECK_METHODS[2] = _check_choice |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | def process(self, opt, value, values, parser): |
|---|
| 195 | # First, convert the value(s) to the right type. Howl if any |
|---|
| 196 | # value(s) are bogus. |
|---|
| 197 | try: |
|---|
| 198 | value = self.convert_value(opt, value) |
|---|
| 199 | except AttributeError: # py < 2.4 |
|---|
| 200 | value = self.check_value(opt, value) |
|---|
| 201 | if self.type == 'named': |
|---|
| 202 | existant = getattr(values, self.dest) |
|---|
| 203 | if existant: |
|---|
| 204 | existant.update(value) |
|---|
| 205 | value = existant |
|---|
| 206 | # And then take whatever action is expected of us. |
|---|
| 207 | # This is a separate method to make life easier for |
|---|
| 208 | # subclasses to add new actions. |
|---|
| 209 | return self.take_action( |
|---|
| 210 | self.action, self.dest, opt, value, values, parser) |
|---|
| 211 | |
|---|
| 212 | class OptionParser(BaseParser): |
|---|
| 213 | """override optik.OptionParser to use our Option class |
|---|
| 214 | """ |
|---|
| 215 | def __init__(self, option_class=Option, *args, **kwargs): |
|---|
| 216 | BaseParser.__init__(self, option_class=Option, *args, **kwargs) |
|---|
| 217 | |
|---|
| 218 | |
|---|
| 219 | class ManHelpFormatter(HelpFormatter): |
|---|
| 220 | """Format help using man pages ROFF format""" |
|---|
| 221 | |
|---|
| 222 | def __init__ (self, |
|---|
| 223 | indent_increment=0, |
|---|
| 224 | max_help_position=24, |
|---|
| 225 | width=79, |
|---|
| 226 | short_first=0): |
|---|
| 227 | HelpFormatter.__init__ ( |
|---|
| 228 | self, indent_increment, max_help_position, width, short_first) |
|---|
| 229 | |
|---|
| 230 | def format_heading(self, heading): |
|---|
| 231 | return '.SH %s\n' % heading.upper() |
|---|
| 232 | |
|---|
| 233 | def format_description(self, description): |
|---|
| 234 | return description |
|---|
| 235 | |
|---|
| 236 | def format_option(self, option): |
|---|
| 237 | try: |
|---|
| 238 | optstring = option.option_strings |
|---|
| 239 | except AttributeError: |
|---|
| 240 | optstring = self.format_option_strings(option) |
|---|
| 241 | if option.help: |
|---|
| 242 | help = ' '.join([l.strip() for l in option.help.splitlines()]) |
|---|
| 243 | else: |
|---|
| 244 | help = '' |
|---|
| 245 | return '''.IP "%s" |
|---|
| 246 | %s |
|---|
| 247 | ''' % (optstring, help) |
|---|
| 248 | |
|---|
| 249 | def format_head(self, optparser, pkginfo, section=1): |
|---|
| 250 | try: |
|---|
| 251 | pgm = optparser._get_prog_name() |
|---|
| 252 | except AttributeError: |
|---|
| 253 | # py >= 2.4.X (dunno which X exactly, at least 2) |
|---|
| 254 | pgm = optparser.get_prog_name() |
|---|
| 255 | short_desc = self.format_short_description(pgm, pkginfo.short_desc) |
|---|
| 256 | long_desc = self.format_long_description(pgm, pkginfo.long_desc) |
|---|
| 257 | return '%s\n%s\n%s\n%s' % (self.format_title(pgm, section), short_desc, |
|---|
| 258 | self.format_synopsis(pgm), long_desc) |
|---|
| 259 | |
|---|
| 260 | def format_title(self, pgm, section): |
|---|
| 261 | date = '-'.join([str(num) for num in time.localtime()[:3]]) |
|---|
| 262 | return '.TH %s %s "%s" %s' % (pgm, section, date, pgm) |
|---|
| 263 | |
|---|
| 264 | def format_short_description(self, pgm, short_desc): |
|---|
| 265 | return '''.SH NAME |
|---|
| 266 | .B %s |
|---|
| 267 | \- %s |
|---|
| 268 | ''' % (pgm, short_desc.strip()) |
|---|
| 269 | |
|---|
| 270 | def format_synopsis(self, pgm): |
|---|
| 271 | return '''.SH SYNOPSIS |
|---|
| 272 | .B %s |
|---|
| 273 | [ |
|---|
| 274 | .I OPTIONS |
|---|
| 275 | ] [ |
|---|
| 276 | .I <arguments> |
|---|
| 277 | ] |
|---|
| 278 | ''' % pgm |
|---|
| 279 | |
|---|
| 280 | def format_long_description(self, pgm, long_desc): |
|---|
| 281 | long_desc = '\n'.join([line.lstrip() |
|---|
| 282 | for line in long_desc.splitlines()]) |
|---|
| 283 | long_desc = long_desc.replace('\n.\n', '\n\n') |
|---|
| 284 | if long_desc.lower().startswith(pgm): |
|---|
| 285 | long_desc = long_desc[len(pgm):] |
|---|
| 286 | return '''.SH DESCRIPTION |
|---|
| 287 | .B %s |
|---|
| 288 | %s |
|---|
| 289 | ''' % (pgm, long_desc.strip()) |
|---|
| 290 | |
|---|
| 291 | def format_tail(self, pkginfo): |
|---|
| 292 | return '''.SH SEE ALSO |
|---|
| 293 | /usr/share/doc/pythonX.Y-%s/ |
|---|
| 294 | |
|---|
| 295 | .SH COPYRIGHT |
|---|
| 296 | %s |
|---|
| 297 | |
|---|
| 298 | This program is free software; you can redistribute it and/or modify |
|---|
| 299 | it under the terms of the GNU General Public License as published |
|---|
| 300 | by the Free Software Foundation; either version 2 of the License, |
|---|
| 301 | or (at your option) any later version. |
|---|
| 302 | |
|---|
| 303 | This program is distributed in the hope that it will be useful, |
|---|
| 304 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 305 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 306 | GNU General Public License for more details. |
|---|
| 307 | |
|---|
| 308 | You should have received a copy of the GNU General Public License |
|---|
| 309 | along with this program; if not, write to the Free Software |
|---|
| 310 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, |
|---|
| 311 | MA 02111-1307 USA. |
|---|
| 312 | .SH BUGS |
|---|
| 313 | Please report bugs on the project\'s mailing list: |
|---|
| 314 | %s |
|---|
| 315 | |
|---|
| 316 | .SH AUTHOR |
|---|
| 317 | %s <%s> |
|---|
| 318 | ''' % (getattr(pkginfo, 'debian_name', pkginfo.modname), pkginfo.copyright, |
|---|
| 319 | pkginfo.mailinglist, pkginfo.author, pkginfo.author_email) |
|---|
| 320 | |
|---|
| 321 | |
|---|
| 322 | def generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout): |
|---|
| 323 | """generate a man page from an optik parser""" |
|---|
| 324 | formatter = ManHelpFormatter() |
|---|
| 325 | print >> stream, formatter.format_head(optparser, pkginfo, section) |
|---|
| 326 | print >> stream, optparser.format_option_help(formatter) |
|---|
| 327 | print >> stream, formatter.format_tail(pkginfo) |
|---|
| 328 | |
|---|
| 329 | |
|---|
| 330 | __all__ = ('OptionParser', 'Option', 'OptionGroup', 'OptionValueError', |
|---|
| 331 | 'Values') |
|---|