Ansible Jinja2 Join Filter: Add Commas Between List Elements
By Luca Berton · Published 2024-01-01 · Category: web-servers
How to add commas between list elements in Ansible Jinja2. Use join filter, loop techniques, and string manipulation for formatted output.
Introduction
Creating a comma-separated list is a common task in templating, but ensuring no trailing comma appears after the last element can be tricky. In Jinja2, theloop.last property provides a simple and elegant solution. In this guide, we’ll explore how to use loop.last in Jinja2 and how to apply it effectively in Ansible playbooks.
See also: Generate Clean YAML Output from Ansible Facts
Jinja2 Template Example
To avoid a trailing comma, leverage theloop.last property in your Jinja2 template:
{% for item in my_list %}
{{ item }}{% if not loop.last %},{% endif %}
{% endfor %}
Explanation:
loop.last: A built-in Jinja2 variable that evaluates to True if the current iteration is the last one.
if not loop.last: Ensures a comma is only added when the current item is not the last element.
Input Example:
my_list:
- apple
- banana
- cherry
Output:
apple, banana, cherry
---
Ansible Playbook Example
To achieve the same result in an Ansible playbook, use thetemplate module or inline Jinja2.
Playbook Example:
---
- name: Generate a comma-separated string
hosts: localhost
vars:
my_list:
- apple
- banana
- cherry
tasks:
- name: Create a string with commas
set_fact:
comma_separated: >-
{{ my_list | map('string') | join(', ') }}
- name: Debug the result
debug:
msg: "{{ comma_separated }}"
Explanation:
join(', '): Combines all elements in the list with a comma and a space.
map('string'): Ensures all elements are converted to strings before joining. This is especially useful when dealing with mixed data types.
Output:
ok: [localhost] => {
"msg": "apple, banana, cherry"
}
---
See also: Mastering Time in Ansible: An Introduction to the now() Function
Practical Considerations
Why Use loop.last?
Using loop.last in Jinja2 is a direct and effective way to control list formatting, especially when generating human-readable strings or configuration files.
Ansible and Jinja2 Integration
Ansible leverages Jinja2 for templating, making it a versatile tool for creating dynamic configurations. The use ofjoin in combination with map often simplifies tasks when handling larger or complex lists.
---
Conclusion
By mastering theloop.last property and the join filter, you can handle comma-separated lists seamlessly in both Jinja2 and Ansible. Whether you’re building templates or automating configurations, these techniques provide clarity and precision to your code.
See also: ansible-core 2.19 Templating Changes: Fix Broken Conditionals & Jinja Errors
Methods to Join Elements
Method 1: join filter (simplest)
- name: Join list with commas
vars:
fruits: ['apple', 'banana', 'cherry']
debug:
msg: "{{ fruits | join(', ') }}"
# Output: "apple, banana, cherry"
Method 2: Join with custom separator
- name: Join with different separators
vars:
servers: ['web1', 'web2', 'web3']
debug:
msg:
- "Comma: {{ servers | join(', ') }}"
- "Pipe: {{ servers | join(' | ') }}"
- "Newline: {{ servers | join('\n') }}"
- "Space: {{ servers | join(' ') }}"
Method 3: Join with and for the last element
- name: Natural language list
vars:
items: ['red', 'green', 'blue']
debug:
msg: "{{ items[:-1] | join(', ') }} and {{ items[-1] }}"
# Output: "red, green and blue"
Method 4: In Jinja2 templates (loop approach)
{# In a template file #}
{% for server in servers %}{{ server }}{% if not loop.last %}, {% endif %}{% endfor %}
{# Output: web1, web2, web3 #}
Method 5: Join with transformation
- name: Join after transforming elements
vars:
ports: [80, 443, 8080]
debug:
msg: "{{ ports | map('string') | map('regex_replace', '^(.*)$', 'port \1') | join(', ') }}"
# Output: "port 80, port 443, port 8080"
Real-World Use Cases
Generate comma-separated hosts for config file
- name: Create cluster config
ansible.builtin.copy:
content: |
cluster_nodes={{ groups['db_servers'] | map('extract', hostvars, 'ansible_host') | join(',') }}
dest: /etc/myapp/cluster.conf
Create DNS-style host list
- name: Generate allowed hosts
vars:
domains: ['example.com', 'api.example.com', 'admin.example.com']
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
# In template: server_name {{ domains | join(' ') }};
Filter and join
- name: Join only non-empty elements
vars:
tags: ['web', '', 'production', '', 'v2']
debug:
msg: "{{ tags | select | join(', ') }}"
# Output: "web, production, v2" (empty strings removed)
FAQ
How do I join a list of dictionaries?
Extract the key first with map:
- vars:
users:
- name: alice
- name: bob
- name: charlie
debug:
msg: "{{ users | map(attribute='name') | join(', ') }}"
# Output: "alice, bob, charlie"
How do I add quotes around each element?
- debug:
msg: "{{ items | map('quote') | join(', ') }}"
# Output: "'apple', 'banana', 'cherry'"
Can I use join in when conditions?
Yes, but it's rarely needed. More common to use in operator:
- debug:
msg: "Found target"
when: "'web' in group_names"
join Filter (Simplest)
- vars:
servers: [web1, web2, web3]
debug:
msg: "{{ servers | join(', ') }}"
# Output: "web1, web2, web3"
Different Separators
- debug:
msg:
- "{{ items | join(', ') }}" # web1, web2, web3
- "{{ items | join(' | ') }}" # web1 | web2 | web3
- "{{ items | join('\n') }}" # Each on new line
- "{{ items | join(';') }}" # web1;web2;web3
- "{{ items | join(' AND ') }}" # web1 AND web2 AND web3
In Templates (loop.last)
{# templates/config.j2 #}
servers = [
{% for server in servers %}
"{{ server }}"{{ "," if not loop.last else "" }}
{% endfor %}
]
Output:
servers = [
"web1",
"web2",
"web3"
]
JSON Array in Template
{
"hosts": [
{% for host in groups['webservers'] %}
"{{ hostvars[host].ansible_host }}"{{ "," if not loop.last }}
{% endfor %}
]
}
Nginx Upstream
upstream backend {
{% for host in groups['appservers'] %}
server {{ hostvars[host].ansible_host }}:{{ app_port }}{{ ";" if not loop.last else ";" }}
{% endfor %}
}
join with Attribute
- vars:
users:
- { name: alice, email: alice@example.com }
- { name: bob, email: bob@example.com }
debug:
msg: "{{ users | map(attribute='email') | join(', ') }}"
# Output: "alice@example.com, bob@example.com"
Quoted List
- vars:
packages: [nginx, curl, vim]
debug:
msg: "{{ packages | map('regex_replace', '^(.*)$', '\"\\1\"') | join(', ') }}"
# Output: "nginx", "curl", "vim"
Conditional Items
# Join only non-empty items
- vars:
parts: [web1, "", web3, null, web5]
debug:
msg: "{{ parts | select | join(', ') }}"
# Output: "web1, web3, web5"
In Config Files
# Generate comma-separated list for config
- copy:
content: |
ALLOWED_HOSTS={{ allowed_hosts | join(',') }}
DB_SERVERS={{ db_servers | join(',') }}
dest: /etc/myapp/.env
FAQ
How do I add "and" before the last item?
{% for item in items %}
{{ item }}{{ ", " if not loop.last else "" }}{% if loop.last and loop.length > 1 %} and {{ item }}{% endif %}
{% endfor %}
Simpler approach:
msg: "{{ items[:-1] | join(', ') }} and {{ items[-1] }}"
Can I join with newlines?
msg: "{{ items | join('\n') }}"
# In templates, just use the loop without join
How do I handle single-item lists?
join works correctly — a single-item list returns just that item with no separator.
join Filter
- vars:
servers: [web1, web2, web3]
debug:
msg: "{{ servers | join(', ') }}"
# Output: "web1, web2, web3"
Different Separators
# Comma
"{{ items | join(', ') }}" # a, b, c
# Semicolon
"{{ items | join('; ') }}" # a; b; c
# Newline
"{{ items | join('\n') }}" # a\nb\nc
# Pipe
"{{ items | join(' | ') }}" # a | b | c
# No separator
"{{ items | join('') }}" # abc
In Templates
{# /etc/myapp/config.conf.j2 #}
allowed_hosts = {{ allowed_hosts | join(', ') }}
dns_servers = {{ dns_servers | join(' ') }}
{# With loop for complex formatting #}
{% for host in hosts %}
server {{ host }}{% if not loop.last %},{% endif %}
{% endfor %}
Join with Attribute
- vars:
users:
- { name: alice, role: admin }
- { name: bob, role: user }
debug:
msg: "{{ users | map(attribute='name') | join(', ') }}"
# Output: "alice, bob"
Conditional Join
# Join only non-empty values
- vars:
parts: ["web", "", "server", "", "01"]
debug:
msg: "{{ parts | select | join('-') }}"
# Output: "web-server-01"
Generate Config Lines
- vars:
nameservers: [8.8.8.8, 8.8.4.4, 1.1.1.1]
copy:
content: |
{% for ns in nameservers %}
nameserver {{ ns }}
{% endfor %}
dest: /etc/resolv.conf
Oxford Comma / Last Element Different
{# "a, b, and c" pattern #}
{% if items | length == 1 %}
{{ items[0] }}
{% elif items | length == 2 %}
{{ items[0] }} and {{ items[1] }}
{% else %}
{{ items[:-1] | join(', ') }}, and {{ items[-1] }}
{% endif %}
FAQ
join on a string?
If the variable is a string, join treats each character as an element. Make sure you're joining a list.
How to join dict values?
"{{ my_dict.values() | list | join(', ') }}"
Can I join with HTML?
Yes: {{ items | join(' — useful for email templates.
') }}
Related Articles
• whitespace control in Jinja2 for Ansible • using ansible.builtin.command effectively • nested loops in AnsibleCategory: web-servers