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