Salome HOME
bad text warning
[modules/kernel.git] / bin / parseConfigFile.py
1 import ConfigParser
2 import os
3 import logging
4 import re
5 from io import StringIO
6 import subprocess
7
8 logging.basicConfig()
9 logConfigParser = logging.getLogger(__name__)
10
11 ADD_TO_PREFIX = 'ADD_TO_'
12 UNSET_KEYWORD = 'UNSET'
13
14
15 # :TRICKY: So ugly solution...
16 class MultiOptSafeConfigParser(ConfigParser.SafeConfigParser):
17   def __init__(self):
18     ConfigParser.SafeConfigParser.__init__(self)
19
20   # copied from python 2.6.8 Lib.ConfigParser.py
21   # modified (see code comments) to handle duplicate keys
22   def _read(self, fp, fpname):
23     """Parse a sectioned setup file.
24
25     The sections in setup file contains a title line at the top,
26     indicated by a name in square brackets (`[]'), plus key/value
27     options lines, indicated by `name: value' format lines.
28     Continuations are represented by an embedded newline then
29     leading whitespace.  Blank lines, lines beginning with a '#',
30     and just about everything else are ignored.
31     """
32     cursect = None                        # None, or a dictionary
33     optname = None
34     lineno = 0
35     e = None                              # None, or an exception
36     while True:
37       line = fp.readline()
38       if not line:
39         break
40       lineno = lineno + 1
41       # comment or blank line?
42       if line.strip() == '' or line[0] in '#;':
43         continue
44       if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
45         # no leading whitespace
46         continue
47       # continuation line?
48       if line[0].isspace() and cursect is not None and optname:
49         value = line.strip()
50         if value:
51           cursect[optname].append(value)
52       # a section header or option header?
53       else:
54         # is it a section header?
55         mo = self.SECTCRE.match(line)
56         if mo:
57           sectname = mo.group('header')
58           if sectname in self._sections:
59             cursect = self._sections[sectname]
60           elif sectname == ConfigParser.DEFAULTSECT:
61             cursect = self._defaults
62           else:
63             cursect = self._dict()
64             cursect['__name__'] = sectname
65             self._sections[sectname] = cursect
66           # So sections can't start with a continuation line
67           optname = None
68         # no section header in the file?
69         elif cursect is None:
70           raise MissingSectionHeaderError(fpname, lineno, line)
71         # an option line?
72         else:
73           mo = self.OPTCRE.match(line)
74           if mo:
75             optname, vi, optval = mo.group('option', 'vi', 'value')
76             optname = self.optionxform(optname.rstrip())
77             # This check is fine because the OPTCRE cannot
78             # match if it would set optval to None
79             if optval is not None:
80               if vi in ('=', ':') and ';' in optval:
81                 # ';' is a comment delimiter only if it follows
82                 # a spacing character
83                 pos = optval.find(';')
84                 if pos != -1 and optval[pos-1].isspace():
85                   optval = optval[:pos]
86               optval = optval.strip()
87               # allow empty values
88               if optval == '""':
89                 optval = ''
90               # REPLACE following line (original):
91               #cursect[optname] = [optval]
92               # BY THESE LINES:
93               # Check if this optname already exists
94               if (optname in cursect) and (cursect[optname] is not None):
95                 cursect[optname][0] += ','+optval
96               else:
97                 cursect[optname] = [optval]
98               # END OF SUBSITUTION
99             else:
100               # valueless option handling
101               cursect[optname] = optval
102           else:
103             # a non-fatal parsing error occurred.  set up the
104             # exception but keep going. the exception will be
105             # raised at the end of the file and will contain a
106             # list of all bogus lines
107             if not e:
108               e = ConfigParser.ParsingError(fpname)
109             e.append(lineno, repr(line))
110     # if any parsing errors occurred, raise an exception
111     if e:
112       raise e
113
114     # join the multi-line values collected while reading
115     all_sections = [self._defaults]
116     all_sections.extend(self._sections.values())
117     for options in all_sections:
118       for name, val in options.items():
119         if isinstance(val, list):
120           options[name] = '\n'.join(val)
121   #
122
123
124 # Parse configuration file
125 # Input: filename, and a list of reserved keywords (environment variables)
126 # Output: a list of pairs (variable, value), and a dictionary associating a list of user-defined values to each reserved keywords
127 # Note: Does not support duplicate keys in a same section
128 def parseConfigFile(filename, reserved = []):
129   config = MultiOptSafeConfigParser()
130   config.optionxform = str # case sensitive
131
132   # :TODO: test file existence
133
134   # Read config file
135   try:
136     config.read(filename)
137   except ConfigParser.MissingSectionHeaderError:
138     logConfigParser.error("No section found in file: %s"%(filename))
139     return []
140
141   return _processConfigFile(config, reserved, filename)
142 #
143
144 def _processConfigFile(config, reserved = [], filename="UNKNOWN FILENAME"):
145   # :TODO: may detect duplicated variables in the same section (raise a warning)
146   #        or even duplicate sections
147
148   unsetVariables = []
149   outputVariables = []
150   # Get raw items for each section, and make some processing for environment variables management
151   reservedKeys = [ADD_TO_PREFIX+str(x) for x in reserved] # produce [ 'ADD_TO_reserved_1', 'ADD_TO_reserved_2', ..., ADD_TO_reserved_n ]
152   reservedValues = dict([str(i),[]] for i in reserved) # create a dictionary in which keys are the 'ADD_TO_reserved_i' and associated values are empty lists: { 'reserved_1':[], 'reserved_2':[], ..., reserved_n:[] }
153   sections = config.sections()
154   for section in sections:
155     entries = config.items(section, raw=False) # use interpolation
156     if len(entries) == 0: # empty section
157       logConfigParser.warning("Empty section: %s in file: %s"%(section, filename))
158       pass
159     for key,val in entries:
160       if key in reserved:
161         logConfigParser.error("Invalid use of reserved variable: %s in file: %s"%(key, filename))
162       elif key == UNSET_KEYWORD:
163         unsetVariables += val.replace(',', ' ').split()
164       else:
165         expandedVal = os.path.expandvars(val) # expand environment variables
166         # Search for not expanded variables (i.e. non-existing environment variables)
167         pattern = re.compile('\${ ( [^}]* ) }', re.VERBOSE) # string enclosed in ${ and }
168         expandedVal = pattern.sub(r'', expandedVal) # remove matching patterns
169         # Trim colons
170         expandedVal = _trimColons(expandedVal)
171
172         if key in reservedKeys:
173           shortKey = key[len(ADD_TO_PREFIX):]
174           vals = expandedVal.split(',')
175           reservedValues[shortKey] += vals
176           # remove left&right spaces on each element
177           vals = [v.strip(' \t\n\r') for v in vals]
178         else:
179           outputVariables.append((key, expandedVal))
180           pass
181         pass # end if key
182       pass # end for key,val
183     pass # end for section
184
185   return unsetVariables, outputVariables, reservedValues
186 #
187
188 def _trimColons(var):
189   v = var
190   # Remove leading and trailing colons (:)
191   pattern = re.compile('^:+ | :+$', re.VERBOSE)
192   v = pattern.sub(r'', v) # remove matching patterns
193   # Remove multiple colons
194   pattern = re.compile('::+', re.VERBOSE)
195   v = pattern.sub(r':', v) # remove matching patterns
196   return v
197 #
198
199 # This class is used to parse .sh environment file
200 # It deals with specific treatments:
201 #    - virtually add a section to configuration file
202 #    - process shell keywords (if, then...)
203 class EnvFileConverter(object):
204   def __init__(self, fp, section_name, reserved = [], outputFile=None):
205     self.fp = fp
206     self.sechead = '[' + section_name + ']\n'
207     self.reserved = reserved
208     self.outputFile = outputFile
209     self.allParsedVariableNames=[]
210     # exclude line that begin with:
211     self.exclude = [ 'if', 'then', 'else', 'fi', '#', 'echo' ]
212     # discard the following keywords if at the beginning of line:
213     self.discard = [ 'export' ]
214     # the following keywords imply a special processing if at the beginning of line:
215     self.special = [ 'unset' ]
216
217   def readline(self):
218     if self.sechead:
219       try:
220         if self.outputFile is not None:
221           self.outputFile.write(self.sechead)
222         return self.sechead
223       finally:
224         self.sechead = None
225     else:
226       line = self.fp.readline()
227       # trim  whitespaces
228       line = line.strip(' \t\n\r')
229       # line of interest? (not beginning by a keyword of self.exclude)
230       for k in self.exclude:
231         if line.startswith(k):
232           return '\n'
233       # look for substrinsg beginning with sharp charcter ('#')
234       line = re.sub(r'#.*$', r'', line)
235       # line to be pre-processed? (beginning by a keyword of self.special)
236       for k in self.special:
237         if k == "unset" and line.startswith(k):
238           line = line[len(k):]
239           line = line.strip(' \t\n\r')
240           line = UNSET_KEYWORD + ": " + line
241       # line to be pre-processed? (beginning by a keyword of self.discard)
242       for k in self.discard:
243         if line.startswith(k):
244           line = line[len(k):]
245           line = line.strip(' \t\n\r')
246       # process reserved keywords
247       for k in self.reserved:
248         if line.startswith(k) and "=" in line:
249           variable, value = line.split('=')
250           value = self._purgeValue(value, k)
251           line = ADD_TO_PREFIX + k + ": " + value
252       # Update list of variable names
253       if "cleandup()" in line:
254         print "WARNING: parseConfigFile.py: skip cleandup and look for '# PRODUCT environment'"
255         while True:
256           line = self.fp.readline()
257           if "# PRODUCT environment" in line:
258             print "WARNING: parseConfigFile.py: '# PRODUCT environment' found"
259             break
260       while "clean " in line[0:6]: #skip clean calls with ending ";" crash
261         line = self.fp.readline()
262       if "=" in line:
263         try:
264           variable, value = line.split('=')
265         except: #avoid error for complicated sh line xx=`...=...`, but warning
266           print "WARNING: parseConfigFile.py: line with multiples '=' character are hazardous: '"+line+"'"
267           variable, value = line.split('=',1)          
268         self.allParsedVariableNames.append(variable)
269       # Self-extending variables that are not in reserved keywords
270       # Example: FOO=something:${FOO}
271       # :TODO:
272       #
273       # replace "${FOO}" and "$FOO" and ${FOO} and $FOO by %(FOO)s if FOO is
274       # defined in current file (i.e. it is not an external environment variable)
275       for k in self.allParsedVariableNames:
276         key = r'\$\{?'+k+'\}?'
277         pattern = re.compile(key, re.VERBOSE)
278         line = pattern.sub(r'%('+k+')s', line)
279       # Remove quotes
280         pattern = re.compile(r'\"', re.VERBOSE)
281         line = pattern.sub(r'', line)
282       #
283       # Replace `shell_command` by its result
284       def myrep(obj):
285         obj = re.sub('`', r'', obj.group(0)) # remove quotes
286         obj = obj.split()
287         res = subprocess.Popen(obj, stdout=subprocess.PIPE).communicate()[0]
288         res = res.strip(' \t\n\r') # trim whitespaces
289         return res
290       #
291       line = re.sub('`[^`]+`', myrep, line)
292       #
293       if self.outputFile is not None:
294         self.outputFile.write(line+'\n')
295       return line
296
297   def _purgeValue(self, value, name):
298     # Replace foo:${PATTERN}:bar or foo:$PATTERN:bar by foo:bar
299     key = r'\$\{?'+name+'\}?'
300     pattern = re.compile(key, re.VERBOSE)
301     value = pattern.sub(r'', value)
302
303     # trim colons
304     value = _trimColons(value)
305
306     return value
307   #
308
309 # Convert .sh environment file to configuration file format
310 def convertEnvFileToConfigFile(envFilename, configFilename):
311   #reserved=['PATH', 'LD_LIBRARY_PATH', 'PYTHONPATH']
312   logConfigParser.debug('convert env file %s to %s'%(envFilename, configFilename))
313   reserved=['PATH', 'LD_LIBRARY_PATH', 'PYTHONPATH', 'MANPATH', 'R_LIBS', 'PV_PLUGIN_PATH', 'TCLLIBPATH', 'TKLIBPATH']
314   fileContents = open(envFilename, 'r').read()
315
316   pattern = re.compile('\n[\n]+', re.VERBOSE) # multiple '\n'
317   fileContents = pattern.sub(r'\n', fileContents) # replace by a single '\n'
318
319   finput = StringIO(unicode(fileContents))
320   foutput = open(configFilename, 'w')
321
322   config = MultiOptSafeConfigParser()
323   config.optionxform = str # case sensitive
324   config.readfp(EnvFileConverter(finput, 'SALOME Configuration', reserved, outputFile=foutput))
325
326   foutput.close()
327
328   logConfigParser.info('Configuration file generated: %s'%configFilename)
329 #