877 lines
30 KiB
Python
877 lines
30 KiB
Python
# -*- 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()
|