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