AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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, the loop.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 the loop.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 the template 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 of join in combination with map often simplifies tasks when handling larger or complex lists.

---

Conclusion

By mastering the loop.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 Ansibleusing ansible.builtin.command effectivelynested loops in Ansible

Category: web-servers

Browse all Ansible tutorials · AnsiblePilot Home