Salome HOME
Merge branch 'master' of https://git.salome-platform.org/git/modules/kernel
[modules/kernel.git] / bin / parseConfigFile.py
1 # Copyright (C) 2013-2014  CEA/DEN, EDF R&D, OPEN CASCADE
2 #
3 # This library is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU Lesser General Public
5 # License as published by the Free Software Foundation; either
6 # version 2.1 of the License, or (at your option) any later version.
7 #
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 #
19
20 import ConfigParser
21 import os
22 import logging
23 import re
24 from io import StringIO
25 import subprocess
26 from salomeContextUtils import SalomeContextException
27
28 logging.basicConfig()
29 logConfigParser = logging.getLogger(__name__)
30
31 ADD_TO_PREFIX = 'ADD_TO_'
32 UNSET_KEYWORD = 'UNSET'
33
34
35 # :TRICKY: So ugly solution...
36 class MultiOptSafeConfigParser(ConfigParser.SafeConfigParser):
37   def __init__(self):
38     ConfigParser.SafeConfigParser.__init__(self)
39
40   # copied from python 2.6.8 Lib.ConfigParser.py
41   # modified (see code comments) to handle duplicate keys
42   def _read(self, fp, fpname):
43     """Parse a sectioned setup file.
44
45     The sections in setup file contains a title line at the top,
46     indicated by a name in square brackets (`[]'), plus key/value
47     options lines, indicated by `name: value' format lines.
48     Continuations are represented by an embedded newline then
49     leading whitespace.  Blank lines, lines beginning with a '#',
50     and just about everything else are ignored.
51     """
52     cursect = None                        # None, or a dictionary
53     optname = None
54     lineno = 0
55     e = None                              # None, or an exception
56     while True:
57       line = fp.readline()
58       if not line:
59         break
60       lineno = lineno + 1
61       # comment or blank line?
62       if line.strip() == '' or line[0] in '#;':
63         continue
64       if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
65         # no leading whitespace
66         continue
67       # continuation line?
68       if line[0].isspace() and cursect is not None and optname:
69         value = line.strip()
70         if value:
71           cursect[optname].append(value)
72       # a section header or option header?
73       else:
74         # is it a section header?
75         mo = self.SECTCRE.match(line)
76         if mo:
77           sectname = mo.group('header')
78           if sectname in self._sections:
79             cursect = self._sections[sectname]
80           elif sectname == ConfigParser.DEFAULTSECT:
81             cursect = self._defaults
82           else:
83             cursect = self._dict()
84             cursect['__name__'] = sectname
85             self._sections[sectname] = cursect
86           # So sections can't start with a continuation line
87           optname = None
88         # no section header in the file?
89         elif cursect is None:
90           raise ConfigParser.MissingSectionHeaderError(fpname, lineno, line)
91         # an option line?
92         else:
93           mo = self.OPTCRE.match(line)
94           if mo:
95             optname, vi, optval = mo.group('option', 'vi', 'value')
96             optname = self.optionxform(optname.rstrip())
97             # This check is fine because the OPTCRE cannot
98             # match if it would set optval to None
99             if optval is not None:
100               if vi in ('=', ':') and ';' in optval:
101                 # ';' is a comment delimiter only if it follows
102                 # a spacing character
103                 pos = optval.find(';')
104                 if pos != -1 and optval[pos-1].isspace():
105                   optval = optval[:pos]
106               optval = optval.strip()
107               # ADD THESE LINES
108               splittedComments = optval.split('#')
109               optval = splittedComments[0].strip().strip("'").strip('"')
110               #if len(splittedComments) > 1:
111               #  optval += " #" + " ".join(splittedComments[1:])
112               # END OF ADD
113               # allow empty values
114               if optval == '""':
115                 optval = ''
116               # REPLACE following line (original):
117               #cursect[optname] = [optval]
118               # BY THESE LINES:
119               # Check if this optname already exists
120               if (optname in cursect) and (cursect[optname] is not None):
121                 cursect[optname][0] += ','+optval
122               else:
123                 cursect[optname] = [optval]
124               # END OF SUBSITUTION
125             else:
126               # valueless option handling
127               cursect[optname] = optval
128           else:
129             # a non-fatal parsing error occurred.  set up the
130             # exception but keep going. the exception will be
131             # raised at the end of the file and will contain a
132             # list of all bogus lines
133             if not e:
134               e = ConfigParser.ParsingError(fpname)
135             e.append(lineno, repr(line))
136     # if any parsing errors occurred, raise an exception
137     if e:
138       raise e
139
140     # join the multi-line values collected while reading
141     all_sections = [self._defaults]
142     all_sections.extend(self._sections.values())
143     for options in all_sections:
144       for name, val in options.items():
145         if isinstance(val, list):
146           options[name] = '\n'.join(val)
147   #
148
149
150 # Parse configuration file
151 # Input: filename, and a list of reserved keywords (environment variables)
152 # Output: a list of pairs (variable, value), and a dictionary associating a list of user-defined values to each reserved keywords
153 # Note: Does not support duplicate keys in a same section
154 def parseConfigFile(filename, reserved = []):
155   config = MultiOptSafeConfigParser()
156   config.optionxform = str # case sensitive
157
158   # :TODO: test file existence
159
160   # Read config file
161   try:
162     config.read(filename)
163   except ConfigParser.MissingSectionHeaderError:
164     logConfigParser.error("No section found in file: %s"%(filename))
165     return []
166
167   try:
168     return __processConfigFile(config, reserved, filename)
169   except ConfigParser.InterpolationMissingOptionError, e:
170     msg = "A variable may be undefined in SALOME context file: %s\nParser error is: %s\n"%(filename, e)
171     raise SalomeContextException(msg)
172 #
173
174 def __processConfigFile(config, reserved = [], filename="UNKNOWN FILENAME"):
175   # :TODO: may detect duplicated variables in the same section (raise a warning)
176   #        or even duplicate sections
177
178   unsetVariables = []
179   outputVariables = []
180   # Get raw items for each section, and make some processing for environment variables management
181   reservedKeys = [ADD_TO_PREFIX+str(x) for x in reserved] # produce [ 'ADD_TO_reserved_1', 'ADD_TO_reserved_2', ..., ADD_TO_reserved_n ]
182   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:[] }
183   sections = config.sections()
184   for section in sections:
185     entries = config.items(section, raw=False) # use interpolation
186     if len(entries) == 0: # empty section
187       logConfigParser.warning("Empty section: %s in file: %s"%(section, filename))
188       pass
189     for key,val in entries:
190       if key in reserved:
191         logConfigParser.error("Invalid use of reserved variable: %s in file: %s"%(key, filename))
192       elif key == UNSET_KEYWORD:
193         unsetVariables += val.replace(',', ' ').split()
194       else:
195         expandedVal = os.path.expandvars(val) # expand environment variables
196         # Search for not expanded variables (i.e. non-existing environment variables)
197         pattern = re.compile('\${ ( [^}]* ) }', re.VERBOSE) # string enclosed in ${ and }
198         expandedVal = pattern.sub(r'', expandedVal) # remove matching patterns
199         # Trim colons
200         if not "DLIM8VAR" in key: # special case: DISTENE licence key can contain double clons (::)
201           expandedVal = _trimColons(expandedVal)
202
203         if key in reservedKeys:
204           shortKey = key[len(ADD_TO_PREFIX):]
205           vals = expandedVal.split(',')
206           reservedValues[shortKey] += vals
207           # remove left&right spaces on each element
208           vals = [v.strip(' \t\n\r') for v in vals]
209         else:
210           outputVariables.append((key, expandedVal))
211           pass
212         pass # end if key
213       pass # end for key,val
214     pass # end for section
215
216   # remove duplicate values
217   outVars = []
218   for (var, values) in outputVariables:
219     vals = values.split(',')
220     vals = list(set(vals))
221     outVars.append((var, ','.join(vals)))
222
223   return unsetVariables, outVars, reservedValues
224 #
225
226 def _trimColons(var):
227   v = var
228   # Remove leading and trailing colons (:)
229   pattern = re.compile('^:+ | :+$', re.VERBOSE)
230   v = pattern.sub(r'', v) # remove matching patterns
231   # Remove multiple colons
232   pattern = re.compile('::+', re.VERBOSE)
233   v = pattern.sub(r':', v) # remove matching patterns
234   return v
235 #
236
237 # This class is used to parse .sh environment file
238 # It deals with specific treatments:
239 #    - virtually add a section to configuration file
240 #    - process shell keywords (if, then...)
241 class EnvFileConverter(object):
242   def __init__(self, fp, section_name, reserved = [], outputFile=None):
243     self.fp = fp
244     self.sechead = '[' + section_name + ']\n'
245     self.reserved = reserved
246     self.outputFile = outputFile
247     self.allParsedVariableNames=[]
248     # exclude line that begin with:
249     self.exclude = [ 'if', 'then', 'else', 'fi', '#', 'echo', 'exit' ]
250     self.exclude.append('$gconfTool') # QUICK FIX :TODO: provide method to extend this variable
251     # discard the following keywords if at the beginning of line:
252     self.discard = [ 'export' ]
253     # the following keywords imply a special processing if at the beginning of line:
254     self.special = [ 'unset' ]
255
256   def readline(self):
257     if self.sechead:
258       try:
259         if self.outputFile is not None:
260           self.outputFile.write(self.sechead)
261         return self.sechead
262       finally:
263         self.sechead = None
264     else:
265       line = self.fp.readline()
266       # trim  whitespaces
267       line = line.strip(' \t\n\r')
268       # line of interest? (not beginning by a keyword of self.exclude)
269       for k in self.exclude:
270         if line.startswith(k):
271           return '\n'
272       # look for substrinsg beginning with sharp charcter ('#')
273       line = re.sub(r'#.*$', r'', line)
274       # line to be pre-processed? (beginning by a keyword of self.special)
275       for k in self.special:
276         if k == "unset" and line.startswith(k):
277           line = line[len(k):]
278           line = line.strip(' \t\n\r')
279           line = UNSET_KEYWORD + ": " + line
280       # line to be pre-processed? (beginning by a keyword of self.discard)
281       for k in self.discard:
282         if line.startswith(k):
283           line = line[len(k):]
284           line = line.strip(' \t\n\r')
285       # process reserved keywords
286       for k in self.reserved:
287         if line.startswith(k) and "=" in line:
288           variable, value = line.split('=')
289           value = self._purgeValue(value, k)
290           line = ADD_TO_PREFIX + k + ": " + value
291       # Update list of variable names
292       # :TODO: define excludeBlock variable (similar to exclude) and provide method to extend it
293       if "cleandup()" in line:
294         print "WARNING: parseConfigFile.py: skip cleandup and look for '# PRODUCT environment'"
295         while True:
296           line = self.fp.readline()
297           if "# PRODUCT environment" in line:
298             print "WARNING: parseConfigFile.py: '# PRODUCT environment' found"
299             break
300       while "clean " in line[0:6]: #skip clean calls with ending ";" crash
301         line = self.fp.readline()
302       # Extract variable=value
303       if "=" in line:
304         try:
305           variable, value = line.split('=')
306         except: #avoid error for complicated sh line xx=`...=...`, but warning
307           print "WARNING: parseConfigFile.py: line with multiples '=' character are hazardous: '"+line+"'"
308           variable, value = line.split('=',1)
309           pass
310
311         # Self-extending variables that are not in reserved keywords
312         # Example: FOO=something:${FOO}
313         # In this case, remove the ${FOO} in value
314         if variable in value:
315           value = self._purgeValue(value, variable)
316           line = "%s=%s"%(variable,value)
317
318         self.allParsedVariableNames.append(variable)
319       # End of extraction
320
321       if not line:
322         return line
323
324       #
325       # replace "${FOO}" and "$FOO" and ${FOO} and $FOO by %(FOO)s if FOO is
326       # defined in current file (i.e. it is not an external environment variable)
327       for k in self.allParsedVariableNames:
328         key = r'\$\{?'+k+'\}?'
329         pattern = re.compile(key, re.VERBOSE)
330         line = pattern.sub(r'%('+k+')s', line)
331         # Remove quotes (if line does not contain whitespaces)
332         try:
333           variable, value = line.split('=', 1)
334         except ValueError:
335           variable, value = line.split(':', 1)
336         if not ' ' in value.strip():
337           pattern = re.compile(r'\"', re.VERBOSE)
338           line = pattern.sub(r'', line)
339       #
340
341       # Replace `shell_command` by its result
342       def myrep(obj):
343         obj = re.sub('`', r'', obj.group(0)) # remove quotes
344         obj = obj.split()
345         res = subprocess.Popen(obj, stdout=subprocess.PIPE).communicate()[0]
346         res = res.strip(' \t\n\r') # trim whitespaces
347         return res
348       #
349       line = re.sub('`[^`]+`', myrep, line)
350       #
351       if self.outputFile is not None:
352         self.outputFile.write(line+'\n')
353       return line
354
355   def _purgeValue(self, value, name):
356     # Replace foo:${PATTERN}:bar or foo:$PATTERN:bar by foo:bar
357     key = r'\$\{?'+name+'\}?'
358     pattern = re.compile(key, re.VERBOSE)
359     value = pattern.sub(r'', value)
360
361     # trim colons
362     value = _trimColons(value)
363
364     return value
365   #
366
367 # Convert .sh environment file to configuration file format
368 def convertEnvFileToConfigFile(envFilename, configFilename, reserved=[]):
369   logConfigParser.debug('convert env file %s to %s'%(envFilename, configFilename))
370   fileContents = open(envFilename, 'r').read()
371
372   pattern = re.compile('\n[\n]+', re.VERBOSE) # multiple '\n'
373   fileContents = pattern.sub(r'\n', fileContents) # replace by a single '\n'
374
375   finput = StringIO(unicode(fileContents))
376   foutput = open(configFilename, 'w')
377
378   config = MultiOptSafeConfigParser()
379   config.optionxform = str # case sensitive
380   config.readfp(EnvFileConverter(finput, 'SALOME Configuration', reserved, outputFile=foutput))
381
382   foutput.close()
383
384   logConfigParser.info('Configuration file generated: %s'%configFilename)
385 #