You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
16 KiB
635 lines
16 KiB
4 months ago
|
""" CommandLine - Get and parse command line options
|
||
|
|
||
|
NOTE: This still is very much work in progress !!!
|
||
|
|
||
|
Different version are likely to be incompatible.
|
||
|
|
||
|
TODO:
|
||
|
|
||
|
* Incorporate the changes made by (see Inbox)
|
||
|
* Add number range option using srange()
|
||
|
|
||
|
"""
|
||
|
|
||
|
__copyright__ = """\
|
||
|
Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com)
|
||
|
Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com)
|
||
|
See the documentation for further information on copyrights,
|
||
|
or contact the author. All Rights Reserved.
|
||
|
"""
|
||
|
|
||
|
__version__ = '1.2'
|
||
|
|
||
|
import sys, getopt, string, glob, os, re, exceptions, traceback
|
||
|
|
||
|
### Helpers
|
||
|
|
||
|
def _getopt_flags(options):
|
||
|
|
||
|
""" Convert the option list to a getopt flag string and long opt
|
||
|
list
|
||
|
|
||
|
"""
|
||
|
s = []
|
||
|
l = []
|
||
|
for o in options:
|
||
|
if o.prefix == '-':
|
||
|
# short option
|
||
|
s.append(o.name)
|
||
|
if o.takes_argument:
|
||
|
s.append(':')
|
||
|
else:
|
||
|
# long option
|
||
|
if o.takes_argument:
|
||
|
l.append(o.name+'=')
|
||
|
else:
|
||
|
l.append(o.name)
|
||
|
return string.join(s,''),l
|
||
|
|
||
|
def invisible_input(prompt='>>> '):
|
||
|
|
||
|
""" Get raw input from a terminal without echoing the characters to
|
||
|
the terminal, e.g. for password queries.
|
||
|
|
||
|
"""
|
||
|
import getpass
|
||
|
entry = getpass.getpass(prompt)
|
||
|
if entry is None:
|
||
|
raise KeyboardInterrupt
|
||
|
return entry
|
||
|
|
||
|
def fileopen(name, mode='wb', encoding=None):
|
||
|
|
||
|
""" Open a file using mode.
|
||
|
|
||
|
Default mode is 'wb' meaning to open the file for writing in
|
||
|
binary mode. If encoding is given, I/O to and from the file is
|
||
|
transparently encoded using the given encoding.
|
||
|
|
||
|
Files opened for writing are chmod()ed to 0600.
|
||
|
|
||
|
"""
|
||
|
if name == 'stdout':
|
||
|
return sys.stdout
|
||
|
elif name == 'stderr':
|
||
|
return sys.stderr
|
||
|
elif name == 'stdin':
|
||
|
return sys.stdin
|
||
|
else:
|
||
|
if encoding is not None:
|
||
|
import codecs
|
||
|
f = codecs.open(name, mode, encoding)
|
||
|
else:
|
||
|
f = open(name, mode)
|
||
|
if 'w' in mode:
|
||
|
os.chmod(name, 0600)
|
||
|
return f
|
||
|
|
||
|
def option_dict(options):
|
||
|
|
||
|
""" Return a dictionary mapping option names to Option instances.
|
||
|
"""
|
||
|
d = {}
|
||
|
for option in options:
|
||
|
d[option.name] = option
|
||
|
return d
|
||
|
|
||
|
# Alias
|
||
|
getpasswd = invisible_input
|
||
|
|
||
|
_integerRE = re.compile('\s*(-?\d+)\s*$')
|
||
|
_integerRangeRE = re.compile('\s*(-?\d+)\s*-\s*(-?\d+)\s*$')
|
||
|
|
||
|
def srange(s,
|
||
|
|
||
|
split=string.split,integer=_integerRE,
|
||
|
integerRange=_integerRangeRE):
|
||
|
|
||
|
""" Converts a textual representation of integer numbers and ranges
|
||
|
to a Python list.
|
||
|
|
||
|
Supported formats: 2,3,4,2-10,-1 - -3, 5 - -2
|
||
|
|
||
|
Values are appended to the created list in the order specified
|
||
|
in the string.
|
||
|
|
||
|
"""
|
||
|
l = []
|
||
|
append = l.append
|
||
|
for entry in split(s,','):
|
||
|
m = integer.match(entry)
|
||
|
if m:
|
||
|
append(int(m.groups()[0]))
|
||
|
continue
|
||
|
m = integerRange.match(entry)
|
||
|
if m:
|
||
|
start,end = map(int,m.groups())
|
||
|
l[len(l):] = range(start,end+1)
|
||
|
return l
|
||
|
|
||
|
def abspath(path,
|
||
|
|
||
|
expandvars=os.path.expandvars,expanduser=os.path.expanduser,
|
||
|
join=os.path.join,getcwd=os.getcwd):
|
||
|
|
||
|
""" Return the corresponding absolute path for path.
|
||
|
|
||
|
path is expanded in the usual shell ways before
|
||
|
joining it with the current working directory.
|
||
|
|
||
|
"""
|
||
|
try:
|
||
|
path = expandvars(path)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
try:
|
||
|
path = expanduser(path)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
return join(getcwd(), path)
|
||
|
|
||
|
### Option classes
|
||
|
|
||
|
class Option:
|
||
|
|
||
|
""" Option base class. Takes no argument.
|
||
|
|
||
|
"""
|
||
|
default = None
|
||
|
helptext = ''
|
||
|
prefix = '-'
|
||
|
takes_argument = 0
|
||
|
has_default = 0
|
||
|
tab = 15
|
||
|
|
||
|
def __init__(self,name,help=None):
|
||
|
|
||
|
if not name[:1] == '-':
|
||
|
raise TypeError,'option names must start with "-"'
|
||
|
if name[1:2] == '-':
|
||
|
self.prefix = '--'
|
||
|
self.name = name[2:]
|
||
|
else:
|
||
|
self.name = name[1:]
|
||
|
if help:
|
||
|
self.help = help
|
||
|
|
||
|
def __str__(self):
|
||
|
|
||
|
o = self
|
||
|
name = o.prefix + o.name
|
||
|
if o.takes_argument:
|
||
|
name = name + ' arg'
|
||
|
if len(name) > self.tab:
|
||
|
name = name + '\n' + ' ' * (self.tab + 1 + len(o.prefix))
|
||
|
else:
|
||
|
name = '%-*s ' % (self.tab, name)
|
||
|
description = o.help
|
||
|
if o.has_default:
|
||
|
description = description + ' (%s)' % o.default
|
||
|
return '%s %s' % (name, description)
|
||
|
|
||
|
class ArgumentOption(Option):
|
||
|
|
||
|
""" Option that takes an argument.
|
||
|
|
||
|
An optional default argument can be given.
|
||
|
|
||
|
"""
|
||
|
def __init__(self,name,help=None,default=None):
|
||
|
|
||
|
# Basemethod
|
||
|
Option.__init__(self,name,help)
|
||
|
|
||
|
if default is not None:
|
||
|
self.default = default
|
||
|
self.has_default = 1
|
||
|
self.takes_argument = 1
|
||
|
|
||
|
class SwitchOption(Option):
|
||
|
|
||
|
""" Options that can be on or off. Has an optional default value.
|
||
|
|
||
|
"""
|
||
|
def __init__(self,name,help=None,default=None):
|
||
|
|
||
|
# Basemethod
|
||
|
Option.__init__(self,name,help)
|
||
|
|
||
|
if default is not None:
|
||
|
self.default = default
|
||
|
self.has_default = 1
|
||
|
|
||
|
### Application baseclass
|
||
|
|
||
|
class Application:
|
||
|
|
||
|
""" Command line application interface with builtin argument
|
||
|
parsing.
|
||
|
|
||
|
"""
|
||
|
# Options the program accepts (Option instances)
|
||
|
options = []
|
||
|
|
||
|
# Standard settings; these are appended to options in __init__
|
||
|
preset_options = [SwitchOption('-v',
|
||
|
'generate verbose output'),
|
||
|
SwitchOption('-h',
|
||
|
'show this help text'),
|
||
|
SwitchOption('--help',
|
||
|
'show this help text'),
|
||
|
SwitchOption('--debug',
|
||
|
'enable debugging'),
|
||
|
SwitchOption('--copyright',
|
||
|
'show copyright'),
|
||
|
SwitchOption('--examples',
|
||
|
'show examples of usage')]
|
||
|
|
||
|
# The help layout looks like this:
|
||
|
# [header] - defaults to ''
|
||
|
#
|
||
|
# [synopsis] - formatted as '<self.name> %s' % self.synopsis
|
||
|
#
|
||
|
# options:
|
||
|
# [options] - formatted from self.options
|
||
|
#
|
||
|
# [version] - formatted as 'Version:\n %s' % self.version, if given
|
||
|
#
|
||
|
# [about] - defaults to ''
|
||
|
#
|
||
|
# Note: all fields that do not behave as template are formatted
|
||
|
# using the instances dictionary as substitution namespace,
|
||
|
# e.g. %(name)s will be replaced by the applications name.
|
||
|
#
|
||
|
|
||
|
# Header (default to program name)
|
||
|
header = ''
|
||
|
|
||
|
# Name (defaults to program name)
|
||
|
name = ''
|
||
|
|
||
|
# Synopsis (%(name)s is replaced by the program name)
|
||
|
synopsis = '%(name)s [option] files...'
|
||
|
|
||
|
# Version (optional)
|
||
|
version = ''
|
||
|
|
||
|
# General information printed after the possible options (optional)
|
||
|
about = ''
|
||
|
|
||
|
# Examples of usage to show when the --examples option is given (optional)
|
||
|
examples = ''
|
||
|
|
||
|
# Copyright to show
|
||
|
copyright = __copyright__
|
||
|
|
||
|
# Apply file globbing ?
|
||
|
globbing = 1
|
||
|
|
||
|
# Generate debug output ?
|
||
|
debug = 0
|
||
|
|
||
|
# Generate verbose output ?
|
||
|
verbose = 0
|
||
|
|
||
|
# Internal errors to catch
|
||
|
InternalError = exceptions.Exception
|
||
|
|
||
|
# Instance variables:
|
||
|
values = None # Dictionary of passed options (or default values)
|
||
|
# indexed by the options name, e.g. '-h'
|
||
|
files = None # List of passed filenames
|
||
|
optionlist = None # List of passed options
|
||
|
|
||
|
def __init__(self,argv=None):
|
||
|
|
||
|
# Setup application specs
|
||
|
if argv is None:
|
||
|
argv = sys.argv
|
||
|
self.filename = os.path.split(argv[0])[1]
|
||
|
if not self.name:
|
||
|
self.name = os.path.split(self.filename)[1]
|
||
|
else:
|
||
|
self.name = self.name
|
||
|
if not self.header:
|
||
|
self.header = self.name
|
||
|
else:
|
||
|
self.header = self.header
|
||
|
|
||
|
# Init .arguments list
|
||
|
self.arguments = argv[1:]
|
||
|
|
||
|
# Setup Option mapping
|
||
|
self.option_map = option_dict(self.options)
|
||
|
|
||
|
# Append preset options
|
||
|
for option in self.preset_options:
|
||
|
if not self.option_map.has_key(option.name):
|
||
|
self.add_option(option)
|
||
|
|
||
|
# Init .files list
|
||
|
self.files = []
|
||
|
|
||
|
# Start Application
|
||
|
try:
|
||
|
# Process startup
|
||
|
rc = self.startup()
|
||
|
if rc is not None:
|
||
|
raise SystemExit,rc
|
||
|
|
||
|
# Parse command line
|
||
|
rc = self.parse()
|
||
|
if rc is not None:
|
||
|
raise SystemExit,rc
|
||
|
|
||
|
# Start application
|
||
|
rc = self.main()
|
||
|
if rc is None:
|
||
|
rc = 0
|
||
|
|
||
|
except SystemExit,rc:
|
||
|
pass
|
||
|
|
||
|
except KeyboardInterrupt:
|
||
|
print
|
||
|
print '* User Break'
|
||
|
print
|
||
|
rc = 1
|
||
|
|
||
|
except self.InternalError:
|
||
|
print
|
||
|
print '* Internal Error (use --debug to display the traceback)'
|
||
|
if self.debug:
|
||
|
print
|
||
|
traceback.print_exc(20, sys.stdout)
|
||
|
elif self.verbose:
|
||
|
print ' %s: %s' % sys.exc_info()[:2]
|
||
|
print
|
||
|
rc = 1
|
||
|
|
||
|
raise SystemExit,rc
|
||
|
|
||
|
def add_option(self, option):
|
||
|
|
||
|
""" Add a new Option instance to the Application dynamically.
|
||
|
|
||
|
Note that this has to be done *before* .parse() is being
|
||
|
executed.
|
||
|
|
||
|
"""
|
||
|
self.options.append(option)
|
||
|
self.option_map[option.name] = option
|
||
|
|
||
|
def startup(self):
|
||
|
|
||
|
""" Set user defined instance variables.
|
||
|
|
||
|
If this method returns anything other than None, the
|
||
|
process is terminated with the return value as exit code.
|
||
|
|
||
|
"""
|
||
|
return None
|
||
|
|
||
|
def exit(self, rc=0):
|
||
|
|
||
|
""" Exit the program.
|
||
|
|
||
|
rc is used as exit code and passed back to the calling
|
||
|
program. It defaults to 0 which usually means: OK.
|
||
|
|
||
|
"""
|
||
|
raise SystemExit, rc
|
||
|
|
||
|
def parse(self):
|
||
|
|
||
|
""" Parse the command line and fill in self.values and self.files.
|
||
|
|
||
|
After having parsed the options, the remaining command line
|
||
|
arguments are interpreted as files and passed to .handle_files()
|
||
|
for processing.
|
||
|
|
||
|
As final step the option handlers are called in the order
|
||
|
of the options given on the command line.
|
||
|
|
||
|
"""
|
||
|
# Parse arguments
|
||
|
self.values = values = {}
|
||
|
for o in self.options:
|
||
|
if o.has_default:
|
||
|
values[o.prefix+o.name] = o.default
|
||
|
else:
|
||
|
values[o.prefix+o.name] = 0
|
||
|
flags,lflags = _getopt_flags(self.options)
|
||
|
try:
|
||
|
optlist,files = getopt.getopt(self.arguments,flags,lflags)
|
||
|
if self.globbing:
|
||
|
l = []
|
||
|
for f in files:
|
||
|
gf = glob.glob(f)
|
||
|
if not gf:
|
||
|
l.append(f)
|
||
|
else:
|
||
|
l[len(l):] = gf
|
||
|
files = l
|
||
|
self.optionlist = optlist
|
||
|
self.files = files + self.files
|
||
|
except getopt.error,why:
|
||
|
self.help(why)
|
||
|
sys.exit(1)
|
||
|
|
||
|
# Call file handler
|
||
|
rc = self.handle_files(self.files)
|
||
|
if rc is not None:
|
||
|
sys.exit(rc)
|
||
|
|
||
|
# Call option handlers
|
||
|
for optionname, value in optlist:
|
||
|
|
||
|
# Try to convert value to integer
|
||
|
try:
|
||
|
value = string.atoi(value)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
|
||
|
# Find handler and call it (or count the number of option
|
||
|
# instances on the command line)
|
||
|
handlername = 'handle' + string.replace(optionname, '-', '_')
|
||
|
try:
|
||
|
handler = getattr(self, handlername)
|
||
|
except AttributeError:
|
||
|
if value == '':
|
||
|
# count the number of occurrences
|
||
|
if values.has_key(optionname):
|
||
|
values[optionname] = values[optionname] + 1
|
||
|
else:
|
||
|
values[optionname] = 1
|
||
|
else:
|
||
|
values[optionname] = value
|
||
|
else:
|
||
|
rc = handler(value)
|
||
|
if rc is not None:
|
||
|
raise SystemExit, rc
|
||
|
|
||
|
# Apply final file check (for backward compatibility)
|
||
|
rc = self.check_files(self.files)
|
||
|
if rc is not None:
|
||
|
sys.exit(rc)
|
||
|
|
||
|
def check_files(self,filelist):
|
||
|
|
||
|
""" Apply some user defined checks on the files given in filelist.
|
||
|
|
||
|
This may modify filelist in place. A typical application
|
||
|
is checking that at least n files are given.
|
||
|
|
||
|
If this method returns anything other than None, the
|
||
|
process is terminated with the return value as exit code.
|
||
|
|
||
|
"""
|
||
|
return None
|
||
|
|
||
|
def help(self,note=''):
|
||
|
|
||
|
self.print_header()
|
||
|
if self.synopsis:
|
||
|
print 'Synopsis:'
|
||
|
# To remain backward compatible:
|
||
|
try:
|
||
|
synopsis = self.synopsis % self.name
|
||
|
except (NameError, KeyError, TypeError):
|
||
|
synopsis = self.synopsis % self.__dict__
|
||
|
print ' ' + synopsis
|
||
|
print
|
||
|
self.print_options()
|
||
|
if self.version:
|
||
|
print 'Version:'
|
||
|
print ' %s' % self.version
|
||
|
print
|
||
|
if self.about:
|
||
|
print string.strip(self.about % self.__dict__)
|
||
|
print
|
||
|
if note:
|
||
|
print '-'*72
|
||
|
print 'Note:',note
|
||
|
print
|
||
|
|
||
|
def notice(self,note):
|
||
|
|
||
|
print '-'*72
|
||
|
print 'Note:',note
|
||
|
print '-'*72
|
||
|
print
|
||
|
|
||
|
def print_header(self):
|
||
|
|
||
|
print '-'*72
|
||
|
print self.header % self.__dict__
|
||
|
print '-'*72
|
||
|
print
|
||
|
|
||
|
def print_options(self):
|
||
|
|
||
|
options = self.options
|
||
|
print 'Options and default settings:'
|
||
|
if not options:
|
||
|
print ' None'
|
||
|
return
|
||
|
long = filter(lambda x: x.prefix == '--', options)
|
||
|
short = filter(lambda x: x.prefix == '-', options)
|
||
|
items = short + long
|
||
|
for o in options:
|
||
|
print ' ',o
|
||
|
print
|
||
|
|
||
|
#
|
||
|
# Example handlers:
|
||
|
#
|
||
|
# If a handler returns anything other than None, processing stops
|
||
|
# and the return value is passed to sys.exit() as argument.
|
||
|
#
|
||
|
|
||
|
# File handler
|
||
|
def handle_files(self,files):
|
||
|
|
||
|
""" This may process the files list in place.
|
||
|
"""
|
||
|
return None
|
||
|
|
||
|
# Short option handler
|
||
|
def handle_h(self,arg):
|
||
|
|
||
|
self.help()
|
||
|
return 0
|
||
|
|
||
|
def handle_v(self, value):
|
||
|
|
||
|
""" Turn on verbose output.
|
||
|
"""
|
||
|
self.verbose = 1
|
||
|
|
||
|
# Handlers for long options have two underscores in their name
|
||
|
def handle__help(self,arg):
|
||
|
|
||
|
self.help()
|
||
|
return 0
|
||
|
|
||
|
def handle__debug(self,arg):
|
||
|
|
||
|
self.debug = 1
|
||
|
# We don't want to catch internal errors:
|
||
|
self.InternalError = None
|
||
|
|
||
|
def handle__copyright(self,arg):
|
||
|
|
||
|
self.print_header()
|
||
|
print string.strip(self.copyright % self.__dict__)
|
||
|
print
|
||
|
return 0
|
||
|
|
||
|
def handle__examples(self,arg):
|
||
|
|
||
|
self.print_header()
|
||
|
if self.examples:
|
||
|
print 'Examples:'
|
||
|
print
|
||
|
print string.strip(self.examples % self.__dict__)
|
||
|
print
|
||
|
else:
|
||
|
print 'No examples available.'
|
||
|
print
|
||
|
return 0
|
||
|
|
||
|
def main(self):
|
||
|
|
||
|
""" Override this method as program entry point.
|
||
|
|
||
|
The return value is passed to sys.exit() as argument. If
|
||
|
it is None, 0 is assumed (meaning OK). Unhandled
|
||
|
exceptions are reported with exit status code 1 (see
|
||
|
__init__ for further details).
|
||
|
|
||
|
"""
|
||
|
return None
|
||
|
|
||
|
# Alias
|
||
|
CommandLine = Application
|
||
|
|
||
|
def _test():
|
||
|
|
||
|
class MyApplication(Application):
|
||
|
header = 'Test Application'
|
||
|
version = __version__
|
||
|
options = [Option('-v','verbose')]
|
||
|
|
||
|
def handle_v(self,arg):
|
||
|
print 'VERBOSE, Yeah !'
|
||
|
|
||
|
cmd = MyApplication()
|
||
|
if not cmd.values['-h']:
|
||
|
cmd.help()
|
||
|
print 'files:',cmd.files
|
||
|
print 'Bye...'
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
_test()
|