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