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