Skip to content

Commit d33c02a

Browse files
committed
Fix quote_str when string ends in double quotes
When a string will be quoted with triple double quotes it must not end in an unescaped double quote or the value will fail to parse as 4 double quotes are not recognized as the end-token for the string. Handle that case by escaping the trailing double quote.
1 parent a8c0cad commit d33c02a

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

easybuild/tools/utilities.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def quote_str(val, escape_newline=False, prefer_single_quotes=False, escape_back
7676
val = val.replace('\\', '\\\\')
7777
# forced triple double quotes
7878
if ("'" in val and '"' in val) or (escape_newline and '\n' in val):
79+
# Triple double quoted string cannot end in double quote.
80+
# So escape it if we also escape backslashes
81+
# and hence can assume escaping works.
82+
if val.endswith('"') and escape_backslash:
83+
val = val[:-1] + '\\"'
7984
return '"""%s"""' % val
8085
# escape double quote(s) used in strings
8186
elif '"' in val:

test/framework/easyconfig.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,11 +2036,56 @@ def test_quote_str(self):
20362036
def test_quote_py_str(self):
20372037
"""Test quote_py_str function."""
20382038

2039-
res = quote_py_str('description = """Example of\n multi-line\n description with \' quotes"""')
2040-
self.assertEqual(res, '"""description = """Example of\n multi-line\n description with \' quotes""""""')
2039+
def eval_quoted_string(quoted_val, val):
2040+
"""
2041+
Helper function to sanity check we can use the quoted string in Python contexts.
2042+
Returns the evaluated (i.e. unquoted) string
2043+
"""
2044+
globals = dict()
2045+
try:
2046+
exec('res = %s' % quoted_val, globals)
2047+
except Exception as e: # pylint: disable=broad-except
2048+
self.fail('Failed to evaluate %s (from %s): %s' % (quoted_val, val, e))
2049+
return globals['res']
20412050

2042-
res = quote_py_str('preconfigopts = "sed -i \'s/`which \\([a-z_]*\\)`/\\1/g;s/`//g\' foo.c && "')
2043-
self.assertEqual(res, '"""preconfigopts = "sed -i \'s/`which \\\\([a-z_]*\\\\)`/\\\\1/g;s/`//g\' foo.c && """"')
2051+
def assertEqual_unquoted(quoted_val, val):
2052+
"""Assert that evaluating the quoted_val yields the val"""
2053+
self.assertEqual(eval_quoted_string(quoted_val, val), val)
2054+
2055+
def subtest_quote_py_str(val):
2056+
"""Quote `val`, check that it roundtrips and return the quoted value"""
2057+
quoted_val = quote_py_str(val)
2058+
assertEqual_unquoted(quoted_val, val)
2059+
return quoted_val
2060+
2061+
res = subtest_quote_py_str('Simple')
2062+
self.assertEqual(res, "'Simple'")
2063+
2064+
res = subtest_quote_py_str('double "quote"')
2065+
self.assertEqual(res, "'double \"quote\"'")
2066+
2067+
res = subtest_quote_py_str("single 'quote'")
2068+
self.assertEqual(res, '"single \'quote\'"')
2069+
2070+
res = subtest_quote_py_str("\"Both \"quotes'")
2071+
self.assertEqual(res, '""""Both "quotes\'"""')
2072+
2073+
# Some more complex examples based on real-world values of EasyConfig parameters
2074+
2075+
res = subtest_quote_py_str('Example of\n multi-line\n description with \' "quotes')
2076+
self.assertEqual(res, '"""Example of\n multi-line\n description with \' "quotes"""')
2077+
2078+
res = subtest_quote_py_str('sed -i \'s/`which \\([a-z_]*\\)`/\\1/g;s/`//g\' "foo.c" && ')
2079+
self.assertEqual(res, '"""sed -i \'s/`which \\\\([a-z_]*\\\\)`/\\\\1/g;s/`//g\' "foo.c" && """')
2080+
2081+
res = subtest_quote_py_str('echo \'key=val\' >> "$TMP/db.conf"')
2082+
self.assertEqual(res, '"""echo \'key=val\' >> "$TMP/db.conf\\""""')
2083+
2084+
res = subtest_quote_py_str('echo \'empty double quotes\' && echo ""')
2085+
self.assertEqual(res, '"""echo \'empty double quotes\' && echo "\\""""')
2086+
2087+
res = subtest_quote_py_str('echo -e "key=val\nkey2=val2" >> "$TMP/db.conf"')
2088+
self.assertEqual(res, '"""echo -e "key=val\nkey2=val2" >> "$TMP/db.conf\\""""')
20442089

20452090
def test_dump(self):
20462091
"""Test EasyConfig's dump() method."""

0 commit comments

Comments
 (0)