Jinja is a powerful template engine for Python. Inside Ansible it is often used to dynamically create files from templates and fill in placeholders with data.

The Jinja language (now in version 2) also offers features like loops and data manipulation. Building files from loops and data-structures is more complex, especially when considering whitespace and line breaks. It can get tricky very fast.

The general approach (and I think many will agree with me on this one) is to just write e.g. a Jinja loop, see what the rendered outcome is, adjust and tweak it a little bit and make it work. This could be described as an trial-and-error approach.

The whole topic of whitespace control in Jinja within Ansible seems to be manageable with about 50% know-how. The last 50% can be completed by fiddling around.

I have done this myself that way for years. Though you are never an expert, you will get the job done and reach your goal. This article is trying to fill in the missing 50% and enable you (and me) to handle white-spaces and line-breaks effortless.

Each segment has a section TL;DR which gives you a brief summary of what’s happening - in case you return to this article and you just quickly need the information.

We will go through a couple of scenarios in detail to understand what actually is going on. In order to give some examples, let’s agree first on common terms:

Syntax Element Code Example Term
for/while/if {% for ... %} Tag (within a block).
{% foobar %}{% endfoobar %} {% for i in x %}...{% endfor %} Block.
{{ foobar }} {{ i }} Variable (replacement).

All examples will use the following Ansible playbook and template file as starting point:

Code example...
$ tree /tmp/ansible/
/tmp/ansible/
├── playbook.yml
└── template.js

$ cat /tmp/ansible/playbook.yml
---
- hosts: all
  gather_facts: false
  become: false
  connection: local
  vars:
    i: foobar
  tasks:
    - name: Deploy a testfile.
      ansible.builtin.template:
        src: /tmp/ansible/template.j2
        dest: /tmp/ansible/outputfile

$ cat /tmp/ansible/template.j2
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
{% if true %}
  {{ i }}
{% endif %}
-----

The Ansible playbook (playbook.yml) and Jinja template file will, when run with Ansible, create a file /tmp/ansible/outputfile with rendered content.

Code example...
$ cd /tmp/ansible
$ ansible-playbook -i localhost, playbook.yml
Executing playbook playbook.yml

- all on hosts: all -
Deploy a testfile...
wednesday 28 june 2023  22:24:22 +0200 (0:00:00.046)       0:00:00.046 ********
wednesday 28 june 2023  22:24:22 +0200 (0:00:00.044)       0:00:00.044 ********
  localhost done

- Play recap -
  localhost          : ok=1    changed=1    unreachable=0    failed=0    rescued=0    ignored=0
Playbook run took 0 days, 0 hours, 0 minutes, 2 seconds
wednesday 28 june 2023  22:24:24 +0200 (0:00:02.078)       0:00:02.125 ********
===============================================================================
Deploy a testfile ---------------------------------------------------------2.08s
wednesday 28 june 2023  22:24:24 +0200 (0:00:02.078)       0:00:02.123 ********
===============================================================================
ansible.builtin.template ------------------------------------------------ 2.08s
total ------------------------------------------------------------------- 2.08s

With this out of the way, we can have a look at how we can influence white-spaces and line-breaks in Jinja templating within Ansible.


lstrip_blocks and trim_blocks

TL;DR

  • lstrip_blocks: False: If True, remove leading tabs/white-spaces from start of line until the next block.
  • trim_blocks: True: Remove trailing newlines in lines lines with only block elements.

Details

There are two main switches in Jinja templating in Ansible, that influence the overall template rendering.

  • lstrip_blocks
  • trim_blocks

Those are parameters from Jinja whitespace control.

By default Ansible templates have set trim_blocks: True and lstrip_blocks: False.

This differs from the default Jinja2 configuration in Python/Jinja. The example template file /tmp/ansible/template.j2 has set the same parameters in the top of the file (line 2). The line beginning with #jinja2 could be technically omitted, when left unchanged. I included it for clarification. We will also play around with it later on.

The syntax rules allow two formats of writing this line. Both are interchangeable. You can even mix them if you like:

  1. #jinja2: trim_blocks: True, lstrip_blocks: False
  2. #jinja2: trim_blocks: "true", lstrip_blocks: "false"
  3. #jinja2: trim_blocks: "true", lstrip_blocks: False
  4. #jinja2: trim_blocks: True, lstrip_blocks: "false"

These two parameters are the only ones included in the templating rendering engine from Jinja within Ansible. Here is what they mean:

Parameter Description
lstrip_blocks If set to True/"true", tabs and spaces will be removed from the beginning of the line to the start of the next block as long as no other elements are in-between.
trim_blocks If set to True/"true", the first newline after a template tag is automatically removed, if there is nothing between the newline and the template tag. This is being enabled per default within Ansible templates.

As mentioned: trim_blocks is set to True/"true" in Ansible. That is why the template file renders like this:

# cat /tmp/ansible/outputfile
-----
  foobar
-----

and not to

-----

  foobar

-----

The lines with the if condition are removed from the output. The line only contains the if block - which renders no output) and a following newline. Since trim_blocks: True applies, the whole line is not rendered.

For Ansible this makes practical sense. Jinja blocks usually do not contain much besides loop, conditions, expressions and other functionality, that - when rendered - usually does not not result in any content.

The lstrip_blocks setting has no effect in this example. First of all this parameter is disabled by default (and by the line being present in the template file). Second, there is no block with leading whitespace in the template file present. The line containing the if-condition does not start with a whitespace or line-break. So lstrip_blocks would not even apply if activated.

This example will make it clear:

Code example... Adding white-spaces before the if-block and keeping the Ansible default setting, will render the template differently.
$ cat /tmp/ansible/template.j2
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
  {% if true %}
  {{ i }}
{% endif %}
-----
This will be rendered to:
-----
    foobar
-----
Notice the four leading white-spaces before the output `foobar` in the output file? Two of them are from the actual line with ` {{ i }}`. The other two are from the previous line heading the if-tag. Setting `lstrip_blocks: True` in the template file will change the template rendering, remove leading white-spaces before a block and return the initial state of the rendered output file.
-----
  foobar
-----

{%- and -%}

TL;DR

Remove all white-spaces and new-lines leading or appending until the next encountered block element.

Details

The hyphen switches only address lines within the template. Adding a hyphen into a Jinja block or variable will remove any heading or pending white-spaces and new-lines before or after that element until the next block element. The exact behaviour depends on where the hyphen is being added.

This is a long sentence, so let’s break it down in an example: Adding a hyphen into a variable will remove heading white-spaces and new-lines.

The following example just adds a hyphen and explores what happens.

Code example... We add a hyphen in front of the variable element:
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
{% if true %}
  {{- i }}
{% endif %}
-----
This renders the template to the following output.
-----
foobar
-----
Any whitespace before the variable element has been removed. In the following code excerpt the removed white-spaces have been replaced with `X` and newlines with `\n` for clarification. This would not actually render correctly, since the `X`, etc are not affected by the hyphens.
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
{% if true %}XXXX\n
XX{{- i }}
{% endif %}
-----
If we do the opposite and add the hyphen at the end of the template instead, the output turns out different.
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
{% if true %}
  {{ i -}}
{% endif %}
-----
Renders to
-----
foobar-----
This is interesting. The output is missing a line-break following the block element as well. The expectation would be line-breaks being removed until the `{{ endif }}` tag. If you agree, you forget about the effect of `trim_blocks: True` in the default settings of the Ansible template engine, which removes the first new line after any template tag. In this case this leads to the removal of two newlines and the template file rendering this way. We can even move the hyphen from the `{{ i }}` element to the `{% endif %}` block and the file template will be rendered unchanged.
#jinja2: trim_blocks: True, lstrip_blocks: False
-----
{% if true %}
  {{ i }}
{%- endif %}
-----
Is being rendered to:
----
foobar----
Unless you add another newline into the template file, you will not be able to render the output file as expected.

{%+ and +%}

TL;DR

  • {%+: Disable lstrip_blocks: True for a specific line.
  • +%}: Disable trim_blocks: True for a specific line.

Details

The plus sign + has two different effects, depending on where is is used.

Adding a plus character to the beginning of the first Jinja block in a line will disable lstrip_blocks: True for that particular line. This works only for the first block in a line. Any following block on that line will be ignored.

Code example... The example below contains two white-spaces before the closing if-condition block. Also: `lstrip_bocks: True` has been enabled!
#jinja2: trim_blocks: True, lstrip_blocks: True
----
{% if true %}
  {{ i }}
  {% endif %}
-----
Those will be removed since `lstrip_blocks: True` has been set in the first line of the template file.
----
  foobar
----
If we now disable `lstrip_blocks` only for the line with the endif-block, the heading white-spaces will be preserved. The `+`-character makes all the difference.
#jinja2: trim_blocks: True, lstrip_blocks: True
-----
{% if true %}
  {{ i }}
  {%+ endif %}
-----
The result will be the usual:
----
  foobar

----
Though the white-spaces are not visible, they are in the output file. Here is the output file with `X` instead of white-spaces:
----
  foobar
XX
----
It is not possible to turn around the configuration, disable `lstrip_blocks` for the whole template and then enable it for a single line. The `+`-character only disables.

Adding the +-character to the closing braces of the first block in a line disables the option trim_blocks if set to True/"true".

Code example... This example will preserve the line-breaks that are introduced with the if-condition block. Notice that trim_blocks: True has been set and any leading whitespace and line-breaks should be removed.
#jinja2: trim_blocks: True, lstrip_blocks: True
-----
{% if true +%}
  {{ i }}
{% endif +%}
-----
The output renders now to this:
-----

  foobar

-----
In order to correct the output back to the default, we actually have to remove some line-breaks. There are several ways of doing this:
#jinja2: trim_blocks: True, lstrip_blocks: True
-----
{% if true %}  {{ i }}{% endif +%}
----

or


#jinja2: trim_blocks: True, lstrip_blocks: True
-----
{% if true +%}  {{ i }}
{% endif +%}-----
Both solutions restore the default rendering, but differ in readability. I prefer the first solution due to the clarity. The output then will be again:
-----
  foobar
-----

Summary

The time it took to write this article was well spent. The tinkering with the parameters and settings, trying to find any unexpected behaviour; all that increased my comfort level with the Jinja whitespace control.

The previous approach with just roughly writing the correct template and then hacking the result still works, of course. But after spending time going through the examples in detail and trying to understand what is happening, I find myself spending less time on try-and-error and more often render my templates correct on the first try.

One thing though I have to keep in mind:

Each parameter on its own is easy to handle. Confusion arises in combination with the default settings for trim_blocks and lstrip_blocks which differ from standard Jinja/Python in Ansible. Since the default settings and the line-operated switches can influence similar behaviour, unexpected things might happen.

Secondly, while the hyphen addresses white-spaces and new-lines, the plus character affects the general template rendering and the effect differs depending on if it is placed in the opening or closing braces.

This general overview served me well so far:

Tweak Description
{‰- Remove all heading white-spaces and new-lines before a block.
-%} Remove all trailing white-spaces and new-lines after the current block.
{%+ Disable lstrip_blocks for the current line.
+%} Disable trim_blocks for the current line.
#jinja2: trim_bloks: True Remove line-breaks from block-lines.
#jinja2: lstrip_blocks: True Remove leading white-spaces before elements.

I hope this article was able to shed some light on how whitespace control works in Ansible in combination with Jinja.

Daniel Buøy-Vehn

Senior Systems Consultant at Redpill Linpro

Daniel works with automation in the realm of Ansible, AWX, Tower, Terraform and Puppet. He rolls out mainly to our customer in Norway to assist them with the integration and automation projects.

Containerized Development Environment

Do you spend days or weeks setting up your development environment just the way you like it when you get a new computer? Is your home directory a mess of dotfiles and metadata that you’re reluctant to clean up just in case they do something useful? Do you avoid trying new versions of software because of the effort to roll back software and settings if the new version doesn’t work?

Take control over your local development environment with containerization and Dev-Env-as-Code!

... [continue reading]

Ansible-runner

Published on February 27, 2024

Portable Java shell scripts with Java 21

Published on February 21, 2024