]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | """ CommandLine - Get and parse command line options\r |
2 | \r | |
3 | NOTE: This still is very much work in progress !!!\r | |
4 | \r | |
5 | Different version are likely to be incompatible.\r | |
6 | \r | |
7 | TODO:\r | |
8 | \r | |
9 | * Incorporate the changes made by (see Inbox)\r | |
10 | * Add number range option using srange()\r | |
11 | \r | |
12 | """\r | |
13 | \r | |
14 | __copyright__ = """\\r | |
15 | Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com)\r | |
16 | Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com)\r | |
17 | See the documentation for further information on copyrights,\r | |
18 | or contact the author. All Rights Reserved.\r | |
19 | """\r | |
20 | \r | |
21 | __version__ = '1.2'\r | |
22 | \r | |
23 | import sys, getopt, string, glob, os, re, exceptions, traceback\r | |
24 | \r | |
25 | ### Helpers\r | |
26 | \r | |
27 | def _getopt_flags(options):\r | |
28 | \r | |
29 | """ Convert the option list to a getopt flag string and long opt\r | |
30 | list\r | |
31 | \r | |
32 | """\r | |
33 | s = []\r | |
34 | l = []\r | |
35 | for o in options:\r | |
36 | if o.prefix == '-':\r | |
37 | # short option\r | |
38 | s.append(o.name)\r | |
39 | if o.takes_argument:\r | |
40 | s.append(':')\r | |
41 | else:\r | |
42 | # long option\r | |
43 | if o.takes_argument:\r | |
44 | l.append(o.name+'=')\r | |
45 | else:\r | |
46 | l.append(o.name)\r | |
47 | return string.join(s,''),l\r | |
48 | \r | |
49 | def invisible_input(prompt='>>> '):\r | |
50 | \r | |
51 | """ Get raw input from a terminal without echoing the characters to\r | |
52 | the terminal, e.g. for password queries.\r | |
53 | \r | |
54 | """\r | |
55 | import getpass\r | |
56 | entry = getpass.getpass(prompt)\r | |
57 | if entry is None:\r | |
58 | raise KeyboardInterrupt\r | |
59 | return entry\r | |
60 | \r | |
61 | def fileopen(name, mode='wb', encoding=None):\r | |
62 | \r | |
63 | """ Open a file using mode.\r | |
64 | \r | |
65 | Default mode is 'wb' meaning to open the file for writing in\r | |
66 | binary mode. If encoding is given, I/O to and from the file is\r | |
67 | transparently encoded using the given encoding.\r | |
68 | \r | |
69 | Files opened for writing are chmod()ed to 0600.\r | |
70 | \r | |
71 | """\r | |
72 | if name == 'stdout':\r | |
73 | return sys.stdout\r | |
74 | elif name == 'stderr':\r | |
75 | return sys.stderr\r | |
76 | elif name == 'stdin':\r | |
77 | return sys.stdin\r | |
78 | else:\r | |
79 | if encoding is not None:\r | |
80 | import codecs\r | |
81 | f = codecs.open(name, mode, encoding)\r | |
82 | else:\r | |
83 | f = open(name, mode)\r | |
84 | if 'w' in mode:\r | |
85 | os.chmod(name, 0600)\r | |
86 | return f\r | |
87 | \r | |
88 | def option_dict(options):\r | |
89 | \r | |
90 | """ Return a dictionary mapping option names to Option instances.\r | |
91 | """\r | |
92 | d = {}\r | |
93 | for option in options:\r | |
94 | d[option.name] = option\r | |
95 | return d\r | |
96 | \r | |
97 | # Alias\r | |
98 | getpasswd = invisible_input\r | |
99 | \r | |
100 | _integerRE = re.compile('\s*(-?\d+)\s*$')\r | |
101 | _integerRangeRE = re.compile('\s*(-?\d+)\s*-\s*(-?\d+)\s*$')\r | |
102 | \r | |
103 | def srange(s,\r | |
104 | \r | |
105 | split=string.split,integer=_integerRE,\r | |
106 | integerRange=_integerRangeRE):\r | |
107 | \r | |
108 | """ Converts a textual representation of integer numbers and ranges\r | |
109 | to a Python list.\r | |
110 | \r | |
111 | Supported formats: 2,3,4,2-10,-1 - -3, 5 - -2\r | |
112 | \r | |
113 | Values are appended to the created list in the order specified\r | |
114 | in the string.\r | |
115 | \r | |
116 | """\r | |
117 | l = []\r | |
118 | append = l.append\r | |
119 | for entry in split(s,','):\r | |
120 | m = integer.match(entry)\r | |
121 | if m:\r | |
122 | append(int(m.groups()[0]))\r | |
123 | continue\r | |
124 | m = integerRange.match(entry)\r | |
125 | if m:\r | |
126 | start,end = map(int,m.groups())\r | |
127 | l[len(l):] = range(start,end+1)\r | |
128 | return l\r | |
129 | \r | |
130 | def abspath(path,\r | |
131 | \r | |
132 | expandvars=os.path.expandvars,expanduser=os.path.expanduser,\r | |
133 | join=os.path.join,getcwd=os.getcwd):\r | |
134 | \r | |
135 | """ Return the corresponding absolute path for path.\r | |
136 | \r | |
137 | path is expanded in the usual shell ways before\r | |
138 | joining it with the current working directory.\r | |
139 | \r | |
140 | """\r | |
141 | try:\r | |
142 | path = expandvars(path)\r | |
143 | except AttributeError:\r | |
144 | pass\r | |
145 | try:\r | |
146 | path = expanduser(path)\r | |
147 | except AttributeError:\r | |
148 | pass\r | |
149 | return join(getcwd(), path)\r | |
150 | \r | |
151 | ### Option classes\r | |
152 | \r | |
153 | class Option:\r | |
154 | \r | |
155 | """ Option base class. Takes no argument.\r | |
156 | \r | |
157 | """\r | |
158 | default = None\r | |
159 | helptext = ''\r | |
160 | prefix = '-'\r | |
161 | takes_argument = 0\r | |
162 | has_default = 0\r | |
163 | tab = 15\r | |
164 | \r | |
165 | def __init__(self,name,help=None):\r | |
166 | \r | |
167 | if not name[:1] == '-':\r | |
168 | raise TypeError,'option names must start with "-"'\r | |
169 | if name[1:2] == '-':\r | |
170 | self.prefix = '--'\r | |
171 | self.name = name[2:]\r | |
172 | else:\r | |
173 | self.name = name[1:]\r | |
174 | if help:\r | |
175 | self.help = help\r | |
176 | \r | |
177 | def __str__(self):\r | |
178 | \r | |
179 | o = self\r | |
180 | name = o.prefix + o.name\r | |
181 | if o.takes_argument:\r | |
182 | name = name + ' arg'\r | |
183 | if len(name) > self.tab:\r | |
184 | name = name + '\n' + ' ' * (self.tab + 1 + len(o.prefix))\r | |
185 | else:\r | |
186 | name = '%-*s ' % (self.tab, name)\r | |
187 | description = o.help\r | |
188 | if o.has_default:\r | |
189 | description = description + ' (%s)' % o.default\r | |
190 | return '%s %s' % (name, description)\r | |
191 | \r | |
192 | class ArgumentOption(Option):\r | |
193 | \r | |
194 | """ Option that takes an argument.\r | |
195 | \r | |
196 | An optional default argument can be given.\r | |
197 | \r | |
198 | """\r | |
199 | def __init__(self,name,help=None,default=None):\r | |
200 | \r | |
201 | # Basemethod\r | |
202 | Option.__init__(self,name,help)\r | |
203 | \r | |
204 | if default is not None:\r | |
205 | self.default = default\r | |
206 | self.has_default = 1\r | |
207 | self.takes_argument = 1\r | |
208 | \r | |
209 | class SwitchOption(Option):\r | |
210 | \r | |
211 | """ Options that can be on or off. Has an optional default value.\r | |
212 | \r | |
213 | """\r | |
214 | def __init__(self,name,help=None,default=None):\r | |
215 | \r | |
216 | # Basemethod\r | |
217 | Option.__init__(self,name,help)\r | |
218 | \r | |
219 | if default is not None:\r | |
220 | self.default = default\r | |
221 | self.has_default = 1\r | |
222 | \r | |
223 | ### Application baseclass\r | |
224 | \r | |
225 | class Application:\r | |
226 | \r | |
227 | """ Command line application interface with builtin argument\r | |
228 | parsing.\r | |
229 | \r | |
230 | """\r | |
231 | # Options the program accepts (Option instances)\r | |
232 | options = []\r | |
233 | \r | |
234 | # Standard settings; these are appended to options in __init__\r | |
235 | preset_options = [SwitchOption('-v',\r | |
236 | 'generate verbose output'),\r | |
237 | SwitchOption('-h',\r | |
238 | 'show this help text'),\r | |
239 | SwitchOption('--help',\r | |
240 | 'show this help text'),\r | |
241 | SwitchOption('--debug',\r | |
242 | 'enable debugging'),\r | |
243 | SwitchOption('--copyright',\r | |
244 | 'show copyright'),\r | |
245 | SwitchOption('--examples',\r | |
246 | 'show examples of usage')]\r | |
247 | \r | |
248 | # The help layout looks like this:\r | |
249 | # [header] - defaults to ''\r | |
250 | #\r | |
251 | # [synopsis] - formatted as '<self.name> %s' % self.synopsis\r | |
252 | #\r | |
253 | # options:\r | |
254 | # [options] - formatted from self.options\r | |
255 | #\r | |
256 | # [version] - formatted as 'Version:\n %s' % self.version, if given\r | |
257 | #\r | |
258 | # [about] - defaults to ''\r | |
259 | #\r | |
260 | # Note: all fields that do not behave as template are formatted\r | |
261 | # using the instances dictionary as substitution namespace,\r | |
262 | # e.g. %(name)s will be replaced by the applications name.\r | |
263 | #\r | |
264 | \r | |
265 | # Header (default to program name)\r | |
266 | header = ''\r | |
267 | \r | |
268 | # Name (defaults to program name)\r | |
269 | name = ''\r | |
270 | \r | |
271 | # Synopsis (%(name)s is replaced by the program name)\r | |
272 | synopsis = '%(name)s [option] files...'\r | |
273 | \r | |
274 | # Version (optional)\r | |
275 | version = ''\r | |
276 | \r | |
277 | # General information printed after the possible options (optional)\r | |
278 | about = ''\r | |
279 | \r | |
280 | # Examples of usage to show when the --examples option is given (optional)\r | |
281 | examples = ''\r | |
282 | \r | |
283 | # Copyright to show\r | |
284 | copyright = __copyright__\r | |
285 | \r | |
286 | # Apply file globbing ?\r | |
287 | globbing = 1\r | |
288 | \r | |
289 | # Generate debug output ?\r | |
290 | debug = 0\r | |
291 | \r | |
292 | # Generate verbose output ?\r | |
293 | verbose = 0\r | |
294 | \r | |
295 | # Internal errors to catch\r | |
296 | InternalError = exceptions.Exception\r | |
297 | \r | |
298 | # Instance variables:\r | |
299 | values = None # Dictionary of passed options (or default values)\r | |
300 | # indexed by the options name, e.g. '-h'\r | |
301 | files = None # List of passed filenames\r | |
302 | optionlist = None # List of passed options\r | |
303 | \r | |
304 | def __init__(self,argv=None):\r | |
305 | \r | |
306 | # Setup application specs\r | |
307 | if argv is None:\r | |
308 | argv = sys.argv\r | |
309 | self.filename = os.path.split(argv[0])[1]\r | |
310 | if not self.name:\r | |
311 | self.name = os.path.split(self.filename)[1]\r | |
312 | else:\r | |
313 | self.name = self.name\r | |
314 | if not self.header:\r | |
315 | self.header = self.name\r | |
316 | else:\r | |
317 | self.header = self.header\r | |
318 | \r | |
319 | # Init .arguments list\r | |
320 | self.arguments = argv[1:]\r | |
321 | \r | |
322 | # Setup Option mapping\r | |
323 | self.option_map = option_dict(self.options)\r | |
324 | \r | |
325 | # Append preset options\r | |
326 | for option in self.preset_options:\r | |
327 | if not self.option_map.has_key(option.name):\r | |
328 | self.add_option(option)\r | |
329 | \r | |
330 | # Init .files list\r | |
331 | self.files = []\r | |
332 | \r | |
333 | # Start Application\r | |
334 | try:\r | |
335 | # Process startup\r | |
336 | rc = self.startup()\r | |
337 | if rc is not None:\r | |
338 | raise SystemExit,rc\r | |
339 | \r | |
340 | # Parse command line\r | |
341 | rc = self.parse()\r | |
342 | if rc is not None:\r | |
343 | raise SystemExit,rc\r | |
344 | \r | |
345 | # Start application\r | |
346 | rc = self.main()\r | |
347 | if rc is None:\r | |
348 | rc = 0\r | |
349 | \r | |
350 | except SystemExit,rc:\r | |
351 | pass\r | |
352 | \r | |
353 | except KeyboardInterrupt:\r | |
354 | print\r | |
355 | print '* User Break'\r | |
356 | print\r | |
357 | rc = 1\r | |
358 | \r | |
359 | except self.InternalError:\r | |
360 | print\r | |
361 | print '* Internal Error (use --debug to display the traceback)'\r | |
362 | if self.debug:\r | |
363 | print\r | |
364 | traceback.print_exc(20, sys.stdout)\r | |
365 | elif self.verbose:\r | |
366 | print ' %s: %s' % sys.exc_info()[:2]\r | |
367 | print\r | |
368 | rc = 1\r | |
369 | \r | |
370 | raise SystemExit,rc\r | |
371 | \r | |
372 | def add_option(self, option):\r | |
373 | \r | |
374 | """ Add a new Option instance to the Application dynamically.\r | |
375 | \r | |
376 | Note that this has to be done *before* .parse() is being\r | |
377 | executed.\r | |
378 | \r | |
379 | """\r | |
380 | self.options.append(option)\r | |
381 | self.option_map[option.name] = option\r | |
382 | \r | |
383 | def startup(self):\r | |
384 | \r | |
385 | """ Set user defined instance variables.\r | |
386 | \r | |
387 | If this method returns anything other than None, the\r | |
388 | process is terminated with the return value as exit code.\r | |
389 | \r | |
390 | """\r | |
391 | return None\r | |
392 | \r | |
393 | def exit(self, rc=0):\r | |
394 | \r | |
395 | """ Exit the program.\r | |
396 | \r | |
397 | rc is used as exit code and passed back to the calling\r | |
398 | program. It defaults to 0 which usually means: OK.\r | |
399 | \r | |
400 | """\r | |
401 | raise SystemExit, rc\r | |
402 | \r | |
403 | def parse(self):\r | |
404 | \r | |
405 | """ Parse the command line and fill in self.values and self.files.\r | |
406 | \r | |
407 | After having parsed the options, the remaining command line\r | |
408 | arguments are interpreted as files and passed to .handle_files()\r | |
409 | for processing.\r | |
410 | \r | |
411 | As final step the option handlers are called in the order\r | |
412 | of the options given on the command line.\r | |
413 | \r | |
414 | """\r | |
415 | # Parse arguments\r | |
416 | self.values = values = {}\r | |
417 | for o in self.options:\r | |
418 | if o.has_default:\r | |
419 | values[o.prefix+o.name] = o.default\r | |
420 | else:\r | |
421 | values[o.prefix+o.name] = 0\r | |
422 | flags,lflags = _getopt_flags(self.options)\r | |
423 | try:\r | |
424 | optlist,files = getopt.getopt(self.arguments,flags,lflags)\r | |
425 | if self.globbing:\r | |
426 | l = []\r | |
427 | for f in files:\r | |
428 | gf = glob.glob(f)\r | |
429 | if not gf:\r | |
430 | l.append(f)\r | |
431 | else:\r | |
432 | l[len(l):] = gf\r | |
433 | files = l\r | |
434 | self.optionlist = optlist\r | |
435 | self.files = files + self.files\r | |
436 | except getopt.error,why:\r | |
437 | self.help(why)\r | |
438 | sys.exit(1)\r | |
439 | \r | |
440 | # Call file handler\r | |
441 | rc = self.handle_files(self.files)\r | |
442 | if rc is not None:\r | |
443 | sys.exit(rc)\r | |
444 | \r | |
445 | # Call option handlers\r | |
446 | for optionname, value in optlist:\r | |
447 | \r | |
448 | # Try to convert value to integer\r | |
449 | try:\r | |
450 | value = string.atoi(value)\r | |
451 | except ValueError:\r | |
452 | pass\r | |
453 | \r | |
454 | # Find handler and call it (or count the number of option\r | |
455 | # instances on the command line)\r | |
456 | handlername = 'handle' + string.replace(optionname, '-', '_')\r | |
457 | try:\r | |
458 | handler = getattr(self, handlername)\r | |
459 | except AttributeError:\r | |
460 | if value == '':\r | |
461 | # count the number of occurances\r | |
462 | if values.has_key(optionname):\r | |
463 | values[optionname] = values[optionname] + 1\r | |
464 | else:\r | |
465 | values[optionname] = 1\r | |
466 | else:\r | |
467 | values[optionname] = value\r | |
468 | else:\r | |
469 | rc = handler(value)\r | |
470 | if rc is not None:\r | |
471 | raise SystemExit, rc\r | |
472 | \r | |
473 | # Apply final file check (for backward compatibility)\r | |
474 | rc = self.check_files(self.files)\r | |
475 | if rc is not None:\r | |
476 | sys.exit(rc)\r | |
477 | \r | |
478 | def check_files(self,filelist):\r | |
479 | \r | |
480 | """ Apply some user defined checks on the files given in filelist.\r | |
481 | \r | |
482 | This may modify filelist in place. A typical application\r | |
483 | is checking that at least n files are given.\r | |
484 | \r | |
485 | If this method returns anything other than None, the\r | |
486 | process is terminated with the return value as exit code.\r | |
487 | \r | |
488 | """\r | |
489 | return None\r | |
490 | \r | |
491 | def help(self,note=''):\r | |
492 | \r | |
493 | self.print_header()\r | |
494 | if self.synopsis:\r | |
495 | print 'Synopsis:'\r | |
496 | # To remain backward compatible:\r | |
497 | try:\r | |
498 | synopsis = self.synopsis % self.name\r | |
499 | except (NameError, KeyError, TypeError):\r | |
500 | synopsis = self.synopsis % self.__dict__\r | |
501 | print ' ' + synopsis\r | |
502 | print\r | |
503 | self.print_options()\r | |
504 | if self.version:\r | |
505 | print 'Version:'\r | |
506 | print ' %s' % self.version\r | |
507 | print\r | |
508 | if self.about:\r | |
509 | print string.strip(self.about % self.__dict__)\r | |
510 | print\r | |
511 | if note:\r | |
512 | print '-'*72\r | |
513 | print 'Note:',note\r | |
514 | print\r | |
515 | \r | |
516 | def notice(self,note):\r | |
517 | \r | |
518 | print '-'*72\r | |
519 | print 'Note:',note\r | |
520 | print '-'*72\r | |
521 | print\r | |
522 | \r | |
523 | def print_header(self):\r | |
524 | \r | |
525 | print '-'*72\r | |
526 | print self.header % self.__dict__\r | |
527 | print '-'*72\r | |
528 | print\r | |
529 | \r | |
530 | def print_options(self):\r | |
531 | \r | |
532 | options = self.options\r | |
533 | print 'Options and default settings:'\r | |
534 | if not options:\r | |
535 | print ' None'\r | |
536 | return\r | |
537 | long = filter(lambda x: x.prefix == '--', options)\r | |
538 | short = filter(lambda x: x.prefix == '-', options)\r | |
539 | items = short + long\r | |
540 | for o in options:\r | |
541 | print ' ',o\r | |
542 | print\r | |
543 | \r | |
544 | #\r | |
545 | # Example handlers:\r | |
546 | #\r | |
547 | # If a handler returns anything other than None, processing stops\r | |
548 | # and the return value is passed to sys.exit() as argument.\r | |
549 | #\r | |
550 | \r | |
551 | # File handler\r | |
552 | def handle_files(self,files):\r | |
553 | \r | |
554 | """ This may process the files list in place.\r | |
555 | """\r | |
556 | return None\r | |
557 | \r | |
558 | # Short option handler\r | |
559 | def handle_h(self,arg):\r | |
560 | \r | |
561 | self.help()\r | |
562 | return 0\r | |
563 | \r | |
564 | def handle_v(self, value):\r | |
565 | \r | |
566 | """ Turn on verbose output.\r | |
567 | """\r | |
568 | self.verbose = 1\r | |
569 | \r | |
570 | # Handlers for long options have two underscores in their name\r | |
571 | def handle__help(self,arg):\r | |
572 | \r | |
573 | self.help()\r | |
574 | return 0\r | |
575 | \r | |
576 | def handle__debug(self,arg):\r | |
577 | \r | |
578 | self.debug = 1\r | |
579 | # We don't want to catch internal errors:\r | |
580 | self.InternalError = None\r | |
581 | \r | |
582 | def handle__copyright(self,arg):\r | |
583 | \r | |
584 | self.print_header()\r | |
585 | print string.strip(self.copyright % self.__dict__)\r | |
586 | print\r | |
587 | return 0\r | |
588 | \r | |
589 | def handle__examples(self,arg):\r | |
590 | \r | |
591 | self.print_header()\r | |
592 | if self.examples:\r | |
593 | print 'Examples:'\r | |
594 | print\r | |
595 | print string.strip(self.examples % self.__dict__)\r | |
596 | print\r | |
597 | else:\r | |
598 | print 'No examples available.'\r | |
599 | print\r | |
600 | return 0\r | |
601 | \r | |
602 | def main(self):\r | |
603 | \r | |
604 | """ Override this method as program entry point.\r | |
605 | \r | |
606 | The return value is passed to sys.exit() as argument. If\r | |
607 | it is None, 0 is assumed (meaning OK). Unhandled\r | |
608 | exceptions are reported with exit status code 1 (see\r | |
609 | __init__ for further details).\r | |
610 | \r | |
611 | """\r | |
612 | return None\r | |
613 | \r | |
614 | # Alias\r | |
615 | CommandLine = Application\r | |
616 | \r | |
617 | def _test():\r | |
618 | \r | |
619 | class MyApplication(Application):\r | |
620 | header = 'Test Application'\r | |
621 | version = __version__\r | |
622 | options = [Option('-v','verbose')]\r | |
623 | \r | |
624 | def handle_v(self,arg):\r | |
625 | print 'VERBOSE, Yeah !'\r | |
626 | \r | |
627 | cmd = MyApplication()\r | |
628 | if not cmd.values['-h']:\r | |
629 | cmd.help()\r | |
630 | print 'files:',cmd.files\r | |
631 | print 'Bye...'\r | |
632 | \r | |
633 | if __name__ == '__main__':\r | |
634 | _test()\r |