#!/usr/bin/env python # -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 import os, sys, re, string from os import path from ulib.all import * ################################################## # fonctions diverses verbosity = 1 verbose = 2 normal = 1 quiet = 0 def _print(level, text, min_verbosity=normal): """afficher un texte avec un niveau d'indentation, si le niveau de verbosité est correct. Retourner 1 si le texte a été affiché, 0 sinon """ global verbosity if verbosity < min_verbosity: return 0 if level < 0: level = 0 print "%s%s" % (" " * level, text) return 1 debug = 0 def _debug(text): if debug: print text def select_files(p, exclude_names=(), exclude_paths=(), basepath=None): """Construire récursivement la liste de tous les fichiers à partir du répertoire p, en excluant les répertoires dont le nom est donné dans excludes_names, ou dont le chemin relativement à p est donné dans exclude_paths Note: on exclue automatiquement les fichiers qui sont 'binaires' """ if basepath is None: return select_files(p, exclude_names, exclude_paths, p) else: files = [] for file in os.listdir(p): if fileext_is_binary(file): continue pf = path.join(p, file) # chemin absolu relpf = pf[len(basepath) + 1:] # chemin relativement à basepath. +1 pour le '/' ignore = 0 for pattern in exclude_names: if matches_name(pattern, file, p): ignore = 1 else: for pattern in exclude_paths: if matches_name(pattern, relpf, basepath): ignore = 1 if not ignore: if path.isdir(pf): files.extend(select_files(pf, exclude_names, exclude_paths, basepath)) else: files.append(pf) return files ################################################## # Gestion des répertoires d'inclusion incpaths = None def init_incpaths(ignore_env=0): """Initialiser incpaths, la liste globale des répertoires dans lesquels on cherche les fichiers d'inclusions. Lire la valeur de la variable d'environnement UPDATEINCPATH, sauf si on est dans un répertoire non déployé. Si on est dans un répertoire non déployé, s'assurer que le répertoire qui contient ce script est dans incpaths. """ global incpaths if incpaths is None: incpaths = [] if path.isdir(path.join(scriptdir, "test")): # ignorer UPDATEINCPATH si on est dans le répertoire de test ignore_env = 1 if not ignore_env: incpaths = filter(None, string.split(os.environ.get('UPDATEINCPATH', ''), ':')) else: if scriptdir not in incpaths: incpaths.insert(0, scriptdir) _debug("init_incpaths()\n incpaths=\n %s" % string.join(incpaths, "\n ")) def set_incpaths(p): """Soit un répertoire ou une liste de répertoires séparés par ':' Si p est un répertoire, l'ajouter à la liste des répertoires de recherche. Si p est une liste de répertoires, remplacer la liste actuelle par cette nouvelle liste. """ global incpaths if incpaths is None: init_incpaths() if ':' in p: incpaths[:] = map(path.abspath, filter(None, string.split(p, ':'))) else: p = path.abspath(p) if p not in incpaths: incpaths.append(p) _debug("set_incpaths()\n incpaths=\n %s" % string.join(incpaths, "\n ")) def inc_abspath(file, local_incpaths=()): """Retourner le chemin absolu vers un fichier. Si file est un chemin relatif, il est recherché dans incpaths et dans local_incpaths """ file = path.expanduser(path.expandvars(file)) if path.isabs(file): return file if path.exists(file): return path.abspath(file) for incpath in incpaths: pf = path.join(incpath, file) if path.exists(pf): return pf else: for incpath in local_incpaths: pf = path.join(incpath, file) if path.exists(pf): return pf return path.abspath(file) ################################################## # Gestion des lignes d'inclusion class InterestingLine: def __init__(self, c=None): """Initialiser l'objet re_inc matche une ligne d'inclusion repliée re_start_inc matche une ligne d'inclusion dépliée de type 'start' re_end_inc match une ligne d'inclusion dépliée de type 'end' re_require match une ligne d'inclusion de type 'require' """ self.re_comments = [ re.compile(r'[ \t]*(?:r|R)(?:e|E)(?:m|M)'), re.compile(r'[ \t]*##'), re.compile(r'[ \t]*;;'), re.compile(r'[ \t]*//'), re.compile(r"[ \t]*''"), ] if c is None: self.re_inc = re.compile(r'(.*)@include[ \t]+(.+)') self.re_start_inc = re.compile(r'(.*)@inc\[(.+)') self.re_end_inc = re.compile(r'(.*)@inc\](.+)') self.re_require = re.compile(r'(.*)@require[ \t]+(.+)') self.re_provide = re.compile(r'(.*)@provide[ \t]+(.+)') self.c = '@' elif c in ('*', '@'): self.re_inc = re.compile(r'(.*)(?:@|\*)include[ \t]+(.+)') self.re_start_inc = re.compile(r'(.*)(?:@|\*)inc\[(.+)') self.re_end_inc = re.compile(r'(.*)(?:@|\*)inc\](.+)') self.re_require = re.compile(r'(.*)(?:@|\*)require[ \t]+(.+)') self.re_provide = re.compile(r'(.*)(?:@|\*)provide[ \t]+(.+)') self.c = c self.inc_templ = "%s%sinclude %s" self.start_inc_templ = "%s%sinc[%s" self.end_inc_templ = "%s%sinc]%s" self.require_templ = "%s%srequire %s" self.provide_templ = "%s%sprovide %s" self.top_level = True def copy(self): """faire une copie de cet objet """ il = self.__class__(self.c) il.top_level = False return il def is_comment(self, s): """retourner vrai si s est un commentaire (matche l'une des expressions régulières de re_comment) """ for re_comment in self.re_comments: if re_comment.match(s): return True return False def matches(self, line): """retourner vrai si line est une ligne 'intéressante': ligne d'inclusion repliée, dépliée de type 'start', dépliée de type 'end', de type 'require', ou de type 'provide' initialiser les informations sur cette ligne: mo, spaces, file """ self.__is_inc = 0 self.__is_start_inc = 0 self.__is_end_inc = 0 self.__is_require = 0 self.__is_provide = 0 mo = self.re_inc.match(line) if mo is not None: self.__is_inc = 1 else: mo = self.re_start_inc.match(line) if mo is not None: self.__is_start_inc = 1 else: mo = self.re_end_inc.match(line) if mo is not None: self.__is_end_inc = 1 else: mo = self.re_require.match(line) if mo is not None: self.__is_require = 1 else: mo = self.re_provide.match(line) if mo is not None: self.__is_provide = 1 if mo is not None: before = mo.group(1) if self.is_comment(before): self.before = before self.file = mo.group(2) self.pf = inc_abspath(self.file) else: self.__is_inc = 0 self.__is_start_inc = 0 self.__is_end_inc = 0 self.__is_require = 0 self.__is_provide = 0 mo = None self.mo = mo return mo is not None def is_inc(self, line=None): """Si line==None, retourner vrai si la ligne matchée par matches() est une ligne d'inclusion repliée. si line!=None, retourner vrai si la ligne est une ligne d'inclusion repliée dont le nom de fichier correspond à la ligne matchée par matches() """ if line is None: return self.__is_inc else: mo = self.re_inc.match(line) if mo is not None: if mo.group(2) == self.file: return 1 return 0 def inc(self): """retourner une ligne d'inclusion repliée construite avec self.before et self.file """ return self.inc_templ % (self.before, self.c, self.file) def is_start_inc(self, line=None): """Si line==None, retourner vrai si la ligne matchée par matches() est une ligne d'inclusion dépliée de type 'start'. si line!=None, retourner vrai si la ligne est une ligne d'inclusion dépliée de type 'start' dont le nom de fichier correspond à la ligne matchée par matches() """ if line is None: return self.__is_start_inc else: mo = self.re_start_inc.match(line) if mo is not None: if mo.group(2) == self.file: return 1 return 0 def start_inc(self): """retourner une ligne d'inclusion dépliée de type 'start' construite avec self.before et self.file """ return self.start_inc_templ % (self.before, self.c, self.file) def is_end_inc(self, line=None): """Si line==None, retourner vrai si la ligne matchée par matches() est une ligne d'inclusion dépliée de type 'end'. si line!=None, retourner vrai si la ligne est une ligne d'inclusion dépliée de type 'end' dont le nom de fichier correspond à la ligne matchée par matches() """ if line is None: return self.__is_end_inc else: mo = self.re_end_inc.match(line) if mo is not None: if mo.group(2) == self.file: return 1 return 0 def end_inc(self): """retourner une ligne d'inclusion dépliée de type 'end' construite avec self.before et self.file """ return self.end_inc_templ % (self.before, self.c, self.file) def is_require(self, line=None): """Si line==None, retourner vrai si la ligne matchée par matches() est une ligne d'inclusion de type 'require'. si line!=None, retourner vrai si la ligne est une ligne d'inclusion de type 'require' dont le nom de fichier correspond à la ligne matchée par matches() """ if line is None: return self.__is_require else: mo = self.re_require.match(line) if mo is not None: if mo.group(2) == self.file: return 1 return 0 def require(self): """retourner une ligne d'inclusion de type 'require' construite avec self.before et self.file """ return self.require_templ % (self.before, self.c, self.file) def is_provide(self, line=None): """Si line==None, retourner vrai si la ligne matchée par matches() est une ligne d'inclusion de type 'provide'. si line!=None, retourner vrai si la ligne est une ligne d'inclusion de type 'provide' dont le nom de fichier correspond à la ligne matchée par matches() """ if line is None: return self.__is_provide else: mo = self.re_provide.match(line) if mo is not None: if mo.group(2) == self.file: return 1 return 0 def provide(self): """retourner une ligne d'inclusion de type 'provide' construite avec self.before et self.file """ return self.provide_templ % (self.before, self.c, self.file) ################################################## # replier un fichier def fold(file, level=0, il=None, recursive_update=0, parent_print_processing_maybe=None, **ignored): """Replier un fichier: c'est l'opération inverse de unfold_or_update """ if il is None: il = InterestingLine() pf = inc_abspath(file) # vérifier la présence du fichier if not path.exists(pf): _print(level, "not found: %s" % pf) return # Il ne faut pas changer le répertoire courant en sortant de cette # fonction. Faire une copie d'abord cwd = os.getcwd() # Se placer dans le répertoire du fichier p, f = path.split(pf) os.chdir(p) try: tf = TextFile(pf, lines=BLines()) try: old = tf.readlines() except: # impossible de lire le fichier _print(level, "unable to read: %s" % pf) return new = [] printed = [0] def print_processing_maybe(level=level, file=file, printed=printed, parent_print_processing_maybe=parent_print_processing_maybe): if parent_print_processing_maybe is not None: parent_print_processing_maybe() if not printed[0]: _print(level, "processing: %s" % file) printed[0] = 1 folding = 0 # est-on en train de replier? was_modified = 0 for line in old: if not folding: if il.matches(line): if il.is_require(): if recursive_update: fold(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, parent_print_processing_maybe=print_processing_maybe) new.append(il.require()) if il.require() != line: was_modified = 1 elif il.is_provide(): if recursive_update: fold(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, parent_print_processing_maybe=print_processing_maybe) new.append(il.provide()) if il.provide() != line: was_modified = 1 elif il.is_inc(): if recursive_update: fold(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, parent_print_processing_maybe=print_processing_maybe) new.append(il.inc()) if il.inc() != line: was_modified = 1 elif il.is_start_inc(): if il.start_inc() != line: was_modified = 1 folding = 1 if was_modified: print_processing_maybe() else: new.append(line) else: if il.is_end_inc(line): if recursive_update: fold(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, parent_print_processing_maybe=print_processing_maybe) print_processing_maybe() _print(level, "f %s" % il.file, verbose) new.append(il.inc()) was_modified = 1 folding = 0 if folding: # on a trouvé une inclusion non terminée. ne pas modifier le fichier _print(level, "warning: %s include not properly ended, ignored" % il.file) was_modified = 0 if was_modified: tf.writelines(new) finally: os.chdir(cwd) ################################################## # déplier un fichier MODIFIED = 2 UNMODIFIED = 1 ERROR = 0 def unfold_or_update(file, level=0, il=None, recursive_update=0, files=None, provided=None, parent_print_processing_maybe=None, **ignored): """déplier un fichier. Seul le fichier file est modifié le cas échéant si files!=None retourner MODIFIED si le fichier a été lu depuis le disque ou s'il a été modifié, UNMODIFIED s'il faut utiliser le cache de lecture files, ERROR si erreur (aucune modification effectuée) """ if il is None: il = InterestingLine() pf = inc_abspath(file) # Si le fichier a déja été traité, nous pouvons retourner if files is not None and files.has_key(pf): return UNMODIFIED # liste des fichiers déjà traités. can_write = recursive_update # doit-on mettre à jour le fichier sur disque? if files is None: can_write = 1 files = {} # liste des fichiers déjà inclus if provided is None: provided = {} printed = [0] def print_processing_maybe(level=level, file=file, printed=printed, parent_print_processing_maybe=parent_print_processing_maybe): if parent_print_processing_maybe is not None: parent_print_processing_maybe() if not printed[0]: _print(level, "processing: %s" % file) printed[0] = 1 # vérifier la présence du fichier if not path.exists(pf): if level == 0: _print(level, "not found: %s" % pf) else: print_processing_maybe() _print(level - 1, "X %s (not found)" % file) return ERROR # Il ne faut pas changer le répertoire courant en sortant de cette # fonction. Faire une copie d'abord cwd = os.getcwd() # Se placer dans le répertoire du fichier p, f = path.split(pf) os.chdir(p) try: tf = TextFile(pf, lines=BLines()) try: old = tf.readlines() except: # impossible de lire le fichier if level == 0: _print(level, "unable to read: %s" % pf) else: _print(level - 1, "X %s (unable to read)" % file) return ERROR new = [] files[pf] = tf provided[pf] = 1 unfolding = None # est-on en train de déplier? was_modified = 0 for line in old: if unfolding is None: if il.matches(line): if il.is_require(): # traiter le cas @require _debug("require\n file=%s\n provided=\n %s" % (il.file, string.join(provided.keys(), '\n '))) if not il.top_level and not provided.has_key(il.pf): print_processing_maybe() _print(level, "R %s (is required)" % il.file) new.append(il.require()) if il.require() != line: was_modified = 1 elif il.is_provide(): # traiter le cas @provide _debug("provide\n file=%s\n provided=\n %s" % (il.file, string.join(provided.keys(), '\n '))) provided[il.pf] = 1 new.append(il.provide()) if il.provide() != line: was_modified = 1 elif il.is_inc(): # traiter le cas @include if unfold_or_update(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, files=files, provided=provided, parent_print_processing_maybe=print_processing_maybe): print_processing_maybe() _print(level, "U %s" % il.file, verbose) new.append(il.start_inc()) new.extend(files[il.pf].lines) new.append(il.end_inc()) was_modified = 1 else: new.append(il.inc()) if il.inc() != line: was_modified = 1 elif il.is_start_inc(): unfolding = [] if il.start_inc() != line: was_modified = 1 else: new.append(line) else: if il.is_end_inc(line): status = unfold_or_update(il.file, level=level + 1, il=il.copy(), recursive_update=recursive_update, files=files, provided=provided, parent_print_processing_maybe=print_processing_maybe) if status == MODIFIED: if unfolding != files[il.pf].lines: print_processing_maybe() _print(level, "u %s" % il.file, verbose) was_modified = 1 new.append(il.start_inc()) new.extend(files[il.pf].lines) new.append(il.end_inc()) if il.end_inc() != line: was_modified = 1 elif status == UNMODIFIED: new.append(il.start_inc()) new.extend(files[il.pf].lines) new.append(il.end_inc()) if il.end_inc() != line: was_modified = 1 elif status == ERROR: print_processing_maybe() _print(level, "X %s (not found)" % il.file) new.append(il.start_inc()) new.extend(unfolding) new.append(il.end_inc()) if il.end_inc() != line: was_modified = 1 unfolding = None else: unfolding.append(line) if unfolding is not None: # on a trouvé une inclusion non terminée. ne pas modifier le fichier print_processing_maybe() _print(level, "warning: %s include not properly ended, ignored" % il.file) was_modified = 0 if was_modified: if new == tf.lines: return UNMODIFIED print_processing_maybe() tf.lines[:] = new if can_write: tf.writelines() return MODIFIED finally: os.chdir(cwd) ################################################## # lecture des paramètres def update_inc_params(args=None, basedir=None): if args is None: args = sys.argv[1:] global verbosity, debug, incpaths pr = {'action': unfold_or_update, 'exclude_names': ['CVS/', '.svn/'], 'exclude_paths': [], 'il': None, 'recursive_update': 0, } args_read = False while not args_read: config_file = None opts, args = get_args(args, 'qvDuRfx:X:I:*@C:', ['quiet', 'verbose', 'debug', 'update', 'unfold', 'recursive-update', 'fold', 'exclude-names=', 'exclude-paths', 'include-paths', 'config-file=', 'basedir=', ]) for opt, value in opts: if opt in ('-q', '--quiet'): verbosity = quiet elif opt in ('-v', '--verbose'): verbosity = verbose elif opt in ('-D', '--debug'): debug = 1 elif opt in ('-u', '--update', '--unfold'): pr['action'] = unfold_or_update elif opt in ('-R', '--recursive-update'): pr['recursive_update'] = 1 elif opt in ('-f', '--fold'): pr['action'] = fold elif opt in ('-x', '--exclude-names'): if ':' in value: pr['exclude_names'] = filter(None, string.split(value, ':')) else: pr['exclude_names'].append(value) elif opt in ('-X', '--exclude-paths'): if ':' in value: pr['exclude_paths'] = filter(None, string.split(value, ':')) else: pr['exclude_paths'].append(value) elif opt in ('-I', '--include-paths'): if incpaths is None: init_incpaths() set_incpaths(value) elif opt in ('-*',): pr['il'] = InterestingLine('*') elif opt in ('-@',): pr['il'] = InterestingLine('@') elif opt in ('-C', '--config-file'): config_file = value elif opt in ('--basedir',): basedir = path.abspath(value) if config_file is not None: cf = ShConfigFile(config_file, raise_exception=False) if cf.is_valid(): args = split_args(cf.get('update_inc_options', '')) + split_args(cf.get('update_inc_args', '')) + args else: ewarn("Impossible de lire le fichier de configuration: %s" % config_file) args_read = config_file is None # transformer tous les chemins de args en chemins absolus. if basedir is None: basedir = os.getcwd() for i in range(len(args)): args[i] = path.abspath(path.join(basedir, args[i])) return pr, args def update_inc(pr, args=None, action=None): global incpaths if incpaths is None: init_incpaths() if action is not None: action = {'fold': fold, 'unfold': unfold_or_update, 'update': unfold_or_update, 'unfold_or_update': unfold_or_update, }.get(action, None) if action is None: action = pr['action'] if args is None: args = [] for arg in args: if path.isdir(arg): for file in select_files(arg, pr['exclude_names'], pr['exclude_paths']): action(file, il=pr['il'], recursive_update=pr['recursive_update']) else: action(arg, il=pr['il'], recursive_update=pr['recursive_update']) if __name__ == '__main__': pr, args = update_inc_params() update_inc(pr, args)