-
Notifications
You must be signed in to change notification settings - Fork 0
/
configfile.py
246 lines (210 loc) · 6.66 KB
/
configfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#!/usr/bin/python2
# Copyright 2010 Daniel Robbins, Funtoo Technologies, LLC.
#
# FUNTOO TECHNOLOGIES INTERNAL SOURCE CODE
# ALPHA - DO NOT RELEASE - NOT FOR DISTRIBUTION
#
# The unauthorized reproduction or distribution of this copyrighted work is
# illegal, and may result in civil or criminal liability.
import re
class ConfigFile(object):
def __repr__(self):
if self.parent:
return "ConfigFile(%s,parent=%s)" % ( self.path.diskpath, self.parent.path.diskpath )
else:
return "ConfigFile(%s)" % self.path.diskpath
# This class implements a parser and class for a Portage-style configuration
# file.
#
# You can use it like this:
# a=ConfigFile("/etc/make.globals")
# b=ConfigFile("/etc/make.conf",parent=a)
#
# Then ConfigFile "b" can reference/expand variables defined in ConfigFile "a".
#
# The syntax is very close to standard Bourne shell variable definition syntax,
# with the following limitations:
# 0) Only the variable expansion "${foo}" is recognized (not "$foo")
#
# 1) Variable data must be enclosed in double-quotes, not single quotes (foo="bar")
# and not no-quotes (foo=bar).
#
# 2) Like shell, multiple-line variables are allowed, as follows:
#
# a="foo
# bar"
#
# However, the trailing '"' will only be recognized as a string termination
# character if it appears at the end of a line, optionally padded only by
# whitespace. This means that our minimal parser allows the following
# strings to be defined, which would be illegal in shell syntax:
#
# a=" "foo bar" oni "
# (this would set "a" to ' "foo bar" oni ')
#
# This dramatically simplifies the code and speeds things up too, since we
# can do some parsing of lines line-by-line rather than character-by-character.
# It eliminates a lot of complexity that we'd need to deal with if we supported
# a full shell syntax.
#
# 3) Backslash escaping only works for '\$', '\\', and '\"' for backwards
# compatibility.
#
# Hopefully, this will fully support all existing Portage configuration files without
# requiring complex code.
# The configuration file format should be kept as simple as possible
# in order to keep the implementation as simple as possible. Shell format
# sucks, so it doesn't make sense to go crazy here.
def __init__(self, path, parent=None):
self.path = path
self.data = None
self.parent = parent
def __getitem__(self,key):
# this is a recursive method that is slow and ready to optimize.
if self.data == None:
self._read()
if self.data.has_key(key):
return self.data[key]
elif self.parent and self.parent.has_key(key):
return self.parent[key]
else:
return ""
_valid_varname = re.compile("\A[a-zA-Z_]\w+$")
def keys(self,recurse=True):
keys = []
if self.data == None:
self._read()
keys.extend(self.data.keys())
if recurse and self.parent:
keys.extend(self.parent.keys(recurse))
return keys
def getExpansion(self,vardata):
pos = 0
while pos < len(vardata):
# quickly scan for all normal text, and yield it
# back to the caller when we reach the end of
# string or the first special character:
pos2 = pos
while pos2 < len(vardata):
if vardata[pos2] in [ "$", "\\" ]:
break
else:
pos2 += 1
# return "normal" string data to caller:
if pos != pos2:
yield vardata[pos:pos2]
# update pos to reflect our current position:
pos = pos2
# At this point, we are either at end of string, or
# we have found a "special" character that requires
# further processing... if we're at end, stop:
if pos >= len(vardata):
break
if vardata[pos:pos+2] == "${":
# variable - scan for end...
pos2 = pos + 2
while vardata[pos2] != "}":
pos2 += 1
if pos2 >= len(vardata):
# unterminated variable
print "unterminated VAR"
return
varname = vardata[pos+2:pos2]
yield [varname]
pos = pos2 + 1
elif vardata[pos:pos+2] == "\\\\":
yield "\\\\"
pos += 2
elif vardata[pos:pos+2] == '\\"':
yield '"'
pos += 2
elif vardata[pos:pos+2] == '\\$':
yield '$'
pos += 2
else:
yield vardata[pos]
pos += 1
def expand_var(self,vardata,stack=[],recurse=True):
out = ""
for part in self.getExpansion(vardata):
if type(part) == type([]):
varname = part[0]
if varname in self.data:
out += self.expand_var(self.data[varname],stack[:].append(varname))
elif recurse and self.parent and self.parent.has_key(varname):
out += self.expand_var(self.parent[varname],stack[:].append(varname))
else:
continue
else:
out += part
return out
def __contains__(self,key):
if self.data == None:
self._read()
if self.data.has_key(key):
return True
elif self.parent and self.parent.has_key(key):
return True
return False
def has_key(self,key,recurse=True):
if self.data == None:
self._read()
if self.data.has_key(key):
return True
elif recurse and self.parent and self.parent.has_key(key):
return True
return False
def _read(self):
a = open(self.path.diskpath,"r")
lines = a.readlines()
a.close()
self.data = {}
lpos = 0
while lpos < len(lines):
line = lines[lpos][:-1].lstrip()
if line == "\n" or len(line) == 0 or line[0] == "#":
lpos += 1
continue
equals = line.find('=')
if equals == -1:
print "ERROR no variable def"
# TODO raise exception - no variable definition
pass
varname = line[0:equals]
if line[equals:equals+2] != '="':
# single line variable with no quotes
self.data[varname] = self.expand_var(line[equals+1:])
lpos += 1
continue
vardata = line[equals+2:]
if self._valid_varname.match(varname) == None:
# TODO raise exception - invalid variable name
print "invalid var name"
lpos += 1
continue
accum = ""
while vardata.rstrip()[-1] != '"':
# look at successive lines until trailing "'" is found:
# trailing backslash support:
if vardata[-1] == "\\":
vardata = vardata[:-1]
accum += vardata
lpos += 1
if lpos >= len(lines):
# TODO: error, no end quote, got to end of file
print "NO END QUOTE for",varname
pass
# for new vardata, remove trailing newline and any whitespace on the left, prepend a " "
# this ensures that the 'foo bar oni' example above has one space between each word...
vardata = " " + lines[lpos][:-1].lstrip()
# keep looking...
accum += vardata.rstrip()[:-1]
self.data[varname] = self.expand_var(accum)
lpos +=1
continue
if __name__ == "__main__":
from access import *
z=ConfigFile(FilePath("/usr/share/portage/config/make.globals"))
a=ConfigFile(FilePath("/etc/make.conf"), parent=z)
for key in a.keys():
print "%s: '%s'" % (key,a[key])