Initital import of mail-filter
[confparser-old] / confparser.py
1 # -*- coding: iso-8859-1 -*-
2
3 import re
4 import sys
5
6 __all__ = [ 'parse' , 'printTree' ]
7
8 # FIXME: Build "preparsed" file ?
9
10 # TODO: Rewrite all of this more clearly
11
12 def advancePosition( text , x , y ) :
13
14         p = text.rfind( '\n' )
15         if p == -1 :
16                 x += len( text )
17         else :
18                 y += text.count( '\n' )
19                 x = len( text ) - ( p + 1 ) + 1
20         return x , y
21
22 unslashMap = {
23         'n' : '\n' ,
24         't' : '\t'
25 }
26
27 def unslash( s ) :
28
29         pos = 0
30         while 1 :
31                 p = s.find( '\\' , pos )
32                 if p == -1 or p == len( s ) - 1 :
33                         break
34                 c = s[ p + 1 ]
35                 c = unslashMap.get( c , c )
36                 s = s[ : p ] + c + s[ p + 2 : ]
37                 pos = p + 2
38         return s
39
40 #
41 # instruction-end: ;
42 # begin-block:     {
43 # end-block:       }
44 # keyword:         [a-z0-9\-_]+
45 # string:          \'(?:\\\'|[^\\\'])+\'
46 # string:          "(?:\\\"|[^\\"])+"
47 # comment:         #[^\n]*
48 #
49 class Tokeniser :
50
51         #
52         # FIXME: Instead of truncating self.str, keep a position ?
53         #
54         def __init__( self , str ) :
55
56                 self.str = str
57                 self.lineNumber = 1
58                 self.colNumber = 1
59                 self.reBlank = re.compile( r'^\s*' )
60                 self.reParser = re.compile( '^'
61                         '('
62                                 ';' # end-of-statement
63                                 '|'
64                                 r'\{' # start-of-block
65                                 '|'
66                                 r'\}' # end-of-block
67                                 '|'
68                                 r'(?:[-_.a-z0-9]+|\*)' # identifier
69                                 '|'
70                                 r"'[^']*'" # quoted string
71                                 '|'
72                                 r"'''.+?'''" # quoted string
73                                 '|'
74                                 r'"(?:\\"|[^"])*"' # quoted string
75                                 '|'
76                                 r'#[^\n]*' # comment
77                         ')' ,
78                         re.I|re.S )
79
80         def next( self , __empty = [ None , None , None ] ) :
81
82                 r = self.reBlank.search( self.str )
83                 if r != None :
84                         blank = r.group( 0 )
85                         self.colNumber , self.lineNumber = advancePosition( blank , self.colNumber , self.lineNumber )
86                         self.str = self.str[ r.end( 0 ) : ]
87
88                 if self.str == '' : return __empty
89
90                 # Match the next token
91                 r = self.reParser.search( self.str )
92                 if r == None : return [ False , self.lineNumber , self.colNumber ]
93
94                 # Remove parsed text from the buffer
95                 self.str = self.str[ r.end( 0 ) : ]
96
97                 token = r.group( 0 )
98
99                 # Keep current position
100                 tokenLine = self.lineNumber
101                 tokenColumn = self.colNumber
102
103                 # Advance position after token
104                 self.colNumber , self.lineNumber = advancePosition( token , self.colNumber , self.lineNumber )
105
106                 # Return the token and its position
107                 return token , tokenLine , tokenColumn
108
109 #
110 # Parse configuration
111 #
112 def parse( str , relax = False , warn = False , meta = None ) :
113
114         stack = [ ( 'root' , [] , [] , ( None , None , meta ) ) ]
115         cmd = None
116         newElement = True
117         tok = Tokeniser( str )
118         lastLine , lastColumn = 0 , 0
119         while 1 :
120                 item , line , column = tok.next()
121                 if item == None : break
122                 if item == False :
123                         raise Exception( 'Syntax error at line %s, column %s' % ( line , column ) )
124                 lastLine = line
125                 lastColumn = column
126                 if item.startswith( '#' ) : continue
127                 if relax :
128                         if column == 1 and len( stack ) > 1 and item != '}' :
129                                 while len( stack ) > 1 :
130                                         cmd = stack[ -1 ]
131                                         stack = stack[ : -1 ]
132                                         if cmd[ 0 ] != 'discard' :
133                                                 stack[ -1 ][ 2 ].append( cmd )
134                                 newElement = True
135                                 print '** Error recovered before line %s (missing `}\' ?)' % line
136                 if item == '}' :
137                         if not newElement and cmd != None :
138                                 raise Exception( 'Missing semicolon before line %s, column %s' % ( line , column ) )
139                         cmd = stack[ -1 ]
140                         stack = stack[ : -1 ]
141                         if len( stack ) == 0 :
142                                 raise Exception( 'Unexpected } at line %s, column %s' % ( line , column ) )
143                         if cmd[ 0 ] != 'discard' :
144                                 stack[ -1 ][ 2 ].append( cmd )
145                         newElement = True
146                 elif newElement :
147                         if item in [ ';' , '{' , '}' ] :
148                                 raise Exception( 'Unexpected token `%s\' at line %s, column %s' % ( item , line , column )  )
149                         elif item.find( '\n' ) != -1 :
150                                 raise Exception( 'Unexpected newline character at line %s, column %s' % ( line , column + item.find( '\n' ) ) )
151                         cmd = ( item , [] , [] , ( line , column , meta ) )
152                         newElement = False
153                 elif item == ';' :
154                         stack[ -1 ][ 2 ].append( cmd )
155                         cmd = None
156                         newElement = True
157                 elif item == '{' :
158                         stack.append( cmd )
159                         newElement = True
160                 else :
161                         if item.startswith( "'''" ) :
162                                 item = item[ 3 : -3 ]
163                         elif item.startswith( '"' ) :
164                                 item = unslash( item[ 1 : -1 ] )
165                         elif item.startswith( "'" ) :
166                                 item = item[ 1 : -1 ]
167                         if item.find( '\n' ) != -1 :
168                                 print '** Warning: string with newline character(s)'
169                         cmd[ 1 ].append( item )
170         if len( stack ) != 1 or not newElement :
171                 raise Exception( 'Unexpected end of file (last token was at line %s, column %s)' % ( lastLine , lastColumn ) )
172         return stack[ -1 ]
173
174 #
175 # Helper function to dump configuration
176 #
177 def printTreeInner( t , prt = sys.stdout.write , prefix = '' ) :
178
179         prt( prefix )
180         prt( t[ 0 ] )
181         for kw in t[ 1 ] :
182                 prt( ' ' + kw )
183         prt( ' (%s,%s,%r)' % ( t[ 3 ][ 0 ] , t[ 3 ][ 1 ] , t[ 3 ][ 2 ] ) )
184         if len( t[ 2 ] ) > 0 :
185                 prt( ' {\n' )
186                 for sub in t[ 2 ] :
187                         printTreeInner( sub , prt , prefix + '  ' )
188                 prt( prefix )
189                 prt( '}\n' )
190         else :
191                 prt( ' ;\n' )
192
193 #
194 # Dump configuration
195 #
196 def printTree( t ) :
197
198         if len( t[ 2 ] ) > 0 :
199                 for sub in t[ 2 ] :
200                         printTreeInner( sub )