Fix backslash escaping inside of jinja2 expressions

Fixes #11891
This commit is contained in:
Toshio Kuratomi 2015-08-31 12:47:36 -07:00
parent 2b82b632cd
commit 7f5080f64a
2 changed files with 129 additions and 0 deletions

View file

@ -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:

View file

@ -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'])