Salome HOME
20d782c8cba342b2895a23a615e983beae410bfc
[modules/kernel.git] / bin / parseConfigFile.py
1 # Copyright (C) 2013-2024  CEA, EDF, 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 import collections
27 from salomeContextUtils import SalomeContextException #@UnresolvedImport
28
29 logging.basicConfig()
30 logConfigParser = logging.getLogger(__name__)
31
32 ADD_TO_PREFIX = 'ADD_TO_'
33 UNSET_KEYWORD = 'UNSET'
34 # variables defined in this section are set only if not already defined
35 DEFAULT_VARS_SECTION_NAME = 'SALOME DEFAULT VALUES'
36
37 def _expandSystemVariables(key, val):
38   expandedVal = os.path.expandvars(val) # expand environment variables
39   # Search for not expanded variables (i.e. non-existing environment variables)
40   pattern = re.compile('\${ ( [^}]* ) }', re.VERBOSE) # string enclosed in ${ and }
41   expandedVal = pattern.sub(r'', expandedVal) # remove matching patterns
42
43   if not "DLIM8VAR" in key: # special case: DISTENE licence key can contain double clons (::)
44     expandedVal = _trimColons(expandedVal)
45   return expandedVal
46 #
47
48 # :TRICKY: So ugly solution...
49 class MultiOptSafeConfigParser(configparser.SafeConfigParser):
50   def __init__(self):
51     configparser.SafeConfigParser.__init__(self)
52
53   # copied from python 2.6.8 Lib.ConfigParser.py
54   # modified (see code comments) to handle duplicate keys
55   def _read(self, fp, fpname):
56     """Parse a sectioned setup file.
57
58     The sections in setup file contains a title line at the top,
59     indicated by a name in square brackets (`[]'), plus key/value
60     options lines, indicated by `name: value' format lines.
61     Continuations are represented by an embedded newline then
62     leading whitespace.  Blank lines, lines beginning with a '#',
63     and just about everything else are ignored.
64     """
65     cursect = None                        # None, or a dictionary
66     optname = None
67     lineno = 0
68     e = None                              # None, or an exception
69     while True:
70       line = fp.readline()
71       if not line:
72         break
73       lineno = lineno + 1
74       # comment or blank line?
75       if line.strip() == '' or line[0] in '#;':
76         continue
77       if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
78         # no leading whitespace
79         continue
80       # continuation line?
81       if line[0].isspace() and cursect is not None and optname:
82         value = line.strip()
83         if value:
84           cursect[optname].append(value)
85       # a section header or option header?
86       else:
87         # is it a section header?
88         mo = self.SECTCRE.match(line)
89         if mo:
90           sectname = mo.group('header')
91           if sectname in self._sections:
92             cursect = self._sections[sectname]
93           elif sectname == configparser.DEFAULTSECT:
94             cursect = self._defaults
95           else:
96             cursect = self._dict()
97             cursect['__name__'] = sectname
98             self._sections[sectname] = cursect
99           # So sections can't start with a continuation line
100           optname = None
101         # no section header in the file?
102         elif cursect is None:
103           raise configparser.MissingSectionHeaderError(fpname, lineno, line)
104         # an option line?
105         else:
106           mo = self.OPTCRE.match(line)
107           if mo:
108             optname, vi, optval = mo.group('option', 'vi', 'value')
109             optname = self.optionxform(optname.rstrip())
110             # This check is fine because the OPTCRE cannot
111             # match if it would set optval to None
112             if optval is not None:
113               if vi in ('=', ':') and ';' in optval:
114                 # ';' is a comment delimiter only if it follows
115                 # a spacing character
116                 pos = optval.find(';')
117                 if pos != -1 and optval[pos-1].isspace():
118                   optval = optval[:pos]
119               optval = optval.strip()
120               # ADD THESE LINES
121               splittedComments = optval.split('#')
122               s = _expandSystemVariables(optname, splittedComments[0])
123               optval = s.strip().strip("'").strip('"')
124               #if len(splittedComments) > 1:
125               #  optval += " #" + " ".join(splittedComments[1:])
126               # END OF ADD
127               # allow empty values
128               if optval == '""':
129                 optval = ''
130               # REPLACE following line (original):
131               #cursect[optname] = [optval]
132               # BY THESE LINES:
133               # Check if this optname already exists
134               if (optname in cursect) and (cursect[optname] is not None):
135                 cursect[optname][0] += ','+optval
136               else:
137                 cursect[optname] = [optval]
138               # END OF SUBSTITUTION
139             else:
140               # valueless option handling
141               cursect[optname] = optval
142           else:
143             # a non-fatal parsing error occurred.  set up the
144             # exception but keep going. the exception will be
145             # raised at the end of the file and will contain a
146             # list of all bogus lines
147             if not e:
148               e = configparser.ParsingError(fpname)
149             e.append(lineno, repr(line))
150     # if any parsing errors occurred, raise an exception
151     if e:
152       raise e
153
154     # join the multi-line values collected while reading
155     all_sections = [self._defaults]
156     all_sections.extend(list(self._sections.values()))
157     for options in all_sections:
158       for name, val in list(options.items()):
159         if isinstance(val, list):
160           options[name] = '\n'.join(val)
161   #
162
163
164 # Parse configuration file
165 # Input: filename, and a list of reserved keywords (environment variables)
166 # Output: a list of pairs (variable, value), and a dictionary associating a list of user-defined values to each reserved keywords
167 # Note: Does not support duplicate keys in a same section
168 def parseConfigFile(filename, reserved = None):
169   if reserved is None:
170     reserved = []
171   config = MultiOptSafeConfigParser()
172   config.optionxform = str # case sensitive
173
174   # :TODO: test file existence
175
176   # Read config file
177   try:
178     config.read(filename)
179   except configparser.MissingSectionHeaderError:
180     logConfigParser.error("No section found in file: %s"%(filename))
181     return []
182
183   try:
184     return __processConfigFile(config, reserved, filename)
185   except configparser.InterpolationMissingOptionError as e:
186     msg = "A variable may be undefined in SALOME context file: %s\nParser error is: %s\n"%(filename, e)
187     raise SalomeContextException(msg)
188 #
189
190 def __processConfigFile(config, reserved = None, filename="UNKNOWN FILENAME"):
191   # :TODO: may detect duplicated variables in the same section (raise a warning)
192   #        or even duplicate sections
193
194   if reserved is None:
195     reserved = []
196   unsetVariables = []
197   outputVariables = []
198   defaultValues = []
199   # Get raw items for each section, and make some processing for environment variables management
200   reservedKeys = [ADD_TO_PREFIX+str(x) for x in reserved] # produce [ 'ADD_TO_reserved_1', 'ADD_TO_reserved_2', ..., ADD_TO_reserved_n ]
201   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:[] }
202   sections = config.sections()
203   for section in sections:
204     entries = config.items(section, raw=False) # use interpolation
205     if len(entries) == 0: # empty section
206       logConfigParser.warning("Empty section: %s in file: %s"%(section, filename))
207       pass
208     for key,val in entries:
209       if key in reserved:
210         logConfigParser.error("Invalid use of reserved variable: %s in file: %s"%(key, filename))
211       elif key == UNSET_KEYWORD:
212         unsetVariables += val.replace(',', ' ').split()
213       else:
214         expandedVal = _expandSystemVariables(key, val)
215
216         if key in reservedKeys:
217           shortKey = key[len(ADD_TO_PREFIX):]
218           vals = expandedVal.split(',')
219           reservedValues[shortKey] += vals
220           # remove left&right spaces on each element
221           vals = [v.strip(' \t\n\r') for v in vals]
222         else:
223           if DEFAULT_VARS_SECTION_NAME == section.upper():
224             defaultValues.append((key, expandedVal))
225           else:
226             outputVariables.append((key, expandedVal))
227           pass
228         pass # end if key
229       pass # end for key,val
230     pass # end for section
231
232   # remove duplicate values
233   outVars = []
234   for (var, values) in outputVariables:
235     vals = values.split(',')
236     vals = list(set(vals))
237     outVars.append((var, ','.join(vals)))
238
239   ConfigInfo = collections.namedtuple("ConfigInfo",
240                                       ["unsetVariables",
241                                        "outputVariables",
242                                        "reservedValues",
243                                        "defaultValues"])
244   return ConfigInfo(unsetVariables, outVars, reservedValues, defaultValues)
245 #
246
247 def _trimColons(var):
248   v = var
249   # Remove leading and trailing colons (:)
250   pattern = re.compile('^:+ | :+$', re.VERBOSE)
251   v = pattern.sub(r'', v) # remove matching patterns
252   # Remove multiple colons
253   pattern = re.compile('::+', re.VERBOSE)
254   v = pattern.sub(r':', v) # remove matching patterns
255   return v
256 #