Salome HOME
Merge remote-tracking branch 'origin/vsr/fix_single_study_pb' 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 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   return unsetVariables, outputVariables, reservedValues
206 #
207
208 def _trimColons(var):
209   v = var
210   # Remove leading and trailing colons (:)
211   pattern = re.compile('^:+ | :+$', re.VERBOSE)
212   v = pattern.sub(r'', v) # remove matching patterns
213   # Remove multiple colons
214   pattern = re.compile('::+', re.VERBOSE)
215   v = pattern.sub(r':', v) # remove matching patterns
216   return v
217 #
218
219 # This class is used to parse .sh environment file
220 # It deals with specific treatments:
221 #    - virtually add a section to configuration file
222 #    - process shell keywords (if, then...)
223 class EnvFileConverter(object):
224   def __init__(self, fp, section_name, reserved = [], outputFile=None):
225     self.fp = fp
226     self.sechead = '[' + section_name + ']\n'
227     self.reserved = reserved
228     self.outputFile = outputFile
229     self.allParsedVariableNames=[]
230     # exclude line that begin with:
231     self.exclude = [ 'if', 'then', 'else', 'fi', '#', 'echo', 'exit' ]
232     self.exclude.append('$gconfTool') # QUICK FIX :TODO: provide method to extend this variable
233     # discard the following keywords if at the beginning of line:
234     self.discard = [ 'export' ]
235     # the following keywords imply a special processing if at the beginning of line:
236     self.special = [ 'unset' ]
237
238   def readline(self):
239     if self.sechead:
240       try:
241         if self.outputFile is not None:
242           self.outputFile.write(self.sechead)
243         return self.sechead
244       finally:
245         self.sechead = None
246     else:
247       line = self.fp.readline()
248       # trim  whitespaces
249       line = line.strip(' \t\n\r')
250       # line of interest? (not beginning by a keyword of self.exclude)
251       for k in self.exclude:
252         if line.startswith(k):
253           return '\n'
254       # look for substrinsg beginning with sharp charcter ('#')
255       line = re.sub(r'#.*$', r'', line)
256       # line to be pre-processed? (beginning by a keyword of self.special)
257       for k in self.special:
258         if k == "unset" and line.startswith(k):
259           line = line[len(k):]
260           line = line.strip(' \t\n\r')
261           line = UNSET_KEYWORD + ": " + line
262       # line to be pre-processed? (beginning by a keyword of self.discard)
263       for k in self.discard:
264         if line.startswith(k):
265           line = line[len(k):]
266           line = line.strip(' \t\n\r')
267       # process reserved keywords
268       for k in self.reserved:
269         if line.startswith(k) and "=" in line:
270           variable, value = line.split('=')
271           value = self._purgeValue(value, k)
272           line = ADD_TO_PREFIX + k + ": " + value
273       # Update list of variable names
274       # :TODO: define excludeBlock variable (similar to exclude) and provide method to extend it
275       if line.startswith("LOGNAME="):
276         return "\n"
277       if "cleandup()" in line:
278         print "WARNING: parseConfigFile.py: skip cleandup and look for '# PRODUCT environment'"
279         while True:
280           line = self.fp.readline()
281           if "# PRODUCT environment" in line:
282             print "WARNING: parseConfigFile.py: '# PRODUCT environment' found"
283             break
284       while "clean " in line[0:6]: #skip clean calls with ending ";" crash
285         line = self.fp.readline()
286       # Extract variable=value
287       if "=" in line:
288         try:
289           variable, value = line.split('=')
290         except: #avoid error for complicated sh line xx=`...=...`, but warning
291           print "WARNING: parseConfigFile.py: line with multiples '=' character are hazardous: '"+line+"'"
292           variable, value = line.split('=',1)
293           pass
294
295         # Self-extending variables that are not in reserved keywords
296         # Example: FOO=something:${FOO}
297         # In this case, remove the ${FOO} in value
298         if variable in value:
299           value = self._purgeValue(value, variable)
300           line = "%s=%s"%(variable,value)
301
302         self.allParsedVariableNames.append(variable)
303       # End of extraction
304
305       if not line:
306         return line
307
308       #
309       # replace "${FOO}" and "$FOO" and ${FOO} and $FOO by %(FOO)s if FOO is
310       # defined in current file (i.e. it is not an external environment variable)
311       for k in self.allParsedVariableNames:
312         key = r'\$\{?'+k+'\}?'
313         pattern = re.compile(key, re.VERBOSE)
314         line = pattern.sub(r'%('+k+')s', line)
315         # Remove quotes (if line does not contain whitespaces)
316         try:
317           variable, value = line.split('=', 1)
318         except ValueError:
319           variable, value = line.split(':', 1)
320         if not ' ' in value.strip():
321           pattern = re.compile(r'\"', re.VERBOSE)
322           line = pattern.sub(r'', line)
323       #
324
325       # Replace `shell_command` by its result
326       def myrep(obj):
327         obj = re.sub('`', r'', obj.group(0)) # remove quotes
328         obj = obj.split()
329         res = subprocess.Popen(obj, stdout=subprocess.PIPE).communicate()[0]
330         res = res.strip(' \t\n\r') # trim whitespaces
331         return res
332       #
333       line = re.sub('`[^`]+`', myrep, line)
334       #
335       if self.outputFile is not None:
336         self.outputFile.write(line+'\n')
337       return line
338
339   def _purgeValue(self, value, name):
340     # Replace foo:${PATTERN}:bar or foo:$PATTERN:bar by foo:bar
341     key = r'\$\{?'+name+'\}?'
342     pattern = re.compile(key, re.VERBOSE)
343     value = pattern.sub(r'', value)
344
345     # trim colons
346     value = _trimColons(value)
347
348     return value
349   #
350
351 # Convert .sh environment file to configuration file format
352 def convertEnvFileToConfigFile(envFilename, configFilename, reserved=[]):
353   logConfigParser.debug('convert env file %s to %s'%(envFilename, configFilename))
354   fileContents = open(envFilename, 'r').read()
355
356   pattern = re.compile('\n[\n]+', re.VERBOSE) # multiple '\n'
357   fileContents = pattern.sub(r'\n', fileContents) # replace by a single '\n'
358
359   finput = StringIO(unicode(fileContents))
360   foutput = open(configFilename, 'w')
361
362   config = MultiOptSafeConfigParser()
363   config.optionxform = str # case sensitive
364   config.readfp(EnvFileConverter(finput, 'SALOME Configuration', reserved, outputFile=foutput))
365
366   foutput.close()
367
368   logConfigParser.info('Configuration file generated: %s'%configFilename)
369 #