# simpleConfig.py
"""
    Simple configuration file parser.  You pass the name of the configuration
    file and an optional valid configuration parameter and it returns a
    dictionary of the options defined in the configuration file.  For example:

        VALID_OPTS = ('host', 'user', 'files', 'remotedir', 'logfile')

        conf = simpleConfig.options( 'myconfigfile.conf', VALID_OPTS )
        for key in conf:
            print "%s is %s" % (key, conf[key])

    The configuration file should be in the format:

        parameter = option [option...]

    Long lines can be continued to the next line by putting a backslash at the
    end of the line:

        parameter = option1 option2 option3 option4 \
                    option5 option6 option7

    Parameters that have multiple options are returned as a list. If only a
    single option it is returned as a string.

    The optional second argument to simpleConfig.options() can be a list,
    tuple, or dictionary.  If you want to pass default values for parameters
    then use a dictionary.  For example you could change VALID_OPTS above to a
    dictionary using 'None' for options with no default:

        VALID_OPTS = {'host' : '127.0.0.1', 'user' : 'nobody',
                      'files' : None, 'remotedir' : '/tmp',
                      'logfile' : '/logs/mylog.log'}

    NOTE: if you omit or pass 'None' for the second argument (valid options)
    then it will return a dictionary of all 'key = value' lines in the
    configuration file.  In other words ALL key=value options will be valud
    and you will NOT get an error about unrecognized option.
    For example:

        conf = simpleConfig.options( 'myconfigfile.conf' )

    will return all configuration parameters defined in the config file.

            ------------------------------------------------

    If the configuration file has a parameter defined as:

        overwrite = true
        edit = false

    Then the value for 'overwrite' will be returned as the boolean value 'True'
    and 'edit' as the boolean value 'False'. If you want the actual string
    value "true" or "false" then put the option in quotes:

        overwrite = "true"
        edit = "false"

    You can also have values with embedded blanks by quoting:

        message = "This is a test message."

    And You can use quotes to span multiple lines:

        param = "...................
                ....................
                ....................
                "
    Use a back tic (`) for literal quotes for a parameter.... <finish this>....
"""

__all__ = ["Error", "options"]

import os

BACKTIC = '`'


class Error( Exception ):
    def __init__( self, msg='' ):
        self.errmsg = msg
        Exception.__init__( self,  msg )

    def __repr__( self ):
        return self.errmsg


def options( fname, validopts=None ):
    if not os.path.exists( fname ):
        raise Error( 'file does not exist: %s' % fname )
    if validopts and not (isinstance( validopts, list ) or
        isinstance( validopts, tuple ) or
        isinstance( validopts, dict )):
        raise Error( "options(): Second argument must be list, tuple or dictionary")
    input = None
    try:
        input = open( fname )
        lines = input.readlines()
    except (IOError, OSError), err:
        if input is not None:
            input.close()
        raise Error( str(err) )
    if not lines:
        raise Error( "Empty configuration file.")
    input.close()
    return parse( lines, validopts )


def boolean_check( value ):
    """ check for true/false and convert to boolean value """
    upperValue = value.upper()
    if upperValue == 'TRUE':
        value = True
    elif upperValue == 'FALSE':
        value = False
    return value


def get_values( value, openquote ):
    """
    check each arg for quotes around it or to combine multiple
    args within quotes into one argument.  Also allows for quotes extending
    over multiple lines.

    returns a 2-tuple of whether a quote is open (the actual quote character)
    or None if quote is not open and List of values.
    """
    values = value.split()
    retvalues = []
    openindex = 0
    i = 0
    while i < len(values):
        if openquote:
            if values[i][-1] == openquote:
                onearg = ' '.join( values[openindex:i+1] )
                retvalues.append( onearg.strip(openquote) )
                openquote = openindex = None
        elif values[i][0] in "\"'":
            if len( values[i] ) > 1 and values[i][0] == values[i][-1]:
                # strip off quotes
                retvalues.append( values[i][1:-1] )
            else:
                # check for quotes enclosing multiple args
                openquote = values[i][0]
                openindex = i
        else:
            value = boolean_check( values[i] )
            retvalues.append( value )
        i += 1

    if openquote:
        onearg = ' '.join( values[openindex:] )
        retvalues.append( onearg.lstrip(openquote) )
    return (openquote, retvalues)


def parse( lines, validopts ):
    """
    parse the configuration file and return dictionary of parameters
    and their arguments.
    """
    if isinstance( validopts, dict ):
        d = validopts
    else:
        d = {}
    continuation_line = False
    openquote = None
    values = []
    save_values = []
    lineno = 0

    try:
        for line in lines:
            lineno += 1
            if openquote == BACKTIC:
                index = line.find(BACKTIC)
                if index == -1:
                    # keep collecting lines
                    save_values.append(line)
                else:
                    save_values.append(line[:index])
                    d[key] = "".join(save_values)
                    save_values = []
                    openquote = None
                continue
            # ignore blank lines and comments
            if line.strip() == "" or line.lstrip()[0] == '#':
                continue
            if openquote:
                (openquote, values) = get_values( line.strip(), openquote )
                if openquote:
                    save_values.extend( values )
                else:
                    save_values.extend( values )
                    d[ key ] = ' '.join(save_values)
                    save_values = []
            elif continuation_line:
                line = line.strip()
                if line[-1] == '\\':
                    # remove ending '\' and any whitespace before it
                    line = line[:-1].rstrip()
                else:
                    continuation_line = False
                if line:
                    (openquote, values) = get_values(line, None)
                    ## what if openquote is TRUE ????????
                    d[ key ].extend( values )
            else:
                if line.rstrip()[-1] == '\\':
                    line = line.strip()
                    continuation_line = True
                    line = line[:-1].rstrip()
                (key, value) = line.split('=')
                key = key.strip()
                if validopts and key not in validopts:
                    raise ValueError
                value = value.lstrip()
                if value and value[0] == BACKTIC:
                    value = value[1:]
                    index = value.find(BACKTIC)
                    if index >= 0:
                        # closing back tic is on the same line
                        d[key] = value[:index]
                    else:
                        # back tic quote spans multiple lines
                        openquote = BACKTIC
                        save_values = []
                        save_values.append(value)
                    continue
                (openquote, values) = get_values( value.rstrip(), None )
                if not openquote:
                    d[key] = values
                else:
                    save_values = []
                    save_values.extend( values )
    except ValueError:
        raise Error( "Bad config line(%d): %s" % (lineno, line) )

    if openquote is not None:
        raise Error("Missing closing quote.")
    # if option has only 1 value convert from list to string
    if validopts:
        for key in validopts:
            if key not in d:
                d[key] = None
            elif d[key] and len(d[key]) == 1:
                # convert 1 item list to a simple string
                d[key] = d[key][0]
    else:
        for key in d:
            if len(d[key]) == 1:
                #convert 1 item list to a simple string
                d[key] = d[key][0]

    return d

if __name__ == '__main__':
    import sys
    if len( sys.argv ) == 1:
        print "No configuration file given."
        sys.exit(1)
    validOptions = ('host', 'user', 'password', 'files', 'astring',
                    'someboolean', 'notboolean')
    config = options( sys.argv[1], validOptions )
    print "Your configuration is:"
    print config
    print "host is", config['host']
