Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 1 | """distutils.fancy_getopt |
| 2 | |
| 3 | Wrapper around the standard getopt module that provides the following |
| 4 | additional features: |
| 5 | * short and long options are tied together |
| 6 | * options have help strings, so fancy_getopt could potentially |
| 7 | create a complete usage summary |
| 8 | * options set attributes of a passed-in object |
| 9 | """ |
| 10 | |
| 11 | # created 1999/03/03, Greg Ward |
| 12 | |
Greg Ward | 3ce77fd | 2000-03-02 01:49:45 +0000 | [diff] [blame^] | 13 | __revision__ = "$Id$" |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 14 | |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 15 | import sys, string, re |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 16 | from types import * |
| 17 | import getopt |
| 18 | from distutils.errors import * |
| 19 | |
| 20 | # Much like command_re in distutils.core, this is close to but not quite |
| 21 | # the same as a Python NAME -- except, in the spirit of most GNU |
| 22 | # utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) |
| 23 | # The similarities to NAME are again not a coincidence... |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 24 | longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)' |
| 25 | longopt_re = re.compile (r'^%s$' % longopt_pat) |
| 26 | |
| 27 | # For recognizing "negative alias" options, eg. "quiet=!verbose" |
| 28 | neg_alias_re = re.compile ("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) |
| 29 | |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 30 | |
| 31 | # This is used to translate long options to legitimate Python identifiers |
| 32 | # (for use as attributes of some object). |
| 33 | longopt_xlate = string.maketrans ('-', '_') |
| 34 | |
| 35 | |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 36 | def fancy_getopt (options, negative_opt, object, args): |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 37 | |
| 38 | # The 'options' table is a list of 3-tuples: |
| 39 | # (long_option, short_option, help_string) |
| 40 | # if an option takes an argument, its long_option should have '=' |
| 41 | # appended; short_option should just be a single character, no ':' in |
| 42 | # any case. If a long_option doesn't have a corresponding |
| 43 | # short_option, short_option should be None. All option tuples must |
| 44 | # have long options. |
| 45 | |
| 46 | # Build the short_opts string and long_opts list, remembering how |
| 47 | # the two are tied together |
| 48 | |
| 49 | short_opts = [] # we'll join 'em when done |
| 50 | long_opts = [] |
| 51 | short2long = {} |
| 52 | attr_name = {} |
| 53 | takes_arg = {} |
| 54 | |
Greg Ward | 0081cc5 | 1999-08-14 23:44:37 +0000 | [diff] [blame] | 55 | for option in options: |
| 56 | try: |
| 57 | (long, short, help) = option |
| 58 | except ValueError: |
| 59 | raise DistutilsGetoptError, \ |
| 60 | "invalid option tuple " + str (option) |
| 61 | |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 62 | # Type-check the option names |
| 63 | if type (long) is not StringType or len (long) < 2: |
| 64 | raise DistutilsGetoptError, \ |
Greg Ward | 0081cc5 | 1999-08-14 23:44:37 +0000 | [diff] [blame] | 65 | "long option '%s' must be a string of length >= 2" % \ |
| 66 | long |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 67 | |
| 68 | if (not ((short is None) or |
| 69 | (type (short) is StringType and len (short) == 1))): |
| 70 | raise DistutilsGetoptError, \ |
Greg Ward | 0081cc5 | 1999-08-14 23:44:37 +0000 | [diff] [blame] | 71 | "short option '%s' must be None or string of length 1" % \ |
| 72 | short |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 73 | |
| 74 | long_opts.append (long) |
| 75 | |
| 76 | if long[-1] == '=': # option takes an argument? |
| 77 | if short: short = short + ':' |
| 78 | long = long[0:-1] |
| 79 | takes_arg[long] = 1 |
| 80 | else: |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 81 | |
| 82 | # Is option is a "negative alias" for some other option (eg. |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 83 | # "quiet" == "!verbose")? |
| 84 | alias_to = negative_opt.get(long) |
| 85 | if alias_to is not None: |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 86 | if not takes_arg.has_key(alias_to) or takes_arg[alias_to]: |
| 87 | raise DistutilsGetoptError, \ |
| 88 | ("option '%s' is a negative alias for '%s', " + |
| 89 | "which either hasn't been defined yet " + |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 90 | "or takes an argument") % (long, alias_to) |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 91 | |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 92 | long_opts[-1] = long |
| 93 | takes_arg[long] = 0 |
| 94 | |
| 95 | else: |
| 96 | takes_arg[long] = 0 |
| 97 | |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 98 | |
| 99 | # Now enforce some bondage on the long option name, so we can later |
| 100 | # translate it to an attribute name in 'object'. Have to do this a |
| 101 | # bit late to make sure we've removed any trailing '='. |
| 102 | if not longopt_re.match (long): |
| 103 | raise DistutilsGetoptError, \ |
| 104 | ("invalid long option name '%s' " + |
| 105 | "(must be letters, numbers, hyphens only") % long |
| 106 | |
| 107 | attr_name[long] = string.translate (long, longopt_xlate) |
| 108 | if short: |
| 109 | short_opts.append (short) |
| 110 | short2long[short[0]] = long |
| 111 | |
| 112 | # end loop over 'options' |
| 113 | |
| 114 | short_opts = string.join (short_opts) |
| 115 | try: |
| 116 | (opts, args) = getopt.getopt (args, short_opts, long_opts) |
| 117 | except getopt.error, msg: |
| 118 | raise DistutilsArgError, msg |
| 119 | |
| 120 | for (opt, val) in opts: |
| 121 | if len (opt) == 2 and opt[0] == '-': # it's a short option |
| 122 | opt = short2long[opt[1]] |
| 123 | |
| 124 | elif len (opt) > 2 and opt[0:2] == '--': |
| 125 | opt = opt[2:] |
| 126 | |
| 127 | else: |
| 128 | raise RuntimeError, "getopt lies! (bad option string '%s')" % \ |
| 129 | opt |
| 130 | |
| 131 | attr = attr_name[opt] |
| 132 | if takes_arg[opt]: |
| 133 | setattr (object, attr, val) |
| 134 | else: |
| 135 | if val == '': |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 136 | alias = negative_opt.get (opt) |
Greg Ward | a564cc3 | 1999-10-03 20:48:53 +0000 | [diff] [blame] | 137 | if alias: |
| 138 | setattr (object, attr_name[alias], 0) |
| 139 | else: |
| 140 | setattr (object, attr, 1) |
Greg Ward | 2689e3d | 1999-03-22 14:52:19 +0000 | [diff] [blame] | 141 | else: |
| 142 | raise RuntimeError, "getopt lies! (bad value '%s')" % value |
| 143 | |
| 144 | # end loop over options found in 'args' |
| 145 | |
| 146 | return args |
| 147 | |
Greg Ward | 44f8e4e | 1999-12-12 16:54:55 +0000 | [diff] [blame] | 148 | # fancy_getopt() |
| 149 | |
| 150 | |
| 151 | WS_TRANS = string.maketrans (string.whitespace, ' ' * len (string.whitespace)) |
| 152 | |
| 153 | def wrap_text (text, width): |
| 154 | |
| 155 | if text is None: |
| 156 | return [] |
| 157 | if len (text) <= width: |
| 158 | return [text] |
| 159 | |
| 160 | text = string.expandtabs (text) |
| 161 | text = string.translate (text, WS_TRANS) |
| 162 | chunks = re.split (r'( +|-+)', text) |
| 163 | chunks = filter (None, chunks) # ' - ' results in empty strings |
| 164 | lines = [] |
| 165 | |
| 166 | while chunks: |
| 167 | |
| 168 | cur_line = [] # list of chunks (to-be-joined) |
| 169 | cur_len = 0 # length of current line |
| 170 | |
| 171 | while chunks: |
| 172 | l = len (chunks[0]) |
| 173 | if cur_len + l <= width: # can squeeze (at least) this chunk in |
| 174 | cur_line.append (chunks[0]) |
| 175 | del chunks[0] |
| 176 | cur_len = cur_len + l |
| 177 | else: # this line is full |
| 178 | # drop last chunk if all space |
| 179 | if cur_line and cur_line[-1][0] == ' ': |
| 180 | del cur_line[-1] |
| 181 | break |
| 182 | |
| 183 | if chunks: # any chunks left to process? |
| 184 | |
| 185 | # if the current line is still empty, then we had a single |
| 186 | # chunk that's too big too fit on a line -- so we break |
| 187 | # down and break it up at the line width |
| 188 | if cur_len == 0: |
| 189 | cur_line.append (chunks[0][0:width]) |
| 190 | chunks[0] = chunks[0][width:] |
| 191 | |
| 192 | # all-whitespace chunks at the end of a line can be discarded |
| 193 | # (and we know from the re.split above that if a chunk has |
| 194 | # *any* whitespace, it is *all* whitespace) |
| 195 | if chunks[0][0] == ' ': |
| 196 | del chunks[0] |
| 197 | |
| 198 | # and store this line in the list-of-all-lines -- as a single |
| 199 | # string, of course! |
| 200 | lines.append (string.join (cur_line, '')) |
| 201 | |
| 202 | # while chunks |
| 203 | |
| 204 | return lines |
| 205 | |
| 206 | # wrap_text () |
| 207 | |
| 208 | |
| 209 | def generate_help (options, header=None): |
| 210 | """Generate help text (a list of strings, one per suggested line of |
| 211 | output) from an option table.""" |
| 212 | |
| 213 | # Blithely assume the option table is good: probably wouldn't call |
| 214 | # 'generate_help()' unless you've already called 'fancy_getopt()'. |
| 215 | |
| 216 | # First pass: determine maximum length of long option names |
| 217 | max_opt = 0 |
| 218 | for option in options: |
| 219 | long = option[0] |
| 220 | short = option[1] |
| 221 | l = len (long) |
| 222 | if long[-1] == '=': |
| 223 | l = l - 1 |
| 224 | if short is not None: |
| 225 | l = l + 5 # " (-x)" where short == 'x' |
| 226 | if l > max_opt: |
| 227 | max_opt = l |
| 228 | |
| 229 | opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter |
| 230 | |
| 231 | # Typical help block looks like this: |
| 232 | # --foo controls foonabulation |
| 233 | # Help block for longest option looks like this: |
| 234 | # --flimflam set the flim-flam level |
| 235 | # and with wrapped text: |
| 236 | # --flimflam set the flim-flam level (must be between |
| 237 | # 0 and 100, except on Tuesdays) |
| 238 | # Options with short names will have the short name shown (but |
| 239 | # it doesn't contribute to max_opt): |
| 240 | # --foo (-f) controls foonabulation |
| 241 | # If adding the short option would make the left column too wide, |
| 242 | # we push the explanation off to the next line |
| 243 | # --flimflam (-l) |
| 244 | # set the flim-flam level |
| 245 | # Important parameters: |
| 246 | # - 2 spaces before option block start lines |
| 247 | # - 2 dashes for each long option name |
| 248 | # - min. 2 spaces between option and explanation (gutter) |
| 249 | # - 5 characters (incl. space) for short option name |
| 250 | |
| 251 | # Now generate lines of help text. |
| 252 | line_width = 78 # if 80 columns were good enough for |
| 253 | text_width = line_width - opt_width # Jesus, then 78 are good enough for me |
| 254 | big_indent = ' ' * opt_width |
| 255 | if header: |
| 256 | lines = [header] |
| 257 | else: |
| 258 | lines = ['Option summary:'] |
| 259 | |
| 260 | for (long,short,help) in options: |
| 261 | |
| 262 | text = wrap_text (help, text_width) |
| 263 | if long[-1] == '=': |
| 264 | long = long[0:-1] |
| 265 | |
| 266 | # Case 1: no short option at all (makes life easy) |
| 267 | if short is None: |
| 268 | if text: |
| 269 | lines.append (" --%-*s %s" % (max_opt, long, text[0])) |
| 270 | else: |
| 271 | lines.append (" --%-*s " % (max_opt, long)) |
| 272 | |
| 273 | for l in text[1:]: |
| 274 | lines.append (big_indent + l) |
| 275 | |
| 276 | # Case 2: we have a short option, so we have to include it |
| 277 | # just after the long option |
| 278 | else: |
| 279 | opt_names = "%s (-%s)" % (long, short) |
| 280 | if text: |
| 281 | lines.append (" --%-*s %s" % |
| 282 | (max_opt, opt_names, text[0])) |
| 283 | else: |
| 284 | lines.append (" --%-*s" % opt_names) |
| 285 | |
| 286 | # for loop over options |
| 287 | |
| 288 | return lines |
| 289 | |
| 290 | # generate_help () |
| 291 | |
| 292 | |
| 293 | def print_help (options, file=None, header=None): |
| 294 | if file is None: |
| 295 | file = sys.stdout |
| 296 | for line in generate_help (options, header): |
| 297 | file.write (line + "\n") |
| 298 | # print_help () |
| 299 | |
| 300 | |
| 301 | if __name__ == "__main__": |
| 302 | text = """\ |
| 303 | Tra-la-la, supercalifragilisticexpialidocious. |
| 304 | How *do* you spell that odd word, anyways? |
| 305 | (Someone ask Mary -- she'll know [or she'll |
| 306 | say, "How should I know?"].)""" |
| 307 | |
| 308 | for w in (10, 20, 30, 40): |
| 309 | print "width: %d" % w |
| 310 | print string.join (wrap_text (text, w), "\n") |
| 311 | print |