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