Salome HOME
Merge remote-tracking branch 'origin/master' into pre/fix_single_study
[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 ConfigParser.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               # ADD THESE LINES
107               splittedComments = optval.split('#')
108               optval = splittedComments[0].strip().strip("'").strip('"')
109               #if len(splittedComments) > 1:
110               #  optval += " #" + " ".join(splittedComments[1:])
111               # END OF ADD
112               # allow empty values
113               if optval == '""':
114                 optval = ''
115               # REPLACE following line (original):
116               #cursect[optname] = [optval]
117               # BY THESE LINES:
118               # Check if this optname already exists
119               if (optname in cursect) and (cursect[optname] is not None):
120                 cursect[optname][0] += ','+optval
121               else:
122                 cursect[optname] = [optval]
123               # END OF SUBSITUTION
124             else:
125               # valueless option handling
126               cursect[optname] = optval
127           else:
128             # a non-fatal parsing error occurred.  set up the
129             # exception but keep going. the exception will be
130             # raised at the end of the file and will contain a
131             # list of all bogus lines
132             if not e:
133               e = ConfigParser.ParsingError(fpname)
134             e.append(lineno, repr(line))
135     # if any parsing errors occurred, raise an exception
136     if e:
137       raise e
138
139     # join the multi-line values collected while reading
140     all_sections = [self._defaults]
141     all_sections.extend(self._sections.values())
142     for options in all_sections:
143       for name, val in options.items():
144         if isinstance(val, list):
145           options[name] = '\n'.join(val)
146   #
147
148
149 # Parse configuration file
150 # Input: filename, and a list of reserved keywords (environment variables)
151 # Output: a list of pairs (variable, value), and a dictionary associating a list of user-defined values to each reserved keywords
152 # Note: Does not support duplicate keys in a same section
153 def parseConfigFile(filename, reserved = []):
154   config = MultiOptSafeConfigParser()
155   config.optionxform = str # case sensitive
156
157   # :TODO: test file existence
158
159   # Read config file
160   try:
161     config.read(filename)
162   except ConfigParser.MissingSectionHeaderError:
163     logConfigParser.error("No section found in file: %s"%(filename))
164     return []
165
166   return __processConfigFile(config, reserved, filename)
167 #
168
169 def __processConfigFile(config, reserved = [], filename="UNKNOWN FILENAME"):
170   # :TODO: may detect duplicated variables in the same section (raise a warning)
171   #        or even duplicate sections
172
173   unsetVariables = []
174   outputVariables = []
175   # Get raw items for each section, and make some processing for environment variables management
176   reservedKeys = [ADD_TO_PREFIX+str(x) for x in reserved] # produce [ 'ADD_TO_reserved_1', 'ADD_TO_reserved_2', ..., ADD_TO_reserved_n ]
177   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:[] }
178   sections = config.sections()
179   for section in sections:
180     entries = config.items(section, raw=False) # use interpolation
181     if len(entries) == 0: # empty section
182       logConfigParser.warning("Empty section: %s in file: %s"%(section, filename))
183       pass
184     for key,val in entries:
185       if key in reserved:
186         logConfigParser.error("Invalid use of reserved variable: %s in file: %s"%(key, filename))
187       elif key == UNSET_KEYWORD:
188         unsetVariables += val.replace(',', ' ').split()
189       else:
190         expandedVal = os.path.expandvars(val) # expand environment variables
191         # Search for not expanded variables (i.e. non-existing environment variables)
192         pattern = re.compile('\${ ( [^}]* ) }', re.VERBOSE) # string enclosed in ${ and }
193         expandedVal = pattern.sub(r'', expandedVal) # remove matching patterns
194         # Trim colons
195         if not "DLIM8VAR" in key: # special case: DISTENE licence key can contain double clons (::)
196           expandedVal = _trimColons(expandedVal)
197
198         if key in reservedKeys:
199           shortKey = key[len(ADD_TO_PREFIX):]
200           vals = expandedVal.split(',')
201           reservedValues[shortKey] += vals
202           # remove left&right spaces on each element
203           vals = [v.strip(' \t\n\r') for v in vals]
204         else:
205           outputVariables.append((key, expandedVal))
206           pass
207         pass # end if key
208       pass # end for key,val
209     pass # end for section
210
211   # remove duplicate values
212   outVars = []
213   for (var, values) in outputVariables:
214     vals = values.split(',')
215     vals = list(set(vals))
216     outVars.append((var, ','.join(vals)))
217
218   return unsetVariables, outVars, reservedValues
219 #
220
221 def _trimColons(var):
222   v = var
223   # Remove leading and trailing colons (:)
224   pattern = re.compile('^:+ | :+$', re.VERBOSE)
225   v = pattern.sub(r'', v) # remove matching patterns
226   # Remove multiple colons
227   pattern = re.compile('::+', re.VERBOSE)
228   v = pattern.sub(r':', v) # remove matching patterns
229   return v
230 #
231
232 # This class is used to parse .sh environment file
233 # It deals with specific treatments:
234 #    - virtually add a section to configuration file
235 #    - process shell keywords (if, then...)
236 class EnvFileConverter(object):
237   def __init__(self, fp, section_name, reserved = [], outputFile=None):
238     self.fp = fp
239     self.sechead = '[' + section_name + ']\n'
240     self.reserved = reserved
241     self.outputFile = outputFile
242     self.allParsedVariableNames=[]
243     # exclude line that begin with:
244     self.exclude = [ 'if', 'then', 'else', 'fi', '#', 'echo', 'exit' ]
245     self.exclude.append('$gconfTool') # QUICK FIX :TODO: provide method to extend this variable
246     # discard the following keywords if at the beginning of line:
247     self.discard = [ 'export' ]
248     # the following keywords imply a special processing if at the beginning of line:
249     self.special = [ 'unset' ]
250
251   def readline(self):
252     if self.sechead:
253       try:
254         if self.outputFile is not None:
255           self.outputFile.write(self.sechead)
256         return self.sechead
257       finally:
258         self.sechead = None
259     else:
260       line = self.fp.readline()
261       # trim  whitespaces
262       line = line.strip(' \t\n\r')
263       # line of interest? (not beginning by a keyword of self.exclude)
264       for k in self.exclude:
265         if line.startswith(k):
266           return '\n'
267       # look for substrinsg beginning with sharp charcter ('#')
268       line = re.sub(r'#.*$', r'', line)
269       # line to be pre-processed? (beginning by a keyword of self.special)
270       for k in self.special:
271         if k == "unset" and line.startswith(k):
272           line = line[len(k):]
273           line = line.strip(' \t\n\r')
274           line = UNSET_KEYWORD + ": " + line
275       # line to be pre-processed? (beginning by a keyword of self.discard)
276       for k in self.discard:
277         if line.startswith(k):
278           line = line[len(k):]
279           line = line.strip(' \t\n\r')
280       # process reserved keywords
281       for k in self.reserved:
282         if line.startswith(k) and "=" in line:
283           variable, value = line.split('=')
284           value = self._purgeValue(value, k)
285           line = ADD_TO_PREFIX + k + ": " + value
286       # Update list of variable names
287       # :TODO: define excludeBlock variable (similar to exclude) and provide method to extend it
288       if line.startswith("LOGNAME="):
289         return "\n"
290       if "cleandup()" in line:
291         print "WARNING: parseConfigFile.py: skip cleandup and look for '# PRODUCT environment'"
292         while True:
293           line = self.fp.readline()
294           if "# PRODUCT environment" in line:
295             print "WARNING: parseConfigFile.py: '# PRODUCT environment' found"
296             break
297       while "clean " in line[0:6]: #skip clean calls with ending ";" crash
298         line = self.fp.readline()
299       # Extract variable=value
300       if "=" in line:
301         try:
302           variable, value = line.split('=')
303         except: #avoid error for complicated sh line xx=`...=...`, but warning
304           print "WARNING: parseConfigFile.py: line with multiples '=' character are hazardous: '"+line+"'"
305           variable, value = line.split('=',1)
306           pass
307
308         # Self-extending variables that are not in reserved keywords
309         # Example: FOO=something:${FOO}
310         # In this case, remove the ${FOO} in value
311         if variable in value:
312           value = self._purgeValue(value, variable)
313           line = "%s=%s"%(variable,value)
314
315         self.allParsedVariableNames.append(variable)
316       # End of extraction
317
318       if not line:
319         return line
320
321       #
322       # replace "${FOO}" and "$FOO" and ${FOO} and $FOO by %(FOO)s if FOO is
323       # defined in current file (i.e. it is not an external environment variable)
324       for k in self.allParsedVariableNames:
325         key = r'\$\{?'+k+'\}?'
326         pattern = re.compile(key, re.VERBOSE)
327         line = pattern.sub(r'%('+k+')s', line)
328         # Remove quotes (if line does not contain whitespaces)
329         try:
330           variable, value = line.split('=', 1)
331         except ValueError:
332           variable, value = line.split(':', 1)
333         if not ' ' in value.strip():
334           pattern = re.compile(r'\"', re.VERBOSE)
335           line = pattern.sub(r'', line)
336       #
337
338       # Replace `shell_command` by its result
339       def myrep(obj):
340         obj = re.sub('`', r'', obj.group(0)) # remove quotes
341         obj = obj.split()
342         res = subprocess.Popen(obj, stdout=subprocess.PIPE).communicate()[0]
343         res = res.strip(' \t\n\r') # trim whitespaces
344         return res
345       #
346       line = re.sub('`[^`]+`', myrep, line)
347       #
348       if self.outputFile is not None:
349         self.outputFile.write(line+'\n')
350       return line
351
352   def _purgeValue(self, value, name):
353     # Replace foo:${PATTERN}:bar or foo:$PATTERN:bar by foo:bar
354     key = r'\$\{?'+name+'\}?'
355     pattern = re.compile(key, re.VERBOSE)
356     value = pattern.sub(r'', value)
357
358     # trim colons
359     value = _trimColons(value)
360
361     return value
362   #
363
364 # Convert .sh environment file to configuration file format
365 def convertEnvFileToConfigFile(envFilename, configFilename, reserved=[]):
366   logConfigParser.debug('convert env file %s to %s'%(envFilename, configFilename))
367   fileContents = open(envFilename, 'r').read()
368
369   pattern = re.compile('\n[\n]+', re.VERBOSE) # multiple '\n'
370   fileContents = pattern.sub(r'\n', fileContents) # replace by a single '\n'
371
372   finput = StringIO(unicode(fileContents))
373   foutput = open(configFilename, 'w')
374
375   config = MultiOptSafeConfigParser()
376   config.optionxform = str # case sensitive
377   config.readfp(EnvFileConverter(finput, 'SALOME Configuration', reserved, outputFile=foutput))
378
379   foutput.close()
380
381   logConfigParser.info('Configuration file generated: %s'%configFilename)
382 #