# # Secret Labs' Regular Expression Engine # $Id$ # # convert re-style regular expression to SRE template. the current # implementation is somewhat incomplete, and not very fast. should # definitely be rewritten before Python 1.6 goes beta. # # Copyright (c) 1998-2000 by Secret Labs AB. All rights reserved. # # This code can only be used for 1.6 alpha testing. All other use # require explicit permission from Secret Labs AB. # # Portions of this engine have been developed in cooperation with # CNRI. Hewlett-Packard provided funding for 1.6 integration and # other compatibility work. # # FIXME: comments marked with the FIXME tag are open issues. all such # issues should be closed before the final beta. import string, sys from sre_constants import * SPECIAL_CHARS = ".\\[{()*+?^$|" REPEAT_CHARS = "*+?{" # FIXME: string in tuple tests may explode with if char is unicode :-( DIGITS = tuple(string.digits) OCTDIGITS = tuple("01234567") HEXDIGITS = tuple("0123456789abcdefABCDEF") ESCAPES = { "\\a": (LITERAL, chr(7)), "\\b": (LITERAL, chr(8)), "\\f": (LITERAL, chr(12)), "\\n": (LITERAL, chr(10)), "\\r": (LITERAL, chr(13)), "\\t": (LITERAL, chr(9)), "\\v": (LITERAL, chr(11)) } CATEGORIES = { "\\A": (AT, AT_BEGINNING), # start of string "\\b": (AT, AT_BOUNDARY), "\\B": (AT, AT_NON_BOUNDARY), "\\d": (IN, [(CATEGORY, CATEGORY_DIGIT)]), "\\D": (IN, [(CATEGORY, CATEGORY_NOT_DIGIT)]), "\\s": (IN, [(CATEGORY, CATEGORY_SPACE)]), "\\S": (IN, [(CATEGORY, CATEGORY_NOT_SPACE)]), "\\w": (IN, [(CATEGORY, CATEGORY_WORD)]), "\\W": (IN, [(CATEGORY, CATEGORY_NOT_WORD)]), "\\Z": (AT, AT_END), # end of string } class Pattern: # FIXME: rename class, and store flags in here too! def __init__(self): self.flags = [] self.groups = 1 self.groupdict = {} def getgroup(self, name=None): gid = self.groups self.groups = gid + 1 if name: self.groupdict[name] = gid return gid def setflag(self, flag): if flag in self.flags: self.flags.append(flag) class SubPattern: # a subpattern, in intermediate form def __init__(self, pattern, data=None): self.pattern = pattern if not data: data = [] self.data = data self.flags = [] self.width = None def __repr__(self): return repr(self.data) def __len__(self): return len(self.data) def __delitem__(self, index): del self.data[index] def __getitem__(self, index): return self.data[index] def __setitem__(self, index, code): self.data[index] = code def __getslice__(self, start, stop): return SubPattern(self.pattern, self.data[start:stop]) def insert(self, index, code): self.data.insert(index, code) def append(self, code): self.data.append(code) def getwidth(self): # determine the width (min, max) for this subpattern if self.width: return self.width lo = hi = 0L for op, av in self.data: if op is BRANCH: l = sys.maxint h = 0 for av in av[1]: i, j = av.getwidth() l = min(l, i) h = min(h, j) lo = lo + i hi = hi + j elif op is CALL: i, j = av.getwidth() lo = lo + i hi = hi + j elif op is SUBPATTERN: i, j = av[1].getwidth() lo = lo + i hi = hi + j elif op in (MIN_REPEAT, MAX_REPEAT): i, j = av[2].getwidth() lo = lo + i * av[0] hi = hi + j * av[1] elif op in (ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY): lo = lo + 1 hi = hi + 1 elif op == SUCCESS: break self.width = int(min(lo, sys.maxint)), int(min(hi, sys.maxint)) return self.width def set(self, flag): if not flag in self.flags: self.flags.append(flag) def reset(self, flag): if flag in self.flags: self.flags.remove(flag) class Tokenizer: def __init__(self, string): self.string = list(string) self.next = self.__next() def __next(self): if not self.string: return None char = self.string[0] if char[0] == "\\": try: c = self.string[1] except IndexError: raise SyntaxError, "bogus escape" char = char + c try: if c == "x": # hexadecimal constant for i in xrange(2, sys.maxint): c = self.string[i] if str(c) not in HEXDIGITS: break char = char + c elif str(c) in DIGITS: # decimal (or octal) number for i in xrange(2, sys.maxint): c = self.string[i] # FIXME: if larger than current number of # groups, interpret as an octal number if str(c) not in DIGITS: break char = char + c except IndexError: pass # use what we've got this far del self.string[0:len(char)] return char def match(self, char): if char == self.next: self.next = self.__next() return 1 return 0 def match_set(self, set): if self.next and self.next in set: self.next = self.__next() return 1 return 0 def get(self): this = self.next self.next = self.__next() return this def _fixescape(escape, character_class=0): # convert escape to (type, value) if character_class: # inside a character class, we'll look in the character # escapes dictionary first code = ESCAPES.get(escape) if code: return code code = CATEGORIES.get(escape) else: code = CATEGORIES.get(escape) if code: return code code = ESCAPES.get(escape) if code: return code if not character_class: try: group = int(escape[1:]) # FIXME: only valid if group <= current number of groups return GROUP, group except ValueError: pass try: if escape[1:2] == "x": escape = escape[2:] return LITERAL, chr(int(escape[-2:], 16) & 0xff) elif str(escape[1:2]) in DIGITS: return LITERAL, chr(int(escape[1:], 8) & 0xff) elif len(escape) == 2: return LITERAL, escape[1] except ValueError: pass raise SyntaxError, "bogus escape: %s" % repr(escape) def _branch(subpattern, items): # form a branch operator from a set of items (FIXME: move this # optimization to the compiler module!) # check if all items share a common prefix while 1: prefix = None for item in items: if not item: break if prefix is None: prefix = item[0] elif item[0] != prefix: break else: # all subitems start with a common "prefix". # move it out of the branch for item in items: del item[0] subpattern.append(prefix) continue # check next one break # check if the branch can be replaced by a character set for item in items: if len(item) != 1 or item[0][0] != LITERAL: break else: # we can store this as a character set instead of a # branch (FIXME: use a range if possible) set = [] for item in items: set.append(item[0]) subpattern.append((IN, set)) return subpattern.append((BRANCH, (None, items))) def _parse(source, pattern, flags=()): # parse regular expression pattern into an operator list. subpattern = SubPattern(pattern) this = None while 1: if str(source.next) in ("|", ")"): break # end of subpattern this = source.get() if this is None: break # end of pattern if this and this[0] not in SPECIAL_CHARS: subpattern.append((LITERAL, this)) elif this == "[": # character set set = [] ## if source.match(":"): ## pass # handle character classes if source.match("^"): set.append((NEGATE, None)) # check remaining characters start = set[:] while 1: this = source.get() if this == "]" and set != start: break elif this and this[0] == "\\": code1 = _fixescape(this, 1) elif this: code1 = LITERAL, this else: raise SyntaxError, "unexpected end of regular expression" if source.match("-"): # potential range this = source.get() if this == "]": set.append(code1) set.append((LITERAL, "-")) break else: if this[0] == "\\": code2 = _fixescape(this, 1) else: code2 = LITERAL, this if code1[0] != LITERAL or code2[0] != LITERAL: raise SyntaxError, "illegal range" if len(code1[1]) != 1 or len(code2[1]) != 1: raise SyntaxError, "illegal range" set.append((RANGE, (code1[1], code2[1]))) else: if code1[0] is IN: code1 = code1[1][0] set.append(code1) # FIXME: move set optimization to support function if len(set)==1 and set[0][0] is LITERAL: subpattern.append(set[0]) # optimization elif len(set)==2 and set[0][0] is NEGATE and set[1][0] is LITERAL: subpattern.append((NOT_LITERAL, set[1][1])) # optimization else: # FIXME: add charmap optimization subpattern.append((IN, set)) elif this and this[0] in REPEAT_CHARS: # repeat previous item if this == "?": min, max = 0, 1 elif this == "*": min, max = 0, sys.maxint elif this == "+": min, max = 1, sys.maxint elif this == "{": min, max = 0, sys.maxint lo = hi = "" while str(source.next) in DIGITS: lo = lo + source.get() if source.match(","): while str(source.next) in DIGITS: hi = hi + source.get() else: hi = lo if not source.match("}"): raise SyntaxError, "bogus range" if lo: min = int(lo) if hi: max = int(hi) # FIXME: check that hi >= lo! else: raise SyntaxError, "not supported" # figure out which item to repeat # FIXME: should back up to the right mark, right? if subpattern: index = len(subpattern)-1 while subpattern[index][0] is MARK: index = index - 1 item = subpattern[index:index+1] else: raise SyntaxError, "nothing to repeat" if source.match("?"): subpattern[index] = (MIN_REPEAT, (min, max, item)) else: subpattern[index] = (MAX_REPEAT, (min, max, item)) elif this == ".": subpattern.append((ANY, None)) elif this == "(": group = 1 name = None if source.match("?"): group = 0 # options if source.match("P"): # named group: skip forward to end of name if source.match("<"): name = "" while 1: char = source.get() if char is None or char == ">": break name = name + char group = 1 elif source.match(":"): # non-capturing group group = 2 elif source.match_set("iI"): pattern.setflag("i") elif source.match_set("lL"): pattern.setflag("l") elif source.match_set("mM"): pattern.setflag("m") elif source.match_set("sS"): pattern.setflag("s") elif source.match_set("xX"): pattern.setflag("x") if group: # parse group contents b = [] if group == 2: # anonymous group group = None else: group = pattern.getgroup(name) if group: subpattern.append((MARK, (group-1)*2)) while 1: p = _parse(source, pattern, flags) if source.match(")"): if b: b.append(p) _branch(subpattern, b) else: subpattern.append((SUBPATTERN, (group, p))) break elif source.match("|"): b.append(p) else: raise SyntaxError, "group not properly closed" if group: subpattern.append((MARK, (group-1)*2+1)) else: # FIXME: should this really be a while loop? while 1: char = source.get() if char is None or char == ")": break elif this == "^": subpattern.append((AT, AT_BEGINNING)) elif this == "$": subpattern.append((AT, AT_END)) elif this and this[0] == "\\": code =_fixescape(this) subpattern.append(code) else: raise SyntaxError, "parser error" return subpattern def parse(source, flags=()): s = Tokenizer(source) g = Pattern() b = [] while 1: p = _parse(s, g, flags) tail = s.get() if tail == "|": b.append(p) elif tail == ")": raise SyntaxError, "unbalanced parenthesis" elif tail is None: if b: b.append(p) p = SubPattern(g) _branch(p, b) break else: raise SyntaxError, "bogus characters at end of regular expression" return p if __name__ == "__main__": from pprint import pprint from testpatterns import PATTERNS a = b = c = 0 for pattern, flags in PATTERNS: if flags: continue print "-"*68 try: p = parse(pattern) print repr(pattern), "->" pprint(p.data) import sre_compile try: code = sre_compile.compile(p) c = c + 1 except: pass a = a + 1 except SyntaxError, v: print "**", repr(pattern), v b = b + 1 print "-"*68 print a, "of", b, "patterns successfully parsed" print c, "of", b, "patterns successfully compiled"