From 1dd5e731c14cb570bbe772f466e8113e653b70ca Mon Sep 17 00:00:00 2001 From: Francois Dang Ngoc Date: Sat, 5 May 2018 00:39:51 -0400 Subject: [PATCH] Fixed escaped quotes inside quoted and unquoted string. This should address #96 and #138 --- pyhocon/config_parser.py | 6 +++--- tests/test_config_parser.py | 28 ++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py index 2bbeb71c..50f35bba 100644 --- a/pyhocon/config_parser.py +++ b/pyhocon/config_parser.py @@ -175,7 +175,7 @@ def create_substitution(instring, loc, token): return substitution # ${path} or ${?path} for optional substitution - STRING_PATTERN = '(")(?P[^"]*)\\1(?P[ \t]*)' + STRING_PATTERN = '"(?P(?:[^"\\\\]|\\\\.)*)"(?P[ \t]*)' def create_quoted_string(instring, loc, token): # remove the ${ and } @@ -240,13 +240,13 @@ def include_config(instring, loc, token): # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969 multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string) # single quoted line string - quoted_string = Regex('".*?"[ \t]*', re.UNICODE).setParseAction(create_quoted_string) + quoted_string = Regex('"(?:[^"\\\\\n]|\\\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string) # unquoted string that takes the rest of the line until an optional comment # we support .properties multiline support which is like this: # line1 \ # line2 \ # so a backslash precedes the \n - unquoted_string = Regex('(?:\\\\|[^\[\{\s\]\}#,=\$])+[ \t]*', re.UNICODE).setParseAction(unescape_string) + unquoted_string = Regex('(?:[^"\[\{\s\]\}#,=\$\\\\]|\\\\.)+[ \t]*', re.UNICODE).setParseAction(unescape_string) substitution_expr = Regex('[ \t]*\$\{[^\}]+\}[ \t]*').setParseAction(create_substitution) string_expr = multiline_string | quoted_string | unquoted_string diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index e57b3fa7..9419ad97 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -1132,14 +1132,15 @@ def test_include_required_file(self): } """ ) - assert { + expected = { 'a': { 'garfield': { 'say': 'meow' }, 't': 2 } - } == config + } + assert expected == config def test_include_missing_required_file(self): with pytest.raises(IOError): @@ -2040,9 +2041,10 @@ def test_pop(self): config_tree = ConfigFactory.parse_string('a:{b: 3, d: 6}') assert 3 == config_tree.pop('a.b', 5) assert 5 == config_tree.pop('a.c', 5) - assert { + expected = { 'a': {'d': 6} - } == config_tree + } + assert expected == config_tree def test_merge_overriden(self): # Adress issue #110 @@ -2065,3 +2067,21 @@ def test_attr_syntax(self): } """) assert 5 == config.b.pb + + def test_escape_quote(self): + config = ConfigFactory.parse_string( + """ + quoted: "abc\\"test" + unquoted: abc\\"test + """) + assert 'abc"test' == config['quoted'] + assert 'abc"test' == config['unquoted'] + + def test_escape_quote_complex(self): + config = ConfigFactory.parse_string( + """ + value: "{\\"critical\\":\\"0.00\\",\\"warning\\":\\"99.99\\"}" + """ + ) + + assert '{"critical":"0.00","warning":"99.99"}' == config['value']