""" CommandLine - Get and parse command line options

    NOTE: This still is very much work in progress !!! Different
    version are likely to be incompatible.

"""

__copyright__ = """\
Copyright (c) 1997,1998 by Marc-Andre Lemburg; mailto:mal@lemburg.com

                      All Rights Reserved.

Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and
supporting documentation.

This software comes with NO WARRANTY. Use at YOUR OWN RISK.
"""

__version__ = '0.6'

import sys,getopt,string,glob,os,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

### Option classes

class Option:

    """ Option base class. Takes no argument.
    """
    default = None
    helptext = ''
    prefix = '-'
    takes_argument = 0
    has_default = 0

    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
        if o.takes_argument:
            if not o.has_default:
                return '%-10s %s' % \
                       (o.prefix+o.name+' arg',o.help)
            else:
                return '%-10s %s (%s)' % \
                       (o.prefix+o.name+' arg',o.help,o.default)
        else:
            return '%-10s %s' % (o.prefix+o.name,o.help)

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 no default value.
    """

### 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 = [Option('-h','show this help text'),
		      Option('--help','show this help text'),
		      Option('--copying','show copyright'),
                      Option('--examples','show examples of usage')]

    # The help layout looks like this:
    # [header]   - defaults to ''
    #
    # [synopsis] - formatted as '<self.name> %s' % self.synopsis
    #
    # [version]  - formatted as 'Version: %s' % self.version, if given
    #
    # options:
    # [options]  - formatted from self.options
    # [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__

    # Do file globbing ?
    globbing = 1

    # 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

    def __init__(self,argv=None):

	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]
        if not self.header:
            self.header = self.name
	self.arguments = argv[1:]
	self.options = self.options + self.preset_options
        self.files = []
        try:
            rc = self.startup()
            if rc is not None:
                raise SystemExit,rc
            rc = self.parse()
            if rc is not None:
                raise SystemExit,rc
            rc = self.main()
            if rc is None:
                rc = 0
        except SystemExit,rc:
            pass
        except:
            traceback.print_exc(20)
            rc = 1
        raise SystemExit,rc

    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

    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

    def help(self,note=''):

        self.print_header()
        if self.synopsis:
            print 'Synopsis:'
            # To remain backward compatible:
            try:
                synopsis = self.synopsis % self.name
            except:
                synopsis = self.synopsis % self.__dict__
            print ' ' + synopsis
	if self.version:
	    print
	    print 'Version:',self.version
	print
	self.print_options()
	if self.about:
	    print string.strip(self.about % self.__dict__)
            print
        if note:
            print '-'*72
            print 'Note:',note
            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:
    #
    def handle_h(self,arg):

    	self.help()
	sys.exit(0)
    
    # Handlers for long options have two underscores in their name
    def handle__help(self,arg):

	self.help()
	sys.exit(0)

    def handle__copying(self,arg):

        self.print_header()
	print self.copyright % self.__dict__
	sys.exit(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
	sys.exit(0)

    def parse(self):

	""" Parse the command line and fill in self.values and self.files.

	    This also calls handlers for the options if given and then
	    checks the files using .check_files().
            
	"""
	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)
	for o,v in optlist:
	    try:
		v = string.atoi(v)
	    except ValueError:
		pass
	    try:
		if o[:2] == '--':
		    m = getattr(self,'handle__'+o[2:])
		else:
		    m = getattr(self,'handle_'+o[1:])
		m(v)
	    except AttributeError:
		if v == '':
		    # count the number of occurances
		    if values.has_key(o):
			values[o] = values[o] + 1
		    else:
			values[o] = 1
		else:
		    values[o] = v
        self.check_files(self.files)

    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 MyCmdline(CommandLine):
	header = 'Test CommandLine'
	version = __version__
	options = [Option('-v','verbose')]
	
	def handle_v(self,arg):
	    print 'VERBOSE, Yeah !'

    cmd = MyCmdline()
    if not cmd.values['-h']:
	cmd.help()
    print 'files:',cmd.files
    print 'Bye...'

if __name__ == '__main__':
    _test()
