summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorToshio Kuratomi <toshio@fedoraproject.org>2015-08-31 12:47:36 -0700
committerToshio Kuratomi <toshio@fedoraproject.org>2015-08-31 13:17:26 -0700
commit7f5080f64ab4a82648cb746990587c1aaff3f61d (patch)
treea74fa75730f7eb2ec36ce7d7ff8f1066800a8cc0
parent2b82b632cde9931cfd9e62393cbd7d735584fcce (diff)
downloadansible-7f5080f64ab4a82648cb746990587c1aaff3f61d.tar.gz
Fix backslash escaping inside of jinja2 expressions
Fixes #11891
-rw-r--r--lib/ansible/template/__init__.py44
-rw-r--r--test/units/template/test_jinja_backslash.py85
2 files changed, 129 insertions, 0 deletions
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index 1793a2f43e..77ea365b09 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -48,6 +48,48 @@ NON_TEMPLATED_TYPES = ( bool, Number )
JINJA2_OVERRIDE = '#jinja2:'
+def _preserve_backslashes(data, jinja_env):
+ """Double backslashes within jinja2 expressions
+
+ A user may enter something like this in a playbook::
+
+ debug:
+ msg: "Test Case 1\\3; {{ test1_name | regex_replace('^(.*)_name$', '\\1')}}"
+
+ The string inside of the {{ gets interpreted multiple times First by yaml.
+ Then by python. And finally by jinja2 as part of it's variable. Because
+ it is processed by both python and jinja2, the backslash escaped
+ characters get unescaped twice. This means that we'd normally have to use
+ four backslashes to escape that. This is painful for playbook authors as
+ they have to remember different rules for inside vs outside of a jinja2
+ expression (The backslashes outside of the "{{ }}" only get processed by
+ yaml and python. So they only need to be escaped once). The following
+ code fixes this by automatically performing the extra quoting of
+ backslashes inside of a jinja2 expression.
+
+ """
+ if '\\' in data and '{{' in data:
+ new_data = []
+ d2 = jinja_env.preprocess(data)
+ in_var = False
+
+ for token in jinja_env.lex(d2):
+ if token[1] == 'variable_begin':
+ in_var = True
+ new_data.append(token[2])
+ elif token[1] == 'variable_end':
+ in_var = False
+ new_data.append(token[2])
+ elif in_var and token[1] == 'string':
+ # Double backslashes only if we're inside of a jinja2 variable
+ new_data.append(token[2].replace('\\','\\\\'))
+ else:
+ new_data.append(token[2])
+
+ data = ''.join(new_data)
+
+ return data
+
class Templar:
'''
The main class for templating, with the main entry-point of template().
@@ -296,6 +338,8 @@ class Templar:
myenv.filters.update(self._get_filters())
myenv.tests.update(self._get_tests())
+ data = _preserve_backslashes(data, myenv)
+
try:
t = myenv.from_string(data)
except TemplateSyntaxError as e:
diff --git a/test/units/template/test_jinja_backslash.py b/test/units/template/test_jinja_backslash.py
new file mode 100644
index 0000000000..58e4d0f47b
--- /dev/null
+++ b/test/units/template/test_jinja_backslash.py
@@ -0,0 +1,85 @@
+# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import jinja2
+from ansible.compat.tests import unittest
+
+from ansible.template import _preserve_backslashes
+
+
+class TestBackslashEscape(unittest.TestCase):
+
+ test_data = (
+ # Test backslashes in a filter arg are double escaped
+ dict(
+ template="{{ 'test2 %s' | format('\\1') }}",
+ intermediate="{{ 'test2 %s' | format('\\\\1') }}",
+ expectation="test2 \\1",
+ args=dict()
+ ),
+ # Test backslashes inside the jinja2 var itself are double
+ # escaped
+ dict(
+ template="Test 2\\3: {{ '\\1 %s' | format('\\2') }}",
+ intermediate="Test 2\\3: {{ '\\\\1 %s' | format('\\\\2') }}",
+ expectation="Test 2\\3: \\1 \\2",
+ args=dict()
+ ),
+ # Test backslashes outside of the jinja2 var are not double
+ # escaped
+ dict(
+ template="Test 2\\3: {{ 'test2 %s' | format('\\1') }}; \\done",
+ intermediate="Test 2\\3: {{ 'test2 %s' | format('\\\\1') }}; \\done",
+ expectation="Test 2\\3: test2 \\1; \\done",
+ args=dict()
+ ),
+ # Test backslashes in a variable sent to a filter are handled
+ dict(
+ template="{{ 'test2 %s' | format(var1) }}",
+ #intermediate="{{ 'test2 %s' | format('\\\\1') }}",
+ intermediate="{{ 'test2 %s' | format(var1) }}",
+ expectation="test2 \\1",
+ args=dict(var1='\\1')
+ ),
+ # Test backslashes in a variable expanded by jinja2 are double
+ # escaped
+ dict(
+ template="Test 2\\3: {{ var1 | format('\\2') }}",
+ intermediate="Test 2\\3: {{ var1 | format('\\\\2') }}",
+ expectation="Test 2\\3: \\1 \\2",
+ args=dict(var1='\\1 %s')
+ ),
+ )
+ def setUp(self):
+ self.env = jinja2.Environment()
+
+ def tearDown(self):
+ pass
+
+ def test_backslash_escaping(self):
+
+ for test in self.test_data:
+ intermediate = _preserve_backslashes(test['template'], self.env)
+ self.assertEquals(intermediate, test['intermediate'])
+ template = jinja2.Template(intermediate)
+ args = test['args']
+ self.assertEquals(template.render(**args), test['expectation'])
+