From 7f5080f64ab4a82648cb746990587c1aaff3f61d Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 31 Aug 2015 12:47:36 -0700 Subject: [PATCH] Fix backslash escaping inside of jinja2 expressions Fixes #11891 --- lib/ansible/template/__init__.py | 44 +++++++++++ test/units/template/test_jinja_backslash.py | 85 +++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 test/units/template/test_jinja_backslash.py 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 +# +# 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 . + +# 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']) +