'''
support for macro substitutions
From the EPICS Application Developer's Guide
:see: http://www.aps.anl.gov/epics/base/R3-14/12-docs/AppDevGuide/node7.html
6.3.2 Unquoted Strings
In the summary section, some values are shown as quoted strings and some unquoted.
The actual rule is that any string consisting of only the following characters
does not have to be quoted unless it contains one of the above keywords:
::
a-z A-Z 0-9 _ -- : . [ ] < > ;
::
my regexp: [\w_\-:.[\]<>;]+([.][A-Z0-9]+)?
These are also the legal characters for process variable names.
Thus in many cases quotes are not needed.
6.3.3 Quoted Strings
A quoted string can contain any ascii character except the quote character ".
The quote character itself can given by using \ as an escape.
For example "\"" is a quoted string containing the single character ".
6.3.4 Macro Substitution
Macro substitutions are permitted inside quoted strings.
Macro instances take the form:
::
$(name)
or
::
${name}
There is no distinction between the use of parentheses or braces
for delimiters, although the two must match for a given macro instance.
The macro name can be made up from other macros, for example:
::
$(name_$(sel))
A macro instance can also provide a default value that is used when no
macro with the given name is defined. The default value can be defined
in terms of other macros if desired, but cannot contain any unescaped
comma characters. The syntax for specifying a default value is as follows:
::
$(name=default)
Finally macro instances can also contain definitions of other macros,
which can (temporarily) override any existing values for those macros
but are in scope only for the duration of the expansion of this macro
instance. These definitions consist of name=value sequences separated
by commas, for example:
::
$(abcd=$(a)$(b)$(c)$(d),a=A,b=B,c=C,d=D)
'''
import re
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# regular expression catalog
# a-z A-Z 0-9 _ -- : . [ ] < > ;
EPICS_UNQUOTED_STRING_RE = r'[\d\w_:;.<>\-\[\],]+'
EPICS_MACRO_SPECIFICATION_P_RE = "\$\("+EPICS_UNQUOTED_STRING_RE+"\)" # _P_: parentheses
EPICS_MACRO_SPECIFICATION_B_RE = "\$\{"+EPICS_UNQUOTED_STRING_RE+"\}" # _B_: braces
# EPICS_MACRO_DEFAULT_RE cannot find $(P=$(S)), that's OK.
# If the inner $(S) was expanded, it would be found then, else macro expansion fails anyway
EPICS_MACRO_DEFAULT_RE = EPICS_UNQUOTED_STRING_RE+'='+EPICS_UNQUOTED_STRING_RE
EPICS_MACRO_SPECIFICATION_PD_RE = "\$\("+EPICS_MACRO_DEFAULT_RE+"\)"
EPICS_MACRO_SPECIFICATION_BD_RE = "\$\{"+EPICS_MACRO_DEFAULT_RE+"\}"
EPICS_UNQUOTED_STRING_PATTERN = re.compile(EPICS_UNQUOTED_STRING_RE, 0)
EPICS_MACRO_SPECIFICATION_P_PATTERN = re.compile(EPICS_MACRO_SPECIFICATION_P_RE, 0)
EPICS_MACRO_SPECIFICATION_B_PATTERN = re.compile(EPICS_MACRO_SPECIFICATION_B_RE, 0)
EPICS_MACRO_DEFAULT_PATTERN = re.compile(EPICS_MACRO_DEFAULT_RE, 0)
EPICS_MACRO_SPECIFICATION_PD_PATTERN = re.compile(EPICS_MACRO_SPECIFICATION_PD_RE, 0)
EPICS_MACRO_SPECIFICATION_BD_PATTERN = re.compile(EPICS_MACRO_SPECIFICATION_BD_RE, 0)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[docs]class Macros(object):
'''manage a set of macros (keys, substitutions)'''
def __init__(self, **env):
self.db = {}
self.setMany(**env)
def __str__(self):
#return ', '.join([k+'="'+str(v.value)+'"' for k, v in sorted(self.items())])
return ', '.join([k+'="'+str(v)+'"' for k, v in sorted(self.items())])
def __len__(self):
'''how many items?'''
return len(self.db)
[docs] def exists(self, key):
'''is there such a *key*?'''
return key in self.db
[docs] def get(self, key, missing=None):
'''find the *key* macro, if not found, return *missing*'''
return self.db.get(key, missing)
[docs] def set(self, key, value, parent=None, ref=None):
'''define the *key* macro'''
self.db[key] = KVpair(parent, key, value, ref)
[docs] def setMany(self, **env):
'''define several macros'''
self.db = dict(self.db.items() + env.items())
[docs] def keys(self):
'''get the list of macros, like dictionary.keys()'''
return self.db.keys()
[docs] def items(self):
'''get the full database, like dictionary.items()'''
return self.db.items()
[docs] def replace(self, text):
'''Replace macro parameters in source string'''
return _replace_(text, self.db)
[docs]class KVpair(object):
'''
any *single* defined key:value pair in an EPICS IOC command file
* PV field
* Record field
* Macro
* Symbol
'''
def __init__(self, parent, key, value, ref=None):
self.parent = parent
self.key = key
self.value = value
self.reference = ref
def __str__(self):
return self.key + ' = ' + str(self.value)
def _replace_(source, macro_dict):
'''
Replace macro parameters in source string.
Search through the dictionary of macros since there
may not be enough macros defined for all the
substitution patterns given.
:param source: string with possible macro replacements
:param macro_dict: dictionary of macro substitutions
:return: string with substitutions applied
:raise Exception: incorrect number of regular expression matches found
'''
if isinstance(source, KVpair):
source = source.value # TODO: is this ALWAYS true?
last = ''
while last != source: # repeat to expand nested macros
last = source
# substitute the simple macro replacements
for subst_marker in identifyEpicsMacros(source):
parts = re.findall(EPICS_UNQUOTED_STRING_PATTERN, subst_marker, 0)
if len(parts) == 1 and parts[0].find(','):
parts = parts[0].split(',')
if len(parts) == 1:
# substitute the simple macros
if parts[0] in macro_dict:
replacement_text = macro_dict[parts[0]]
if isinstance(replacement_text, KVpair):
replacement_text = replacement_text.value
source = source.replace(subst_marker, replacement_text)
elif len(parts) == 2:
# substitute the macros with default expressions
macro_variable, default_substitution = parts
if macro_variable in macro_dict:
replacement_text = macro_dict[macro_variable]
if isinstance(replacement_text, KVpair):
replacement_text = replacement_text.value
replacement_text = macro_dict[macro_variable].value
else:
replacement_text = default_substitution
source = source.replace(subst_marker, replacement_text)
else:
# add more diagnostics if this happens
raise Exception, "should only match 1 or 2 parts here"
return source
[docs]def identifyEpicsMacros(source):
'''
Identify any EPICS macro substitutions in the source string.
Multiple entries of the same substitution (redundancies)
are ignored. Does not include nested macros such as:
::
$(P=$(S))${S_$(P)}
$(PJ=$(P))${S_$(P)}
For these, only the innermost are returned:
::
['$(S)', '$(P)']
['$(P)']
:note: This routine will also properly identify command shell macro substitutions.
:param source: string with possible (EPICS) macro substitution expressions
:return: list of macro substitutions found
'''
parts = []
for patt in (EPICS_MACRO_SPECIFICATION_P_PATTERN,
EPICS_MACRO_SPECIFICATION_B_PATTERN):
for subst_marker in re.findall(patt, source, 0):
if subst_marker not in parts:
parts.append( subst_marker )
for patt in (EPICS_MACRO_SPECIFICATION_PD_PATTERN,
EPICS_MACRO_SPECIFICATION_BD_PATTERN):
for subst_marker in re.findall(patt, source, 0):
parts.append( subst_marker )
return parts