Jinja2 教程 - 第 3 部分 - 空白控制
作者:互联网
文本文档是渲染模板的最终结果。根据这些文档的最终消费者,空白放置可能很重要。在我看来,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块-
的开头添加即可按预期呈现文本。让我们尝试这样做,看看会发生什么。if
endif
{% 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 不关心if
orfor
块内的额外空间,它会简单地忽略它们。它只关心它在块之外找到的空格。
让我们渲染它来看看我们得到了什么:
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
标签:access,教程,idata,0.0,空格,空白,Jinja2,模板 来源: https://www.cnblogs.com/a00ium/p/16058504.html