#!/usr/bin/env python2 # -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 u"""Afficher les lignes d'un fichier en mettant en surbrillance certains patterns""" import sys, subprocess, re from collections import OrderedDict def fixnlf(line, mo=None): return re.sub(r'\\n', '\n', line) def bluef(line, mo=None): return '\x1B[34m%s\x1B[0m' % line def greenf(line, mo=None): return '\x1B[32m%s\x1B[0m' % line def yellowf(line, mo=None): return '\x1B[33m%s\x1B[0m' % line def redf(line, mo=None): return '\x1B[31m%s\x1B[0m' % line def nonef(line, mo=None): return re.sub('\x1B\[.*?m', '', line) FORMATS = OrderedDict([ ('fixnl', fixnlf), ('blue', bluef), ('green', greenf), ('yellow', yellowf), ('red', redf), ('none', nonef), ]) FORMAT_ALIASES = { 'f': 'fixnl', 'b': 'blue', 'g': 'green', 'y': 'yellow', 'r': 'red', '': 'none', } DEFAULT_PATTERNS = OrderedDict([ (r'(?i)error', redf), (r'(?i)warn(ing)?', yellowf), (r'(?i)info', bluef), (None, nonef), ]) APACHE_PATTERNS = [ r'(?i)error:red', r'(?i)warn(ing)?:yellow', r'(?i)info:blue', r':none', ] PHP_PATTERNS = [ r'(?i)php fatal error:fixnl,red', r'(?i)php notice:yellow,fixnl', r'(?i)php warning:fixnl,blue', r':fixnl,none', ] PRESETS = { 'apache': ('/var/log/apache2/error.log', True, APACHE_PATTERNS, False), 'php': ('/var/log/apache2/error.log', True, PHP_PATTERNS, False), } PRESET_ALIASES = { 'a': 'apache', 'p': 'php', } def strip_nl(s): if s is None: return None elif s.endswith("\r\n"): s = s[:-2] elif s.endswith("\n"): s = s[:-1] elif s.endswith("\r"): s = s[:-1] return s def run_tailor(inputfile=None, follow=False, patterns=None): if inputfile is None or not follow: if inputfile is None: inf = sys.stdin close = False else: inf = open(inputfile, 'rb') close = True def next_line(): try: while True: try: line = inf.readline() except: break if line == '': break yield line finally: if close: inf.close() else: def next_line(): p = subprocess.Popen( ['tail', '-f', inputfile], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: try: line = p.stdout.readline() except: break if line == '': break yield line if patterns is None: patterns = DEFAULT_PATTERNS for line in next_line(): line = nonef(strip_nl(line)) func = False mo = None for p, f in patterns.items(): if p is None: func = f mo = None break mo = re.search(p, line) if mo is not None: func = f break if func is not False: line = func(line, mo) sys.stdout.write(line) sys.stdout.write('\n') if __name__ == '__main__': from argparse import ArgumentParser, HelpFormatter if sys.argv[1:2] == ['--compat']: # Avec l'argument --compat, désactiver la classe FancyHelpFormatter qui # se base sur une API non documentée sys.argv = sys.argv[0:1] + sys.argv[2:] FancyHelpFormatter = HelpFormatter else: class FancyHelpFormatter(HelpFormatter): """Comme HelpFormatter, mais ne touche pas aux lignes qui commencent par les caractères '>>>'. Cela permet de mixer du texte formaté et du texte non formaté. """ def _fill_text(self, text, width, indent): return ''.join([indent + line for line in text.splitlines(True)]) def _split_lines(self, text, width): lines = [''] for line in text.splitlines(): if line.startswith('>>>'): lines.append(line) lines.append('') else: lines[-1] += '\n' + line lines = filter(None, lines) texts = [] for line in lines: if line.startswith('>>>'): texts.append(line[3:]) else: texts.extend(super(FancyHelpFormatter, self)._split_lines(line, width)) return texts pattern_vars = dict(formats=', '.join(FORMATS.keys())) pattern_help = u"""\ Ajouter une spécification de pattern et le format dans lequel il doit être affiché. Le format par défaut est red. Les formats valides sont: >>> %(formats)s Les lignes qui ne correspondent à aucun pattern ne sont pas affichées.""" % pattern_vars default_patterns = [u"%s:%s" % (p or '', f.__name__[:-1]) for (p, f) in DEFAULT_PATTERNS.items()] no_defaults_vars = dict(default_patterns='\n'.join([u">>> %s" % pattern for pattern in default_patterns])) no_defaults_help = u"""\ Ne pas ajouter les patterns par défaut à ceux définis par l'option --pattern. Sans cette option, les patterns par défaut sont: %(default_patterns)s""" % no_defaults_vars follow_help = u"""Suivre le contenu du fichier spécifié""" presets_help = u"""Utiliser un ensemble prédéfini de paramètres. Si cette option est utilisée, les autres options sont ignorées sauf --pattern qui peut être utilisé pour insérer des motifs avant les motifs par défaut du préréglage. >>>Les valeurs valides sont apache et php. Ces deux préréglages ont en commun de suivre le fichier /var/log/apache2/error.log en mettant en surbrillance les lignes intéressantes.""" inputfile_help = u"""\ Fichier à afficher ou dont il faut suivre le contenu. Si cet argument n'est pas spécifié, l'entrée standard est utilisée comme source""" AP = ArgumentParser( usage=u"%(prog)s [-f] [INPUTFILE]", description=__doc__, formatter_class=FancyHelpFormatter, ) AP.set_defaults(inputfile=None, follow=None, patterns=None, defaults=None, presets=None) AP.add_argument('-e', '--pattern', action='append', dest='patterns', metavar='PATTERN:FORMAT', help=pattern_help) AP.add_argument('-z', '--no-defaults', action='store_false', dest='defaults', help=no_defaults_help) AP.add_argument('-d', '--defaults', action='store_true', dest='defaults', help=no_defaults_help) AP.add_argument('-f', '--follow', action='store_true', dest='follow', help=follow_help) AP.add_argument('-p', '--presets', action='store', dest='presets', help=presets_help) AP.add_argument('inputfile', metavar='INPUTFILE', nargs='?', help=inputfile_help) o = AP.parse_args() if o.presets is not None: presets = PRESET_ALIASES.get(o.presets, o.presets) if presets not in PRESETS: raise ValueError("%s: argument invalide" % presets) inputfile, follow, opatterns, odefaults = PRESETS.get(presets) if o.inputfile is not None: inputfile = o.inputfile if o.follow is not None: follow = o.follow if o.patterns is not None: opatterns = o.patterns if o.defaults is not None: odefaults = o.defaults else: inputfile, follow, opatterns, odefaults = o.inputfile, o.follow, o.patterns, o.defaults if follow is None: follow = False if odefaults is None: odefaults = True if opatterns is None: patterns = DEFAULT_PATTERNS else: patterns = OrderedDict() for pf in opatterns: mo = re.match('(.*):([a-zA-Z,]*)$', pf) if mo is not None: p = mo.group(1) ofs = mo.group(2) else: p = pf ofs = 'red' if p == '': p = None ofs = filter(None, ofs.lower().split(',')) fs = None for of in ofs: f = FORMAT_ALIASES.get(of, of) if f not in FORMATS: raise ValueError("%s: format invalide" % of) f = FORMATS[f] if fs is None: fs = f else: def wrapf(line, mo, curf=f, prevf=fs): return curf(prevf(line, mo)) fs = wrapf patterns[p] = fs if odefaults: for p, f in DEFAULT_PATTERNS.items(): patterns.setdefault(p, f) run_tailor(inputfile, follow, patterns)