# Part of the A-A-P recipe executive: handling of a dictlist

# Copyright (C) 2002-2003 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING


# A Dictlist is a list of dictonaries, used for a parsed variable.
# The dictionary contains the "name" key for the item itself, and other keys
# for attributes of that item.  Attributes starting with an underscore are for
# internal use (e.g., "_node").

import string
import os.path
import glob

from Util import *
from Process import recipe_error
from Remote import is_url

def get_attrval(line, idx):
    """Get items starting at line[idx] and ending at a '}' character.
       Items are white separated.
       Quotes are used to include {} characters in an item.
       Returns the string for list of items and the index of the next
       character."""
    line_len = len(line)
    res = ''                    # result collected so far
    i = idx
    inquote = ''                # inside quotes
    nesting = 0                 # nested {}
    while 1:
        if i >= line_len:       # end of line
            break

        # End of quoted string?
        if inquote:
            if line[i] == inquote:
                inquote = ''

        # Start of quoted string?
        elif line[i] == '"' or line[i] == "'":
            inquote = line[i]

        # Stop character found?
        else:
            if line[i] == '}':
                if nesting == 0:
                    # Remove trailing white space
                    while res and is_white(res[-1]):
                        res = res[:-1]
                    break
                nesting = nesting - 1
            elif line[i] == '{':
                nesting = nesting + 1

        res = res + line[i]
        i = i + 1

    return res, i


def parse_attr(rpstack, arg, idx):
    """Get the name and value of the attribute at arg[idx], advance until the
       character after the "}"."""
    arglen = len(arg)

    # Skip white after '{'.
    idx = idx + 1
    try:
        c = arg[idx]
        while c == ' ' or c == '\t':
            idx = idx + 1
            c = arg[idx]
    except:
        pass    # ran into the end of the string

    e = skip_varchars(arg, idx)
    if e >= arglen:
        recipe_error(rpstack, _("Syntax error after {"))
    if e == idx:
        recipe_error(rpstack, _("Missing name after {"))
    name = arg[idx:e]

    # skip white after the variable name
    idx = e
    try:
        c = arg[idx]
        while c == ' ' or c == '\t':
            idx = idx + 1
            c = arg[idx]
    except:
        pass    # ran into the end of the string

    if idx < arglen and arg[idx] == '}':
        # No "= value", use one.
        val = 1
    else:
        if idx >= arglen or arg[idx] != '=':
            recipe_error(rpstack, _("Missing = or } after {%s") % name)

        # skip white after the '='
        idx = idx + 1
        try:
            c = arg[idx]
            while c == ' ' or c == '\t':
                idx = idx + 1
                c = arg[idx]
        except:
            pass        # ran into the end of the string

        # get the value
        val, idx = get_attrval(arg, idx)

    if idx >= arglen or arg[idx] != '}':
        recipe_error(rpstack, _("Missing } after {"))

    return name, val, idx + 1


def get_attrdict(rpstack, recdict, arg, start_idx, argexpand):
    """Obtain attributes {name = val} from arg[idx:].
       Returns a dictionary with the attributes and the index of the character
       after the last "}"
       When there is no attribute return {} and idx.
       When "argexpand" is non-zero, expand $VAR things.
       When "argexpand" is zero "recdict" isn't used.
       """
    from Commands import expand

    res = {}

    # Keep looking for attributes until no "{" encountered: " {foo} {bar}"
    idx = start_idx
    while 1:
        # Skip leading white space.  Don't use skip_white() here for speed.
        # When no "{" found don't skip the white space.
        i = idx
        try:
            c = arg[i]
            while c == ' ' or c == '\t':
                i = i + 1
                c = arg[i]
        except:
            break
        if c != '{':
            break

        # Found the start of an attribute.  Get the name and value of the
        # attribute, advance until the character after the "}".
        name, val, idx = parse_attr(rpstack, arg, i)

        # May need to expand $VAR things.
        if argexpand and val != 1:
            if not rpstack:
                line_nr = 0
            else:
                line_nr = rpstack[-1].line_nr
            val = expand(line_nr, recdict, val, Expand(1, Expand.quote_aap))
        
        res[name] = val

    return res, idx


def str2dictlist(rpstack, var, startquote = ''):
    """Create a Dictlist from a variable string.  The variable has to
    be evaluated and white-separated items isolated.
    When "startquote" isn't empty, behave like "var" was preceded by it.
    """
    if not var:
        return []

    # TODO: handle parenthesis: "(foo bar) {attr = val}"

    result = []
    varlen = len(var)
    inquote = startquote
    i = 0
    while i < varlen:

        # Separate one item, removing quotes.
        item = ''
        while 1:
            # Quoted string: check for its end.
            if i < varlen:
                c = var[i]
            if inquote:
                if i >= varlen:
                    break           # Missing quote! error message below.
                if c == inquote:
                    inquote = ''    # End of quoted text.
                else:
                    item = item + c
                i = i + 1
                continue

            # An item ends at the end of the line, at white space or at '{'.
            # When "var" starts with an attribute us an empty name.
            if (i >= varlen or c == '\n' or c == ' ' or c == '\t' or c == '{'):
                if item or c == '{':
                    # Found one item, add it.
                    # Parse {attr = value} zero or more times.
                    adddict, i = get_attrdict(rpstack, None, var, i, 0)
                    adddict["name"] = item
                    result.append(adddict)
                    item = ''
                else:
                    i = i + 1

                if i >= varlen: # end of var
                    break
                continue

            # Start of quoted string?
            if c == '"' or c == "'":
                inquote = c
                i = i + 1
                continue

            item = item + c
            i = i + 1

    if inquote != '':
        recipe_error(rpstack, _('Missing quote %s in "%s"') % (inquote, var))

    return result


def str2list(rpstack, var):
    """Like str2dictlist(), but return a list of the "name" items, no
    attributes."""
    return map(lambda x: x["name"], str2dictlist(rpstack, var))


def varname2dictlist(recdict, scope, varname):
    """Get the value of $"varname" as a dictlist.
       Should only be called when $"varname" exists and isn't empty."""
    try:
        dictlist = str2dictlist([], get_var_val(0, recdict, scope, varname))
    except UserError, e:
        if scope:
            name = scope + '.' + varname
        else:
            name = varname
        raise UserError, (_("Error in parsing $%s: ") % name) + str(e)
    if not dictlist:
        if scope:
            name = scope + '.' + varname
        else:
            name = varname
        raise UserError, _("$%s evaluates to nothing") % name
    return dictlist


def listitem2str(item, escaped = " \t", trailing = ''):
    """Turn an item of a list into a string, making sure characters in
       "escaped" are escaped such that concatenated items are
       white-separatable.  Don't escape a trailing character in "trailing".
       """
    # First check which quote would be most appropriate to start with.  It
    # looks a lot better when it's not halfway the item.
    quote = ''
    item_str = str(item)
    item_str_len = len(item_str)
    i = 0
    while i < item_str_len:
        c = item_str[i]
        if c == "'":
            quote = '"'
            break
        if c == '"':
            quote = "'"
            break
        if c in escaped and (i + 1 < item_str_len or not c in trailing):
            quote = '"'
        i = i + 1

    res = quote
    esc = "'\"" + escaped
    i = 0
    while i < item_str_len:
        c = item_str[i]
        if c in esc and (i + 1 < item_str_len or not c in trailing):
            if c == quote:
                res = res + quote
                quote = ''
            if not quote:
                if c == '"':
                    quote = "'"
                else:
                    quote = '"'
                res = res + quote
        res = res + c
        i = i + 1
    return res + quote


def list2str(list):
    """Turn a list of items into a string, using quotes for list items with
       white space."""
    s = ''
    for item in list:
        if s:
            s = s + ' '
        s = s + listitem2str(str(item))
    return s


def dictlistattr2str(dl):
    """Print the attributes in dictlist "dl"."""
    res = ''
    for k in dl.keys():
        if k != "name" and k[0] != "_":
            # Assume no escaping is necessary, the value should already include
            # it when it's needed.
            res = res + ('{%s=%s}' % (k, str(dl[k])))
    return res


def dictlist2str(list, argexpand = None):
    """Turn a dictlist into a string that can be printed.
       Don't use backslashes to escape special characters.
       Do expanding according to "argexpand"."""
    if not argexpand:
        argexpand = Expand(1, Expand.quote_aap)
    res = ''
    for i in list:
        if res:
            res = res + ' '
        res = res + expand_item(i, argexpand, "name")
    return res


def dictlist_expanduser(dl):
    """
    Expand "~user" and "~/" in dictlist "dl".
    """
    for dicty in dl:
        if dicty["name"][0] == '~':
            dicty["name"] = os.path.expanduser(dicty["name"])

def dictlist_expand(dl):
    """
    Call dict_expand() for every dict in the list "dl".
    Expand wildcards "*", "?" and "[abc]".
    When there are no matches while there are wildcards, the item is not added.
    Returns a new dictlist, possibly with more (or less) entries.
    """
    ret = []
    for dicty in dl:
        dict_expand(dicty)

        n = dicty.get("name")
        if not n:
            # No name??? Just append it.
            ret.append(dicty)
        else:
            # Expand wildcars.
            try:
                exp = glob.glob(n)
            except StandardError, e:
                raise UserError, (_("Error while expanding %s: ") % str(n)) + str(e)
            if not exp:
                # If no match and has no wildcards: add without expanding.
                if not has_wildcard(n):
                    ret.append(dicty)
            else:
                # Add a dictionary to the list for each match.
                # Add the first match without duplicating the dict.
                dicty["name"] = exp[0]
                ret.append(dicty)
                if len(exp) > 1:
                    # Need to make a copy of the dictionary for others.
                    for n in exp[1:]:
                        d = dicty.copy()
                        d["name"] = n
                        ret.append(d)
    return ret


def dict_expand(dicty):
    """
    Expand "~user" and "~/" in "dicty".
    Does NOT expand wildcards.
    Also make some attributes absolute paths.
    """
    n = dicty.get("name")
    if n and n[0] == '~':
        dicty["name"] = os.path.expanduser(n)

    # Turn file names of selected attributes relative to the current directory
    # into absolute path names, so that ":cd" doesn't change them.
    for n in ['signfile', 'depdir']:
        v = dicty.get(n)
        if v and v[0] != '~' and not os.path.isabs(v) and not is_url(v):
            dicty[n] = os.path.abspath(v)


# vim: set sw=4 et sts=4 tw=79 fo+=l:
