treat the escape char normally when not next to reference open
diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py
index 77c45e3..c76b721 100644
--- a/reclass/datatypes/tests/test_parameters.py
+++ b/reclass/datatypes/tests/test_parameters.py
@@ -7,7 +7,7 @@
# Released under the terms of the Artistic Licence 2.0
#
from reclass.datatypes import Parameters
-from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS
+from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, ESCAPE_CHARACTER
from reclass.errors import InfiniteRecursionError
from reclass.utils.mergeoptions import MergeOptions
import unittest
@@ -340,5 +340,52 @@
p1.interpolate()
self.assertEqual(p1.as_dict(), r)
+ def test_interpolate_escaping(self):
+ v = 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS)
+ d = {'foo': ESCAPE_CHARACTER + 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ 'bar': 'unused'}
+ p = Parameters(d)
+ p.render_simple()
+ self.assertEqual(p.as_dict()['foo'], v)
+
+ def test_interpolate_double_escaping(self):
+ v = ESCAPE_CHARACTER + 'meep'
+ d = {'foo': ESCAPE_CHARACTER + ESCAPE_CHARACTER + 'bar'.join(PARAMETER_INTERPOLATION_SENTINELS),
+ 'bar': 'meep'}
+ p = Parameters(d)
+ p.interpolate()
+ self.assertEqual(p.as_dict()['foo'], v)
+
+ def test_interpolate_escaping_backwards_compatibility(self):
+ """In all following cases, escaping should not happen and the escape character
+ needs to be printed as-is, to ensure backwards compatibility to older versions."""
+ v = ' '.join([
+ # Escape character followed by unescapable character
+ '1', ESCAPE_CHARACTER,
+ # Escape character followed by escape character
+ '2', ESCAPE_CHARACTER + ESCAPE_CHARACTER,
+ # Escape character followed by interpolation end sentinel
+ '3', ESCAPE_CHARACTER + PARAMETER_INTERPOLATION_SENTINELS[1],
+ # Escape character at the end of the string
+ '4', ESCAPE_CHARACTER
+ ])
+ d = {'foo': v}
+ p = Parameters(d)
+ p.render_simple()
+ self.assertEqual(p.as_dict()['foo'], v)
+
+ def test_escape_close_in_ref(self):
+ p1 = Parameters({'one}': 1, 'two': '${one\\}}'})
+ r = {'one}': 1, 'two': 1}
+ p1.interpolate()
+ self.assertEqual(p1.as_dict(), r)
+
+ def test_double_escape_in_ref(self):
+ d = {'one\\': 1, 'two': '${one\\\\}'}
+ p1 = Parameters(d)
+ r = {'one\\': 1, 'two': 1}
+ p1.interpolate()
+ self.assertEqual(p1.as_dict(), r)
+
if __name__ == '__main__':
unittest.main()
diff --git a/reclass/defaults.py b/reclass/defaults.py
index 13b8496..641aace 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -28,6 +28,7 @@
PARAMETER_INTERPOLATION_SENTINELS = ('${', '}')
PARAMETER_INTERPOLATION_DELIMITER = ':'
PARAMETER_DICT_KEY_OVERRIDE_PREFIX = '~'
+ESCAPE_CHARACTER = '\\'
MERGE_ALLOW_SCALAR_OVER_DICT = False
MERGE_ALLOW_SCALAR_OVER_LIST = False
diff --git a/reclass/errors.py b/reclass/errors.py
index 5ce4d73..d6936d3 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -211,3 +211,17 @@
"definition in '{3}'. Nodes can only be defined once " \
"per inventory."
return msg.format(self._storage, self._name, self._uris[1], self._uris[0])
+
+
+class ParseError(ReclassException):
+
+ def __init__(self, msg, line, col, lineno, rc=posix.EX_DATAERR):
+ super(ParseError, self).__init__(rc=rc, msg=None)
+ self._err = msg
+ self._line = line
+ self._col = col
+ self._lineno = lineno
+
+ def _get_message(self):
+ msg = "Parse error: {0} : {1} at char {2}"
+ return msg.format(self._line, self._err, self._col - 1)
diff --git a/reclass/utils/tests/test_value.py b/reclass/utils/tests/test_value.py
index 39cf062..5e11ef4 100644
--- a/reclass/utils/tests/test_value.py
+++ b/reclass/utils/tests/test_value.py
@@ -13,7 +13,7 @@
from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS, \
PARAMETER_INTERPOLATION_DELIMITER
from reclass.errors import UndefinedVariableError, \
- IncompleteInterpolationError
+ IncompleteInterpolationError, ParseError
import unittest
def _var(s):
@@ -122,7 +122,7 @@
def test_incomplete_variable(self):
s = PARAMETER_INTERPOLATION_SENTINELS[0] + 'incomplete'
- with self.assertRaises(pp.ParseException):
+ with self.assertRaises(ParseError):
tv = Value(s)
if __name__ == '__main__':
diff --git a/reclass/utils/value.py b/reclass/utils/value.py
index b934b1e..7af16c1 100644
--- a/reclass/utils/value.py
+++ b/reclass/utils/value.py
@@ -12,10 +12,23 @@
from reclass.utils.listitem import ListItem
from reclass.utils.refitem import RefItem
from reclass.utils.scaitem import ScaItem
-from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER, PARAMETER_INTERPOLATION_SENTINELS
+from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER, PARAMETER_INTERPOLATION_SENTINELS, ESCAPE_CHARACTER
+from reclass.errors import *
+
_STR = 'STR'
_REF = 'REF'
+_OPEN = PARAMETER_INTERPOLATION_SENTINELS[0]
+_CLOSE = PARAMETER_INTERPOLATION_SENTINELS[1]
+_CLOSE_FIRST = _CLOSE[0]
+_ESCAPE = ESCAPE_CHARACTER
+_DOUBLE_ESCAPE = _ESCAPE + _ESCAPE
+_ESCAPE_OPEN = _ESCAPE + _OPEN
+_ESCAPE_CLOSE = _ESCAPE + _CLOSE
+_DOUBLE_ESCAPE_OPEN = _DOUBLE_ESCAPE + _OPEN
+_DOUBLE_ESCAPE_CLOSE = _DOUBLE_ESCAPE + _CLOSE
+_EXCLUDES = _ESCAPE + _OPEN + _CLOSE
+
class Value(object):
@@ -29,23 +42,25 @@
token = list(tokens[0])
tokens[0] = (_REF, token)
- string = (pp.Literal('\\\\').setParseAction(pp.replaceWith('\\')) |
- pp.Literal('\\$').setParseAction(pp.replaceWith('$')) |
- pp.White() |
- pp.Word(pp.printables, excludeChars='\\$')).setParseAction(_string)
+ ref_open = pp.Literal(_OPEN).suppress()
+ ref_close = pp.Literal(_CLOSE).suppress()
+ not_open = ~pp.Literal(_OPEN) + ~pp.Literal(_ESCAPE_OPEN) + ~pp.Literal(_DOUBLE_ESCAPE_OPEN)
+ not_close = ~pp.Literal(_CLOSE) + ~pp.Literal(_ESCAPE_CLOSE) + ~pp.Literal(_DOUBLE_ESCAPE_CLOSE)
+ escape_open = pp.Literal(_ESCAPE_OPEN).setParseAction(pp.replaceWith(_OPEN))
+ escape_close = pp.Literal(_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_CLOSE))
+ double_escape = pp.Combine(pp.Literal(_DOUBLE_ESCAPE) + pp.MatchFirst([pp.FollowedBy(_OPEN), pp.FollowedBy(_CLOSE)])).setParseAction(pp.replaceWith(_ESCAPE))
+ text = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXCLUDES), pp.CharsNotIn('', exact=1)])
+ text_ref = pp.MatchFirst([pp.Word(pp.printables, excludeChars=_EXCLUDES), pp.CharsNotIn(_CLOSE_FIRST, exact=1)])
+ white_space = pp.White()
- refString = (pp.Literal('\\\\').setParseAction(pp.replaceWith('\\')) |
- pp.Literal('\\$').setParseAction(pp.replaceWith('$')) |
- pp.Literal('\\{').setParseAction(pp.replaceWith('{')) |
- pp.Literal('\\}').setParseAction(pp.replaceWith('}')) |
- pp.White() |
- pp.Word(pp.printables, excludeChars='\\${}')).setParseAction(_string)
+ content = pp.Combine(pp.OneOrMore(not_open + text))
+ ref_content = pp.Combine(pp.OneOrMore(not_open + not_close + text_ref))
+ string = pp.MatchFirst([double_escape, escape_open, content, white_space]).setParseAction(_string)
+ refString = pp.MatchFirst([double_escape, escape_open, escape_close, ref_content, white_space]).setParseAction(_string)
refItem = pp.Forward()
refItems = pp.OneOrMore(refItem)
- reference = (pp.Literal(PARAMETER_INTERPOLATION_SENTINELS[0]).suppress() +
- pp.Group(refItems) +
- pp.Literal(PARAMETER_INTERPOLATION_SENTINELS[1]).suppress()).setParseAction(_reference)
+ reference = (ref_open + pp.Group(refItems) + ref_close).setParseAction(_reference)
refItem << (reference | refString)
item = reference | string
@@ -60,7 +75,10 @@
self._allRefs = False
self._container = False
if isinstance(val, str):
- tokens = Value._parser.leaveWhitespace().parseString(val).asList()
+ try:
+ tokens = Value._parser.leaveWhitespace().parseString(val).asList()
+ except pp.ParseException as e:
+ raise ParseError(e.msg, e.line, e.col, e.lineno)
items = self._createItems(tokens)
if len(items) is 1:
self._item = items[0]