# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

"""Fonctions utilitaires pour lire des fichiers de configuration.

Dans un fichier de configuration, l'on reconnait des lignes de la forme::

  [comment][prefix]varname=value

value peut être placé entre double quotes ou simple quotes. Elle peut s'étendre sur
plusieurs lignes si elle est mise entre quotes, ou si elle se termine par \
"""

__all__ = (
    'ConfigFile', 'ShConfigFile', 'PListFile',
    'ShConfig',
)

import os, string, re, types, shlex
from os import path

from .base import odict, make_prop, isseq, seqof, firstof
from .uio import _s, _u
from .files import TextFile
from .formats import unicodeF

####################
# gestion des commentaires

re_comments = {
    'shell': re.compile(r'[ \t]*#+'),
    'conf': re.compile(r"[ \t]*;+"),
    'C': re.compile(r'[ \t]*//+'),
    'visual basic': re.compile(r"[ \t]*'+"),
    'wincmd': re.compile(r'[ \t]*(?:r|R)(?:e|E)(?:m|M)'),
    }

def is_comment(s, type=None):
    """Retourner vrai si s un commentaire (c'est à dire si la ligne commence par
    un des styles de commentaires supportés)
    """
    comment_types = type is None and re_comments.values() or [re_comments[type]]
    for comment_type in comment_types:
        if comment_type.match(s): return True
    return False

####################
# gestion des fichiers de configuration

_marker = object()

class ConfigFile(TextFile):
    r"""Un fichier de configuration, que l'on doit lire sous Python, et que l'on
    doit partager éventuellement avec d'autres langages ou d'autres systèmes
    d'exploitation. Par exemple, il peut s'agir d'un fichier de configuration
    sous bash.

    Une valeur non quotée est trimée à droite et à gauche. Une valeur quotée
    n'est jamais trimée.

    Une valeur quotée peut être suivie d'une valeur non quotée, et les deux sont
    mergés. Mais une fois que l'on commence à parser une valeur non quotée, plus
    aucun traitement n'est effectuée, ce qui fait qu'une valeur quotée ne peut
    pas suivre une valeur non quotée (cf le "andme" ci-dessus).

    Ceci diffère du comportement de parseur plus évolués comme celui de bash. On
    considère néanmoins que c'est une caractéristique, non un bug. XXX corriger
    ce problème, ne serait-ce que pour supporter la lecture de fichiers tels que
    var='value'\''with a quote'

    Tests
    =====

    >>> from StringIO import StringIO
    >>> input = StringIO(r'''# comment
    ... name=value
    ... name2=  value
    ... name3  =  value
    ... qname="qvalue"
    ... qname2="  qvalue  "
    ... qname3  =  "  qvalue  "
    ... qname4="
    ...     multi-line
    ...     qvalue
    ...     "
    ... fancy="\
    ... noNL\
    ... "foryou"andme"
    ... quote='"'
    ... quote2="\""
    ... quote3='\''
    ... quote4='\\'
    ... quote5='\\\''
    ... quote6='\\\'remainder'
    ... ''')
    >>> from ulib.base.config import ConfigFile
    >>> cf = ConfigFile(input)
    >>> cf.get_string('name')
    u'value'
    >>> cf.get_string('name2')
    u'value'
    >>> cf.get_string('name3')
    u'value'
    >>> cf.get_string('qname')
    u'qvalue'
    >>> cf.get_string('qname2')
    u'  qvalue  '
    >>> cf.get_string('qname3')
    u'  qvalue  '
    >>> cf.get_string('qname4')
    u'\n    multi-line\n    qvalue\n    '
    >>> cf.get_string('fancy')
    u'noNLforyouandme'
    >>> cf.get_string('quote')
    u'"'
    >>> cf.get_string('quote2')
    u'\\"'
    >>> cf.get_string('quote3')
    u"\\'"
    >>> cf.get_string('quote4')
    u'\\\\'
    >>> cf.get_string('quote5')
    u"\\\\\\'"
    >>> cf.get_string('quote6')
    u"\\\\\\'remainder"

    """

    # valeurs lues dans le fichier de configuration
    _items, items = make_prop('_items')[:2]
    # valeurs par défaut
    _defaults, defaults = make_prop('_defaults')[:2]
    # expression régulière identifiant le préfixe des variables
    _prefix, prefix = make_prop('_prefix', '')[:2]
    # expression régulière identifiant pour le séparateur entre le nom de la
    # variable et sa valeur.
    _equals, equals = make_prop('_equals', r'\s*=')[:2]
    # faut-il considérer les variables en commentaires?
    _comment, comment = make_prop('_comment')[:2]

    ############################################################################
    # interface publique

    def __init__(self, file=None, defaults=None,
                 prefix=None, equals=None, comment=False,
                 raise_exception=True, lines=None):
        """
        @param prefix une expression régulière identifiant un préfixe mentionné
               avant chaque variable. par exemple, si prefix=='##@' et qu'on
               cherche la variable value, alors la ligne ##@value est cherchée.
        @param comment faut-il considérer les valeurs qui sont en commentaires?
               Si oui, tout se passe comme si le commentaire n'existe pas.
        @param defaults un ensemble de valeurs par défaut qui sont retournées si la
               variable n'existe pas dans le fichier.
        @param lines instance de Lines ou BLines permettant de décoder le contenu du
               fichier.
        """
        super(ConfigFile, self).__init__(file, raise_exception=raise_exception, lines=lines)
        self._items = {}
        self._defaults = defaults or {}
        if prefix is not None: self._prefix = prefix
        if equals is not None: self._equals = equals
        self._comment = comment

    def __getitem__(self, name, default=_marker):
        """Obtenir la valeur de la variable name, telle qu'elle a été lue.
        Si c'est un tableau, retourner une liste. Sinon retourner une chaine.

        Si la variable n'est pas définie, retourner default.
        """
        if not self._items.has_key(name): self._load_value(name)
        if default is _marker:
            if not self._items.has_key(name) and self._defaults.has_key(name):
                return self._defaults[name]
            return self._items[name]
        return self._items.get(name, default)
    get = __getitem__

    def __setitem__(self, name, value):
        self._items[name] = value

    def __delitem__(self, name):
        del self._items[name]

    def has_key(self, name):
        try: self.__getitem__(name)
        except KeyError: return False
        else: return True

    def get_string(self, name, default=_marker):
        """Obtenir la valeur de la variable name. Si la variable est un tableau,
        retourner la première valeur de ce tableau. Retourner None si le tableau
        est vide.
        """
        value = self.__getitem__(name, default)
        if isseq(value): return firstof(value)
        else: return value

    def get_lines(self, name, strip=False, default=_marker):
        """Obtenir une valeur avec get_string(), et la spliter sur le caractère
        de fin de ligne. Retourner la liste des lignes.

        si strip est vrai, on strip toutes les lignes puis on enlève les
        lignes vides.
        """
        lines = self.get_string(name, default)
        if not isseq(lines): lines = re.split(r'(?:\r?)\n', lines)
        if strip: lines = filter(None, map(string.strip, lines))
        return lines

    def get_paths(self, name, strip=False, default=_marker):
        """Obtenir une valeur avec get_string(), la splitter sur le caractère
        'os.path.pathsep'. Retourner la liste des chemins.

        si strip est vrai, on strip toutes les valeurs puis on enlève les
        valeurs vide.
        """
        paths = self.get_string(name, default)
        if not isseq(paths): paths = paths.split(path.pathsep)
        if strip: paths = filter(None, map(string.strip, paths))
        return paths

    def get_array(self, name, default=_marker):
        """Obtenir la liste des valeurs de la variable name. Si name est une
        valeur scalaire, retourner une liste d'un seul élément.
        """
        return list(seqof(self.__getitem__(name, default)))

    ############################################################################
    # partie privée

    RE_ANTISLASHES = re.compile(r'\\+$')
    def _is_cont(self, value):
        """Tester si value doit être fusionné avec la ligne suivante à cause de
        la présence d'un caractère de continuation de ligne.

        Par défaut, on teste si value se termine par un nombre impair de '\\'
        """
        mo = self.RE_ANTISLASHES.search(value)
        if mo is None: return False
        return len(mo.group()) % 2 == 1

    def _strip_cont(self, value):
        """Enlever le caractère de continuation de ligne de value. On assume que
        self._is_cont(value) est vrai.
        """
        return value[:-1]

    def _merge_cont(self, index, value, sep=''):
        """Merger value située à la ligne index, et la ligne suivante, en les
        séparant par sep. On assume que self._is_cont(value) est vrai, et que le
        caractère de continuation a été enlevé avec self._strip_cont(value)

        Dans la valeur de retour, eof vaut True si la fin de fichier est
        rencontrée.

        @return (index+1, merged_value, eof)
        """
        if index + 1 < len(self.lines):
            index += 1
            value = value + sep + self.lines[index]
            eof = False
        else:
            eof = True
        return index, value, eof

    def _unescape(self, value, quote=''):
        """Traiter les séquences d'échappement dans une valeur scalaire. Si la
        valeur était quotée, quote contient la valeur du caractère ("'", '"' ou
        ''). Par défaut, ne rien faire.

        Cette fonction doit être surchargée en fonction du type de fichier de
        configuration que l'on lit.

        La valeur quote=='' signifie que la valeur n'était pas quotée, mais il
        peut quand même y avoir des séquences d'échappement à traiter.
        """
        return value

    def _load_value(self, name):
        """charger la valeur d'une variable depuis le fichier.

        XXX rendre le parcours plus robuste: faire attention à ne pas lire une
        valeur à l'intérieur d'une autre valeur. Par exemple:

            var1="\
            var2=bad
            "
            var2=good

        Avec l'implémentaion actuelle, si on demande la valeur de var2, on
        obtient bad. Une façon de corriger cela de parcourir *tout* le fichier,
        de lire les valeurs non analysées de chaque variable au fur et à mesure,
        puis de les placer en cache. ensuite, _load_value() se contenterai
        d'analyser les valeurs dans le cache.

        @return None si la valeur n'est pas trouvée dans le fichier. Sinon,
        retourner une valeur scalaire ou une séquence en fonction du type de la
        valeur.
        """
        # le groupe 1 sera testé pour voir si c'est un commentaire
        re_varname = re.compile(r'(.*)%s%s%s' % (self._prefix, name, self._equals))
        re_value = re.compile(r'.*%s%s%s(.*)' % (self._prefix, name, self._equals))

        indexes = self.grepi(re_varname)
        if not indexes: return None

        # trouver d'abord la ligne appropriée
        comment = ''
        for index in indexes:
            comment = re_varname.match(self.lines[index]).group(1)
            if is_comment(comment):
                # si la valeur est en commentaire, ne l'accepter que si
                # self._comment est vrai
                if not self._comment:
                    continue
            # nous avons trouvé l'index de la ligne
            break
        else:
            # aucune ligne n'a été trouvée
            return

        # ensuite lire la valeur
        value = re_value.match(self.lines[index]).group(1)
        value = self._parse_logic(index, value)
        self._items[name] = value

    def _parse_logic(self, index, value):
        """Implémenter la logique d'analyse de la valeur d'une variable.

        Il faut reimplémenter cette méthode si on veut modifier le type de
        valeurs supportées. _parse_scalar() permet d'analyser une valeur simple,
        _parse_array() permet d'analyser un tableau de valeurs.

        Par défaut, on ne supporte que les valeurs scalaire. Utiliser
        ShConfigFile pour supporter les tableaux.
        """
        value = value.lstrip() # ignorer les espaces avant la valeur
        return self._parse_scalar(index, value)

    ## valeurs scalaires simples

    RE_SPACES = re.compile(r'\s+')
    def _parse_scalar(self, index, value):
        remainder = value
        value = ''
        lstrip = None
        rstrip = None
        while remainder:
            mo = self.RE_SPACES.match(remainder)
            if mo is not None:
                # ne pas supprimer les espaces entre les valeurs
                remainder = remainder[mo.end():]
                value += mo.group()
            # XXX supporter de spécifier le type de commentaires valides dans ce
            # fichier de configuration. A cet endroit, il faudrait pouvoir
            # éliminer les commentaires qui sont sur la ligne. évidemment, ce ne
            # serait pas forcément approprié suivant la configuration. exemple:
            # REM pour un fichier cmd n'est valide qu'en début de ligne.
            elif self._is_quoted(remainder):
                # valeur quotée. pas de strip
                if lstrip is None: lstrip = False
                rstrip = False
                index, next_value, remainder = self._parse_quoted(index, remainder)
                value += self._unescape(next_value)
            else:
                # valeur non quotée. lstrip si en premier. rstrip si en dernier
                if lstrip is None: lstrip = True
                rstrip = True
                index, next_value, remainder = self._parse_value(index, remainder)
                value += self._unescape(next_value)
        if lstrip: value = value.lstrip()
        if rstrip: value = value.rstrip()
        return value

    RE_VALUE = re.compile('[^\\s\'"]*')
    def _parse_value(self, index, value, pattern=None):
        """Parser une valeur simple non quotée à partir de value (qui se trouve
        à la position index) et des lignes suivant index si la ligne se termine
        par '\\'.

        @return index, value, remainder
        """
        while self._is_cont(value):
            value = self._strip_cont(value)
            index, value, eof = self._merge_cont(index, value)
            if eof: break
        if pattern is None: pattern = self.RE_VALUE
        mo = pattern.match(value)
        if mo is None:
            return index, '', value
        else:
            remainder = value[mo.end():]
            value = value[:mo.end()]
            return index, value, remainder

    ## valeurs scalaires quotées

    def _is_quoted(self, value):
        """Tester si value est le début d'une valeur quotée. Ignorer les espaces
        avant la quote.
        """
        return value.lstrip()[:1] in ('"', "'")

    def _search_next_quote(self, value, re_quote):
        """Chercher un match de re_quote dans value, qui ne soit pas précédé par
        un nombre impair de '\\'.
        """
        pos = 0
        while True:
            mo = re_quote.search(value, pos)
            if mo is None: return None
            if self._is_cont(value[:mo.start()]):
                # nombre impair de '\\', la quote est mise en échappement
                pos = mo.end()
            else:
                return mo

    RE_QUOTE = re.compile(r'[\'"]')
    def _parse_quoted(self, index, value):
        """Parser une valeur quotée à partir de value (qui se trouve à la
        position index) et des lignes suivant index.

        value *doit* commencer par la quote. si _is_quoted(value) est vrai, il
        faut enlever les espaces éventuels au début de value avant de la passer
        à cette méthode.

        @return index, value, remainder
        """
        if self.RE_QUOTE.match(value) is None:
            raise ValueError("value must start with a quote, got %s" % repr(_s(value)))
        quote, value = value[:1], value[1:]
        re_quote = re.compile(quote)
        mo = self._search_next_quote(value, re_quote)
        while mo is None:
            if self._is_cont(value):
                value = self._strip_cont(value)
                index, value, eof = self._merge_cont(index, value)
            else:
                index, value, eof = self._merge_cont(index, value, self.nl)
            mo = self._search_next_quote(value, re_quote)
            if eof: break
        if mo is None:
            # valeur quotée, mais mal terminée. on fait comme si on a rien vu
            return index, value, ''
        else:
            remainder = value[mo.end():]
            value = value[:mo.start()]
            return index, value, remainder

    ## tableaux

    def _is_array(self, value):
        """Tester si value est le début d'un tableau. Ignorer les espaces avant
        le tableau.
        """
        return False

    def _parse_array(self, index, value):
        """Parser un tableau à partir de value (qui se trouve à la position
        index) et des lignes suivant index.

        value *doit* commencer par le tableau. si _is_array(value) est vrai, il
        faut enlever les espaces éventuels au début de value avant de la passer
        à cette méthode.
        """
        return []

class ShConfigFile(ConfigFile):
    r"""Un fichier de configuration qui est susceptible d'être lu aussi par bash
    (ou tout autre shell sh-like). On supporte l'évaluation de variables, et
    certaines séquences d'échappement pour des valeurs quotées.

    Il y a certaines limitations: lors de la lecture des valeurs des variables,
    les caractères sont traduits suivant la correspondance suivante:

    \ en fin de ligne:          continuer sur la ligne suivante
    \"                          "
    \\                          \
    \$                          $

    La séquence \` n'est pas traduite. En effet, pour que cela aie un sens, il
    faudrait que l'on traduise aussi `cmd`

    De plus, on ne supporte que les variables de la forme $var et ${var}

    Tests
    =====

    >>> from StringIO import StringIO
    >>> input = StringIO(r'''# comment
    ... var1=value
    ... var2="value"
    ... var3='value'
    ... var4=(value1 "value2" 'value3')
    ... var5=(
    ... value1
    ... "value2\
    ... " 'value3'
    ... )
    ... var6=()
    ... var7=( )
    ... var8=(
    ... )
    ... ''')
    >>> from ulib.base.config import ShConfigFile
    >>> cf = ShConfigFile(input)
    >>> cf.get_string('var1')
    u'value'
    >>> cf.get_string('var2')
    u'value'
    >>> cf.get_string('var3')
    u'value'
    >>> cf.get_string('var4')
    u'value1'
    >>> cf.get_array('var4')
    [u'value1', u'value2', u'value3']
    >>> cf.get_array('var5')
    [u'value1', u'value2', u'value3']
    >>> [cf.get_array(name) for name in ('var6', 'var7', 'var8')]
    [[], [], []]
    >>> cf.get_array('var1')
    [u'value']
    >>> cf.get_string('var4')
    u'value1'
    >>> cf.get_string('var6') is None
    True
    """

    RE_VAR = re.compile(r'\$(?:\{([^}]+)\}|(\w+))')
    TRANS_MAP = {r'\"': '"', r'\\': '\\', r'\$': '$'}

    def __convert(self, value):
        # XXX rendre la conversion plus robuste: veiller à l'ordre ('\\\\' en
        # dernier...), et ne faire la conversion que pour un nombre impaire de
        # '\\'.
        for s, r in self.TRANS_MAP.items():
            value = value.replace(s, r)
        return value

    def _unescape(self, value, quote=''):
        """convertir une valeur quotée, suivant les règles de bash.
        quote peut valoir "'", '"', ''
        """
        # aucune traduction entre ''
        if quote == "'": return value
        # sinon appliquer les règles standards. notamment, remplacer $var et
        # ${var} par self._items["var"] ou os.environ["var"]
        splited = self.RE_VAR.split(value)
        value = self.__convert(splited[0])
        splited = splited[1:]
        while splited:
            var0 = splited[0]
            var1 = splited[1]
            text = splited[2]
            splited = splited[3:]
            var = var0 or var1
            if self.has_key(var): value = value + self.get_string(var)
            else: value = value + os.environ.get(var, "")
            value = value + self.__convert(text)
        return value

    def _parse_logic(self, index, value):
        value = value.lstrip() # ignorer les espaces avant la valeur
        if self._is_array(value): return self._parse_array(index, value)
        else: return self._parse_scalar(index, value)

    ## tableaux

    def _is_array(self, value):
        """Tester si value est le début d'un tableau.
        """
        return value.strip()[:1] == '('

    RE_ARRAY_VALUE = re.compile('[^\\s\'")]*')
    def _parse_next_scalar(self, index, value):
        """Parser la prochaine valeur scalaire
        XXX à faire
        @return index, value, remainder
        """
        remainder = value
        value = ''
        lstrip = None
        rstrip = None
        while remainder:
            if self.RE_SPACES.match(remainder) is not None:
                # les valeurs sont séparées par des espaces
                break
            # XXX cf ConfigFile._parse_scalar pour la gestion des commentaires
            elif self.RE_EOA.match(remainder) is not None:
                # fin de tableau
                break
            elif self._is_quoted(remainder):
                # valeur quotée. pas de strip
                if lstrip is None: lstrip = False
                rstrip = False
                index, next_value, remainder = self._parse_quoted(index, remainder)
                value += self._unescape(next_value)
            else:
                # valeur non quotée. lstrip si en premier. rstrip si en dernier
                if lstrip is None: lstrip = True
                rstrip = True
                index, next_value, remainder = self._parse_value(index, remainder, self.RE_ARRAY_VALUE)
                value += self._unescape(next_value)
        if lstrip: value = value.lstrip()
        if rstrip: value = value.rstrip()
        return index, value, remainder

    RE_SOA = re.compile(r'\(')
    RE_EOA = re.compile(r'\)')
    def _parse_array(self, index, value):
        """Parser un tableau à partir de value (qui se trouve à la position
        index) et des lignes suivant index.

        @return index, values, remaining
        """
        if self.RE_SOA.match(value) is None:
            raise ValueError("value must start with '(', got %s" % repr(_s(value)))
        remainder = value[1:]
        values = []
        eoa = False # end of array
        while True:
            if not remainder:
                # nous n'avons pas encore rencontré la fin du tableau. Lire les
                # lignes jusqu'à ce que nous trouvions ce qui est nécessaire
                index, remainder, eof = self._merge_cont(index, remainder)
                if eof: break
            # ignorer les espaces entre les valeurs
            mo = self.RE_SPACES.match(remainder)
            if mo is not None:
                remainder = remainder[mo.end():]
                continue
            # tester si on arrive à la fin du tableau
            if self.RE_EOA.match(remainder) is not None:
                remainder = remainder[1:]
                eoa = True
                break
            # parser une valeur scalaire
            index, next_value, remainder = self._parse_next_scalar(index, remainder)
            values.append(next_value)
        # ici, eoa vaut True si le tableau a été terminé proprement.
        # sinon, on fait comme si on a rien vu.
        return values

_debug = False
def _print_debug(s):
    if _debug: print s

class PListFile(TextFile):
    def readlines(self, raise_exception=True, close=True):
        TextFile.readlines(self, raise_exception, close)

        self.items = None
        self.list = None
        self.value = None

        if self.is_valid():
            if self.lines and self.lines[0][:5] == '<?xml':
                self.__read_xml()
            else:
                self.__read_plist()

    def is_dict(self):
        return self.items is not None

    def is_list(self):
        return self.list is not None

    def is_scalar(self):
        return self.value is not None

    def __getitem__(self, name, default=_marker):
        if self.is_dict():
            if default is _marker:
                return self.items[name]
            return self.items.get(name, default)
        if self.is_list():
            return self.list[name]
        raise IndexError("This object contains a scalar value. use the value attribute instead")

    def __setitem__(self, name, value):
        self.items[name] = value

    def get(self, name, default=_marker):
        return self.__getitem__(name, default)

    def __read_xml(self):
        """charger un fichier au format plist xml

        XXX à faire
        """
        raise NotImplementedError

    def __read_plist(self):
        """charger un fichier au format plist natif
        """
        self.data = self.get_nl().join(self.grep(r'[ \t]*//', inverse=True))

        value = self.__parse_value()
        if type(value) is types.DictType:
            self.items = value
        elif type(value) is types.ListType:
            self.list = value
        else:
            self.value = value

    re_blank = re.compile(r'[ \t\r\n]+')
    def __skip_blank(self):
        mo = self.re_blank.match(self.data)
        if mo is not None:
            self.data = self.data[mo.end(0):]

    def __parse_chars(self, *cs, **kw):
        if kw.get('skip_blank', False):
            self.__skip_blank()
        if self.data[:1] in cs:
            c, self.data = self.data[:1], self.data[1:]
            return c
        else:
            if kw.get('optional', False): return None
            raise ValueError("Unable to find '%s'" % _s(''.join(cs)))

    re_name = re.compile(r'[a-zA-Z0-9_]+')
    def __parse_name(self, optional=False):
        """chercher un nom, retourner None si pas trouvé
        """
        self.__skip_blank()
        c = self.data[:1]
        if c == '"':
            name = self.__parse_string()
        else:
            mo = self.re_name.match(self.data)
            if mo is None:
                if optional: return None
                raise ValueError("Expected an unquoted name")
            name = mo.group(0)
            self.data = self.data[mo.end(0):]

        _print_debug("XXX name=%s" % name)
        return name

    re_value = re.compile(r'[a-zA-Z0-9_/.$]+')
    def __parse_value(self, optional=False):
        _print_debug("XXX parse_value, data=\n    %s" % self.data[:70])
        value = None

        self.__skip_blank()
        c = self.data[:1]
        if c == '{':
            value = self.__parse_dict()
        elif c == '(':
            value = self.__parse_list()
        elif c == '"':
            value = self.__parse_string()
        else:
            mo = self.re_value.match(self.data)
            if mo is None:
                if optional: return None
                raise ValueError("Expected a quoted name")
            value = mo.group(0)
            self.data = self.data[mo.end(0):]

        _print_debug("XXX value=%s" % value)
        return value

    def __parse_dict(self):
        dict = {}
        self.__parse_chars('{')
        while True:
            name = self.__parse_name(optional=True)
            if name is None: break
            self.__parse_chars('=', skip_blank=True)
            value = self.__parse_value()
            self.__parse_chars(';', skip_blank=True)

            dict[name] = value
        self.__parse_chars('}', skip_blank=True)

        _print_debug("XXX dict=%s" % dict)
        return dict

    def __parse_list(self):
        list = []
        first = True
        self.__parse_chars('(')
        while True:
            if first:
                value = self.__parse_value(optional=True)
                if value is None: break
                first = False
            else:
                c = self.__parse_chars(',', skip_blank=True, optional=True)
                if c is None: break
                value = self.__parse_value(optional=True)
                if value is None: break
            list.append(value)
        self.__parse_chars(')', skip_blank=True)

        _print_debug("XXX list=%s" % list)
        return list

    re_string = re.compile(r'"((?:\\"|[^"])*)"')
    def __parse_string(self):
        mo = self.re_string.match(self.data)
        if mo is None:
            raise ValueError("Expected a quoted string")
        string = mo.group(1)
        self.data = self.data[mo.end(0):]

        _print_debug("XXX string=%s" % string)
        return string

################################################################################
# classes utilisant shlex et l'interface odict

class ShConfig(odict):
    _formats = None
    def __init__(self, config, formats=None):
        super(ShConfig, self).__init__()
        if formats is None: formats = {}
        else: formats = dict(formats)
        self.__dict__['_formats'] = formats
        for name in self._formats.keys():
            self[name] = None

        inf = open(config, 'rb')
        try: s = inf.read()
        finally: inf.close()
        parts = shlex.split(s, True)
        self.parse(parts)

    def get_format(self, name):
        format = None
        if format is None: format = self._formats.get(name, None)
        if format is None: format = self._formats.get(None, None)
        if format is None: format = unicodeF
        return format

    RE_ARRAY = re.compile(r'([^=]+)=\((.*)')
    RE_ARRAY_LAST = re.compile(r'(.*)\)$')
    RE_SCALAR = re.compile(r'([^=]+)=(.*)')
    def parse(self, parts):
        i = 0
        while i < len(parts):
            part = parts[i]
            i += 1
            amo = self.RE_ARRAY.match(part)
            smo = self.RE_SCALAR.match(part)
            if amo is not None:
                array = []
                name, value = amo.groups()
                format = self.get_format(name)
                if value != '': array.append(format.parse(value))
                while i < len(parts):
                    value = parts[i]
                    i += 1
                    mo = self.RE_ARRAY_LAST.match(value)
                    if mo is not None:
                        value = mo.group(1)
                        if value != '': array.append(format.parse(value))
                        break
                    else:
                        array.append(format.parse(value))
                self[name] = array
            elif smo is not None:
                name, value = smo.groups()
                format = self.get_format(name)
                self[name] = format.parse(value)
            else:
                continue # ignorer l'erreur pour le moment
                raise ValueError("%s: not a variable" % part)

################################################################################

if __name__ == '__main__':
    import doctest
    doctest.testmod()
