import io
import re
import six
from string import Template
import json
# Macro parsing states
PRE_NAME = 0
IN_NAME = 1
PRE_VAL = 2
IN_VAL = 3
[docs]def substitute_in_file(file_path, macros):
"""
Substitute the macros given by ${name} at the given file with the entries on the `macros` dictionary.
Parameters
----------
file_path : str
The path to the file in which to substitute
macros : dict
Dictionary containing macro name as key and value as what will be substituted.
Returns
-------
file : io.StringIO
File-like object with the proper substitutions.
"""
template = template_for_file(file_path)
return replace_macros_in_template(template, macros)
def replace_macros_in_template(template, macros):
curr_template = template
prev_template = Template("")
expanded_text = ""
# Escape any single or double quotes to ensure macro substitution results in valid python code
# when replaced (e.g. xterm -e 'echo hi')
macros = {
key: re.sub(r'(?<!\\)"', '\\"', re.sub(r"(?<!\\)'", "\\'", value)) if isinstance(value, str) else value
for key, value in macros.items()
}
for i in range(100):
expanded_text = curr_template.safe_substitute(macros)
if curr_template.template == prev_template.template:
break
prev_template = curr_template
curr_template = Template(expanded_text)
return io.StringIO(six.text_type(expanded_text))
def template_for_file(file_path):
with open(file_path) as orig_file:
text = Template(orig_file.read())
return text
[docs]def parse_macro_string(macro_string):
"""Parses a macro string and returns a dictionary.
First, this method attempts to parse the string as JSON.
If that fails, it attempts to parse it as an EPICS-style
macro string. The parsing algorithm for that case is very
closely based on macParseDefns in libCom/macUtil.c"""
if not macro_string:
return {}
macro_string = str(macro_string)
try:
macros = json.loads(macro_string)
return macros
except ValueError:
if "=" not in macro_string:
raise ValueError("Could not parse macro argument as JSON.")
macros = {}
state = PRE_NAME
quote = None
name_start = None
name_end = None
val_start = None
val_end = None
for i, c in enumerate(macro_string):
if quote:
if c == quote:
quote = False
elif c == "'" or c == '"':
quote = c
continue
escape = macro_string[i - 1] == "\\"
if state == PRE_NAME:
if (not quote) and (not escape) and (c.isspace() or c == ","):
continue
name_start = i
state = IN_NAME
elif state == IN_NAME:
if quote or escape:
continue
if c == "=" or c == ",":
name_end = i
state = PRE_VAL
elif state == PRE_VAL:
if (not quote) and (not escape) and c.isspace():
continue
val_start = i
state = IN_VAL
if i == len(macro_string) - 1:
val_end = i + 1
elif state == IN_VAL:
if quote or escape:
continue
if c == ",":
val_end = i
state = PRE_NAME
elif i == len(macro_string) - 1:
val_end = i + 1
state = PRE_NAME
else:
continue
if None not in (name_start, name_end, val_start, val_end):
key = macro_string[name_start:name_end].strip().replace("\\", "")
val = macro_string[val_start:val_end].strip("\"'").replace("\\", "")
macros[key] = val
name_start = None
name_end = None
val_start = None
val_end = None
state = PRE_NAME
return macros