文本文檔是渲染模板的最終結果。根據這些文檔的最終消費者,空白放置可能很重要。在我看來,Jinja2 的主要問題之一是控制語句和其他元素影響最終文檔中的空白輸出的方式。
坦率地說,掌握 Jinja2 中的空格是確保模板完全按照您的意圖生成文本的唯一方法。
現在我們知道了問題的重要性,是時候了解它的起源了,為此我們將看很多例子。然后我們將學習如何在 Jinja2 模板中控制渲染空白。
我們將從查看 Jinja2 如何通過查看簡單示例、沒有變量的模板、只有兩行文本和一個注釋來呈現空白開始我們的學習:
Starting line
{# Just a comment #}
Line after comment
這是渲染時的樣子:
Starting line
Line after comment
好的,這里發生了什么?您是否期望出現一個空行來代替我們的評論?我沒有。我希望評論行會消失在虛無之中,但這里的情況並非如此。
所以這是關於 Jinja2 的一個非常重要的事情。渲染模板時,所有語言塊都將被刪除,但所有空格都保留在原處。也就是說,如果在塊之前或之后有空格、制表符或換行符,那么這些將被渲染。
這解釋了為什么一旦模板被渲染,注釋塊就會留下一個空行。塊后有一個換行符{# #}。當塊本身被刪除時,換行符仍然存在。
下面是一個更復雜但相當典型的模板,包含for循環和if語句:
{% for iname, idata in interfaces.items() %}
interface {{ iname }}
description {{ idata.description }}
{% if idata.ipv4_address is defined %}
ip address {{ idata.ipv4_address }}
{% endif %}
{% endfor %}
我們輸入模板的值:
interfaces: Ethernet1: description: capture-port Ethernet2: description: leaf01-eth51 ipv4_address: 10.50.0.0/31
https://j2live.ttl255.com/

這就是 Jinja2 將如何渲染它,所有設置都保留為默認值:
interface Ethernet1 description capture-port interface Ethernet2 description leaf01-eth51 ip address 10.50.0.0/31
這看起來不太好,是嗎?在少數地方添加了額外的換行符。此外,有趣的是,某些行上有前導空格,這些空格在屏幕上看不到,但將來可能真的會破壞我們的東西。總的來說,很難弄清楚所有空格的來源。
為了幫助您更好地可視化生成的文本,下面是相同的輸出,但現在渲染了所有空格:

每個項目符號點代表一個空格字符,返回圖標代表換行符。您現在應該清楚地看到 Jinja2 塊在其中三行留下的前導空格,以及所有額外的換行符。
好的,你說的很好,但這些來自哪里仍然不是很明顯。我們要回答的真正問題是:
哪個模板行對最終結果中的哪個行有貢獻?
為了回答這個問題,我在模板和輸出文本中渲染了空格。然后我在感興趣的行中添加了彩色、編號、突出顯示的塊,以便我們將源與最終產品相匹配。


您現在應該很容易看到每個 Jinja 塊在結果文本中添加空格的位置。
如果您也好奇為什么,請繼續閱讀以獲取詳細說明:
-
包含
{% for %}塊的行,編號為 1 的藍色輪廓,以換行符結尾。為字典中的每個鍵執行此塊。我們有 2 個鍵,所以我們在最終文本中插入了額外的 2 個換行符。 -
包含
{% if %}塊的行,數字 2a 和 2b,帶有綠色和淺綠色的輪廓,有 2 個前導空格並以換行符結尾。這就是事情變得有趣的地方。實際{% if %}塊被移除,留下 2 個總是被渲染的空間。但尾隨換行符在塊內。這意味着通過{% if %}評估false我們得到 2a 但不是 2b。如果它評估為true我們得到 2a 和 2b。 -
包含
{% endif %}塊的行,數字 3a 和 3b,帶有紅色和橙色輪廓,有 2 個前導空格並以換行符結尾。這又很有趣,我們這里的情況與以前的情況相反。兩個前導空格在if塊內,但換行符在塊外。所以 3b,換行,總是被渲染。但是當{% if %}塊評估為時,true我們也得到 3a,如果是,false那么我們只得到 3b。
還值得指出的是,如果您的模板在{% endfor %}塊之后繼續,則該塊將貢獻一個額外的換行符。但不要擔心,我們稍后會舉一些例子來說明這個案例。
我希望您同意我的觀點,我們在示例中使用的模板不是特別大或特別復雜,但它導致了相當多的額外空格。
幸運的是,我不能再強調它的用處了,有一些方法可以改變 Jinja2 的行為並重新控制我們文本的確切外觀和感覺。
注意。上述解釋於 2020 年 12 月 12 日更新。之前第一次出現的 3b 被錯誤地歸因於 2b。非常感謝 Lawrr,他對我進行了三重檢查,並極大地幫助了我了解這一點!
查找空格的來源 - 替代方法
我們已經討論了如何在空白生成方面馴服 Jinja 的引擎。您還知道J2Live 之類的工具可以幫助您可視化生成的文本中的所有空格。但是我們能否確定是哪個模板行(包含塊)為最終渲染貢獻了這些字符?
為了得到這個問題的答案,我們可以使用一個小技巧。我想出了以下不需要任何外部工具的技術,用於匹配來自模板塊行的空格和出現在結果文本文檔中的無關空格。
這個方法真的很簡單,你只需要在模板中與渲染文檔中的行相對應的每個塊行中添加明確的字符。
我發現它特別適用於模板繼承和宏,我們將在本教程的后續部分中討論這些主題。
空格的起源 - 示例
讓我們看看那個秘方在行動。我們將在 Jinja2 塊所在行的戰略位置放置額外的字符,經過精心挑選,使它們從周圍的文本中脫穎而出。我使用的是我們之前使用過的相同模板,因此您可以輕松比較結果。
{% for iname, idata in interfaces.items() %}(1)
interface {{ iname }}
description {{ idata.description }}
(2){% if idata.ipv4_address is defined %}
ip address {{ idata.ipv4_address }}
(3){% endif %}
{% endfor %}
最后結果:
(1)
interface Ethernet1
description capture-port
(2)
(1)
interface Ethernet2
description leaf01-eth51
(2)
ip address 10.50.0.0/31
(3)
我在我們有 Jinja2 塊的行上添加了(1),(2)和字符。(3)最終結果與我們從Show whitespaces啟用選項的 J2Live 返回的結果相匹配。
如果您無法訪問J2Live或者您需要解決生產模板中的空白放置問題,那么我絕對推薦使用此方法。這很簡單但很有效。
為了獲得更多練習,我在稍微復雜的模板中添加了額外的字符。這個有分支if語句和 final 下面的一些文本endfor,以便我們查看來自該塊的空格。
我們的模板:
{% for acl, acl_lines in access_lists.items() %}(1)
ip access-list extended {{ acl }}
{% for line in acl_lines %}(2)
(3){% if line.action == "remark" %}
remark {{ line.text }}
(4){% elif line.action == "permit" %}
permit {{ line.src }} {{ line.dst }}
(5){% endif %}
{% endfor %}(6)
{% endfor %}(7)
# All ACLs have been generated
用於渲染它的數據:
access_lists: al-hq-in: - action: remark text: Allow traffic from hq to local office - action: permit src: 10.0.0.0/22 dst: 10.100.0.0/24

最終結果:
(1) ip access-list extended al-hq-in (2) (3) remark Allow traffic from hq to local office (4) (2) (3) permit 10.0.0.0/22 10.100.0.0/24 (5) (6) (7) # All ACLs have been generated


這里發生了很多事情,但不再有任何謎團了。您可以輕松地將每個源代碼行與最終文本中的行匹配。了解空格的來源是學習如何控制它們的第一步,這也是我們稍后要討論的內容。
此外,為了比較是不使用helper字符的渲染文本:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
如果你還在讀這篇文章,恭喜!您對掌握空白渲染的奉獻精神值得稱贊。好消息是,我們現在正在學習如何控制 Jinja2 的行為。
控制 Jinja2 空格
我們可以通過三種方式控制模板中的空白生成:
- 啟用渲染選項之一或兩者
trim_blocks。lstrip_blocks -通過在塊的開頭或結尾添加減號來手動去除空格。- 在 Jinja2 塊內應用縮進。
首先,我會給你一個簡單的,更可取的馴服空白的方法,然后我們將深入研究更多涉及的方法。
所以它來了:
始終在啟用
trim_blocks和lstrip_blocks選項的情況下進行渲染。
就是這樣,大秘密就出來了。省去麻煩,告訴 Jinja2 對所有塊應用修剪和剝離。
如果您將 Jinja2 用作另一個框架的一部分,那么您可能需要查閱文檔以了解默認行為是什么以及如何更改它。在這篇文章的后面,我將解釋在使用 Ansible 渲染 Jinja2 模板時如何控制空格。
只需簡單解釋一下這些選項的作用。修剪會在塊后刪除換行符,而剝離會刪除塊前行上的所有空格和制表符。現在,如果您單獨啟用修剪,如果包含塊的行上有任何前導空格,您可能仍然會得到一些有趣的輸出,所以這就是為什么我建議同時啟用這兩個。
修剪和剝離在行動
例如,當我們啟用塊修剪但禁用塊剝離時會發生這種情況:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
這就是我們剛剛看過的同一個例子,我敢肯定你根本沒想到會發生這種情況。讓我們添加一些額外的字符來弄清楚發生了什么:
{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
{% for line in acl_lines %}
(3){% if line.action == "remark" %}
remark {{ line.text }}
(4){% elif line.action == "permit" %}
permit {{ line.src }} {{ line.dst }}
{% endif %}
{% endfor %}
{% endfor %}
# All ACLs have been generated
ip access-list extended al-hq-in
(3) remark Allow traffic from hq to local office
(4) (3) permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated

另一個難題解決了,我們擺脫了trim_blocks啟用但前面的前導空格if和elif塊仍然存在的換行符。完全不受歡迎的東西。
那么如果我們同時啟用了修剪和剝離,這個模板將如何呈現呢?看一看:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
很漂亮吧?這就是我在談到獲得預期結果時的意思。沒有驚喜,沒有額外的換行符或空格,最終文本符合我們的預期。

現在,我說啟用 trim 和 lstrip 選項是一種簡單的方法,但是如果由於某種原因你不能使用它,或者想要完全控制每個塊上空格的生成方式,那么我們需要求助於手動控制.
手動
Jinja2 允許我們手動控制空格的生成。您可以通過使用減號-從塊、注釋或變量表達式中去除空格來做到這一點。您需要將其添加到給定表達式的開頭或結尾,以分別刪除塊之前或之后的空格。
與往常一樣,最好從示例中學習。我們將回到文章開頭的示例。首先我們渲染沒有添加任何-標志:
{% for iname, idata in interfaces.items() %}
interface {{ iname }}
description {{ idata.description }}
{% if idata.ipv4_address is defined %}
ip address {{ idata.ipv4_address }}
{% endif %}
{% endfor %}
結果:
對,一些額外的換行符,還有額外的空格。讓我們在塊的末尾添加減號for:
{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
description {{ idata.description }}
{% if idata.ipv4_address is defined %}
ip address {{ idata.ipv4_address }}
{% endif %}
{% endfor %}

看起來很有希望,我們刪除了兩個額外的換行符。
接下來我們看if塊。我們需要去掉這個塊生成的換行符,所以我們嘗試-在最后添加,就像我們對for塊所做的那樣。
{% for iname, idata in interfaces.items() -%} interface {{ iname }} description {{ idata.description }} {% if idata.ipv4_address is defined -%} ip address {{ idata.ipv4_address }} {% endif %} {% endfor %}

下一行后的換行description消失Ethernet2了。哦,但是等等,為什么我們現在有兩個空格ip address?啊哈!這些一定是塊前面的兩個空格if。讓我們也添加-到該塊的開頭,我們就完成了!
{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
description {{ idata.description }}
{%- if idata.ipv4_address is defined -%}
ip address {{ idata.ipv4_address }}
{% endif %}
{% endfor %}

嗯,現在都壞了!這里發生了什么?確實是一個非常好的問題。
事情就是這樣。這些神奇的減號刪除了塊之前或之后的所有空格,而不僅僅是同一行上的空格。不知道你是否預料到了,當我第一次使用手動空白控制時,我當然沒有!
在我們的具體例子中,-我們在塊的末尾添加了if第一個換行符和下一行的一個空格,即ip address * 之前的一個空格。因為,如果我們現在仔細觀察,我們應該有三個空格,而不僅僅是兩個。我們自己放在那里的一個空間和我們在if街區前面的兩個空間。-但是我們放置的那個空間由於放置在if塊中的標志而被 Jinja2 刪除了。
不過,並非一切都丟失了。您可能會注意到,只需在and塊-的開頭添加即可按預期呈現文本。讓我們嘗試這樣做,看看會發生什么。ifendif
{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
description {{ idata.description }}
{%- if idata.ipv4_address is defined %}
ip address {{ idata.ipv4_address }}
{%- endif %}
{% endfor %}
結果:

答對了!我們擺脫了所有那些討厭的空格!但這簡單直觀嗎?並不真地。公平地說,這不是一個非常復雜的例子。手動控制空格當然是可能的,但你必須記住,所有的空格都被刪除了,只有與塊在同一行的那些。
Jinja2 塊內的縮進
有一種編寫塊的方法可以使事情變得更容易和可預測。我們只需將塊的開頭放在行的開頭並在塊內應用縮進。與往常一樣,使用示例更容易解釋:
{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
{% for line in acl_lines %}
{% if line.action == "remark" %}
remark {{ line.text }}
{% elif line.action == "permit" %}
permit {{ line.src }} {{ line.dst }}
{% endif %}
{% endfor %}
{% endfor %}
# All ACLs have been generated
如您所見,我們將塊開口{%一直移動到左側,然后在塊內適當縮進。Jinja2 不關心iforfor塊內的額外空間,它會簡單地忽略它們。它只關心它在塊之外找到的空格。
讓我們渲染它來看看我們得到了什么:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
你可能會問,這有什么好?乍一看,並沒有好多少。但在我告訴你為什么這可能是一個好主意以及它特別有用的地方之前,我將向你展示與之前相同的模板。
我們將trim_blocks啟用它來渲染它:
{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
{% for line in acl_lines %}
{% if line.action == "remark" %}
remark {{ line.text }}
{% elif line.action == "permit" %}
permit {{ line.src }} {{ line.dst }}
{% endif %}
{% endfor %}
{% endfor %}
# All ACLs have been generated
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
太可怕了,簡直太可怕了。縮進完全失控了。但我想向你展示什么?好吧,現在讓我們渲染這個模板的版本,其中for和if塊內有縮進,再次trim_blocks打開:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
這不是很好嗎?請記住,以前我們必須啟用兩者trim_blocks並lstrip_blocks達到相同的效果。
所以這里是:
從行首開始 Jinja2 塊並在其中應用縮進大致相當於 enable
lstrip_block。
我說大致等價,因為我們在這里沒有剝離任何東西,我們只是在塊內隱藏了額外的空間,以防止它們被拾取。
使用這種方法還有一個額外的好處,它會讓你在 Ansible 中使用的 Jinja2 模板更安全。為什么?繼續閱讀!
Ansible 中的空白控制
您可能已經知道 Jinja2 模板在使用 Ansible 進行網絡自動化時被大量使用。大多數人會使用 Ansible 的template模塊來進行模板的渲染。該模塊默認啟用trim_blocks選項,但lstrip_blocks已關閉,需要手動啟用。
我們可以假設大多數用戶將使用template帶有默認選項的模塊,這意味着在塊技術內部使用縮進將提高我們的模板和呈現文本的安全性。
由於上述原因,如果您知道您的模板將在 Ansible 中使用,我建議您應用此技術。您將大大降低模板在配置和其他文檔中出現看似隨機的空格的風險。
我還要說,如果您還沒有掌握 Jinja2 的神秘方式,那么始終堅持這種編寫積木的方式並不是一個壞主意。以這種方式編寫模板並沒有真正的缺點。
這里唯一的副作用是模板如何在視覺上呈現自己,很多塊模板看起來“忙”。這可能會導致很難看到塊之間的文本行,因為這些需要具有與您的意圖相匹配的縮進。
就我個人而言,我總是嘗試在用於 Ansible 的模板中使用 blocks 方法中的縮進。對於其他模板,當使用 Python 渲染時,我會在可讀性方面做任何感覺正確的事情,並且我渲染所有模板時都啟用了塊修剪和剝離。
示例劇本
為了完整起見,我構建了兩個簡短的 Ansible Playbook,一個使用template模塊的默認設置,而另一個啟用lstrip選項。
我們將使用之前用於測試trim和lstrip選項的相同模板和數據。
Playbook 使用默認設置,即僅trim打開:
---
- hosts: localhost
gather_facts: no
connection: local
vars_files:
- vars/access-lists.yml
tasks:
- name: Show vars
debug:
msg: "{{ access_lists }}"
- name: Render config for host
template:
src: "templates/ws-access-lists.j2"
dest: "out/ws-default.cfg"
和渲染結果:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
trim如果您還記得,我們在啟用選項的 Python 中渲染此模板時得到了完全相同的結果。同樣,縮進是錯位的,所以我們需要做得更好。
劇本啟用lstrip:
---
- hosts: localhost
gather_facts: no
connection: local
vars_files:
- vars/access-lists.yml
tasks:
- name: Show vars
debug:
msg: "{{ access_lists }}"
- name: Render config for host
template:
src: "templates/ws-access-lists.j2"
dest: "out/ws-lstrip.txt"
lstrip_blocks: yes
渲染文本:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
trim同樣,與啟用和lstrip在 Python 中渲染 Jinja2 時的結果相同。
最后,讓我們運行第一個 Playbook,使用默認設置,使用帶有塊內縮進的模板。
劇本:
---
- hosts: localhost
gather_facts: no
connection: local
vars_files:
- vars/access-lists.yml
tasks:
- name: Show vars
debug:
msg: "{{ access_lists }}"
- name: Render config for host
template:
src: "templates/ws-bi-access-lists.j2"
dest: "out/ws-block-indent.txt"
結果:
ip access-list extended al-hq-in
remark Allow traffic from hq to local office
permit 10.0.0.0/22 10.100.0.0/24
# All ACLs have been generated
因此,我們不必啟用lstrip選項來獲得相同的、完美的結果。希望現在您能明白為什么我建議在塊內使用縮進作為 Ansible 模板的默認設置。這讓您更有信心使用默認設置以您想要的方式呈現您的模板。
結束的想法
當我坐下來寫這篇文章時,我以為我知道 Jinja2 中的空格是如何工作的。但事實證明,有些行為對我來說不是很清楚。對於使用符號進行手動剝離尤其如此-,我一直忘記剝離塊之前/之后的所有空格,而不僅僅是帶有塊的行。
所以我的建議是:盡可能使用修剪和剝離選項,並且通常更喜歡塊內的縮進而不是外部的縮進。並花一些時間學習 Jinja2 如何生成空格,這將使您能夠在需要時完全控制您的模板。
就是這樣,我希望你覺得這篇文章有用,我期待再次見到你!
參考:
- Jinja2 (2.11.x) 最新版本的官方文檔。可在:https ://jinja.palletsprojects.com/en/2.11.x/
- Ansible 模板模塊的文檔:https ://docs.ansible.com/ansible/latest/modules/template_module.html
- PyPi 上的 Jinja2 Python 庫。可在:https ://pypi.org/project/Jinja2/
- 包含這篇文章資源的 GitHub 存儲庫。可在:https ://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p3-whitespace-control
