blob: 715bc49b58d1c23a20a02604ae188cdbbf631f0a [file] [log] [blame]
""" 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()
"""
from __future__ import print_function
__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, glob, os, re, 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 ''.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, 0o600)
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,
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 s.split(','):
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 = BaseException
# 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 option.name in self.option_map:
self.add_option(option)
# Init .files list
self.files = []
# Start Application
rc = 0
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 as rcException:
rc = rcException
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 as 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 = int(value)
except ValueError:
pass
# Find handler and call it (or count the number of option
# instances on the command line)
handlername = 'handle' + optionname.replace('-', '_')
try:
handler = getattr(self, handlername)
except AttributeError:
if value == '':
# count the number of occurances
if optionname in values:
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:
about = self.about % self.__dict__
print(about.strip())
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
int = [x for x in options if x.prefix == '--']
short = [x for x in options if x.prefix == '-']
items = short + int
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:
class NoErrorToCatch(Exception): pass
self.InternalError = NoErrorToCatch
def handle__copyright(self,arg):
self.print_header()
copyright = self.copyright % self.__dict__
print(copyright.strip())
print()
return 0
def handle__examples(self,arg):
self.print_header()
if self.examples:
print('Examples:')
print()
examples = self.examples % self.__dict__
print(examples.strip())
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()