<p>Use <code>select()</code> with the <code>defined</code> test.</p>
<pre><code>ssh_listen_to: &quot;{{ [ansible_facts.default_ipv4.address, ansible_facts.default_ipv6.address] | select('defined') | list }}&quot;
</code></pre>
<p>As for the linked post about matching dictionary keys in JMESPath, I wouldn't use JMESPath at all for that in Ansible. As I've grown more accustomed to Jinja I've found <code>json_query()</code> less and less necessary.</p>
<pre><code>ssh_listen_to: &quot;{{ ansible_facts | select('match', '.+_ipv[46]$') | map('extract', ansible_facts, 'address') | select('defined') | list }}&quot;
</code></pre>
<p><code>ansible_facts | select('match', '.+_ipv[46]$')</code> filters the keys using a regex. Iterating over a dictionary returns its keys, which are then matched against the provided regex.</p>
<p>Passing this filtered list of keys to <code>map('extract', ansible_facts, 'address')</code> extracts their values and looks up 'address' in each value. You could do this in two steps as <code>map('extract', ansible_facts) | map(attribute='address')</code>, if that makes things clearer.</p>
<p><code>select('defined')</code> gets rid of any undefined values, then <code>list</code> converts the generator returned from <code>select()</code> into a list (this generator unrolling happens automatically in ansible-base/ansible-core &gt;= 2.10, but a lot of people are still on Ansible 2.9.)</p>

CC BY-SA 4.0
2021-03-18T09:09:50.303Z
wonderful answer, thank you! Would you mind elaborating on that second command? What is `46`?
Michael Altfield
CC BY-SA 4.0
2021-03-18T14:41:18.477Z
`[46]` is a regex character class; it matches a single '4' or '6' in the string.
flowerysong
CC BY-SA 4.0
2021-03-17T19:56:20.757Z
2021-03-18T14:36:27.397Z
2021-03-18T14:36:27.397Z
flowerysong
<p>Create the list in Jinja2 and convert it, e.g.</p>
<pre class="lang-yaml prettyprint-override"><code>- hosts: localhost
  gather_facts: true
  tasks:
    - set_fact:
         ssh_listen_to: &quot;{{ _ssh_listen_to|from_yaml }}&quot;
      vars:
        _ssh_listen_to: |
          {% if ansible_default_ipv4.address is defined %}
          - {{ ansible_default_ipv4.address }}
          {% endif %}
          {% if ansible_default_ipv6.address is defined %}
          - {{ ansible_default_ipv6.address }}
          {% endif %}
    - debug:
        var: ssh_listen_to|type_debug
    - debug:
        var: ssh_listen_to
</code></pre>
<p>gives</p>
<pre class="lang-yaml prettyprint-override"><code>TASK [debug] ********************************************************************
ok: [localhost] =&gt; 
  ssh_listen_to|type_debug: list

TASK [debug] ********************************************************************
ok: [localhost] =&gt; 
  ssh_listen_to:
  - 10.1.0.27
</code></pre>
<hr>
<p>If you put the name of the variables into the list, e.g.</p>
<pre class="lang-yaml prettyprint-override"><code>    vars_addr:
      - default_ipv4
      - default_ipv6
</code></pre>
<p>the task below gives the same result</p>
<pre class="lang-yaml prettyprint-override"><code>    - set_fact:
         ssh_listen_to: &quot;{{ vars_addr|
                            map('extract', ansible_facts, 'address')|
                            select('defined')|
                            list  }}&quot;
</code></pre>

CC BY-SA 4.0
2021-03-17T16:20:59.863Z
2021-03-17T16:56:12.827Z
2021-03-17T16:56:12.827Z
Vladimir Botka
<p>This doesn't answer the question directly, but I found that I could query the entire ansible facts dictionary as a json document using the <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#selecting-json-data-json-queries" rel="nofollow noreferrer"><code>json_query</code> filter</a> and a <a href="https://jmespath.org/tutorial.html" rel="nofollow noreferrer">jmespath expression</a>.</p>
<p>For example, the following produces a list of all the ip addresses.</p>
<pre><code>ssh_listen_to: &quot;{{ ansible_facts | json_query('*.address') }}&quot;
</code></pre>
<p>You can make this safer by just taking the default ipv4 &amp; ipv6 addresses like so:</p>
<pre><code>ssh_listen_to: &quot;{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') }}&quot;
</code></pre>
<p>And you can even further check the sanity of the options by <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters_ipaddr.html#filtering-lists" rel="nofollow noreferrer">removing anything from the list that's not an IP address</a></p>
<pre><code>ssh_listen_to: &quot;{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') | ansible.netcommon.ipaddr }}&quot;
</code></pre>

CC BY-SA 4.0
2021-03-17T19:38:54.937Z
Be careful `shell> export address="This is not what I want."` will render the code unusable.
Vladimir Botka
CC BY-SA 4.0
2021-03-18T09:22:55.100Z
@VladimirBotka I updated the answer with some sanity checks :)
Michael Altfield
CC BY-SA 4.0
2021-03-17T16:52:28.117Z
2021-03-18T09:21:48.097Z
2021-03-18T09:21:48.097Z
Michael Altfield
<p>How do I write a yaml file such that it defines an array with a list of values, and it skips those values from being added to the yaml array if the given value is undefined?</p>
<p>Specifically, I'm trying to define an array of IP addresses to which I want a service (sshd) to bind to, including the ipv4 and ipv6 addresses. If there is both an ipv4 &amp; ipv6 address, then the array should contain both values. If there is only an ipv4 address or only an ipv6 address, then it should contain one of those values.</p>
<p>This doesn't work, as it will produce an error <code>&quot;msg&quot;: &quot;AnsibleUndefinedVariable: 'dict object' has no attribute 'address'&quot;}</code></p>
<pre><code>    ssh_listen_to:
      - &quot;{{ ansible_default_ipv4.address }}&quot;
      - &quot;{{ ansible_default_ipv6.address }}&quot;
</code></pre>
<p>This also doesn't work, as it will end-up adding a line <code>ListenAddress </code> with no argument, that breaks the sshd server (this may be a shortsighted design of my ansible role, but that's out-of-scope in this question).</p>
<pre><code>    ssh_listen_to:
      - &quot;{{ ansible_default_ipv4.address | default('') }}&quot;
      - &quot;{{ ansible_default_ipv6.address | default('') }}&quot;
</code></pre>
<p>How can I update the above yaml block such that it will skip adding the values to the array if the value isn't defined? Note that this is being done in my playbook's <code>vars:</code> section, so no tasks-based solutions please.</p>

CC BY-SA 4.0
2021-03-17T16:46:57.670Z
See also https://stackoverflow.com/questions/66677527/how-to-match-a-regex-on-an-array-name-itself-in-jmespath-json-element-string-re
Michael Altfield
<ansible><yaml>
Add item(s) to yaml array only if defined (ansible)
Michael Altfield
# Add item(s) to yaml array only if defined (ansible)

(Asked by: username_0 on 2021-03-17)

How do I write a yaml file such that it defines an array with a list of values, and it skips those values from being added to the yaml array if the given value is undefined?
Specifically, I'm trying to define an array of IP addresses to which I want a service (sshd) to bind to, including the ipv4 and ipv6 addresses. If there is both an ipv4 & ipv6 address, then the array should contain both values. If there is only an ipv4 address or only an ipv6 address, then it should contain one of those values.
This doesn't work, as it will produce an error "msg": "AnsibleUndefinedVariable: 'dict object' has no attribute 'address'"}
    ssh_listen_to:
      - "{{ ansible_default_ipv4.address }}"
      - "{{ ansible_default_ipv6.address }}"

This also doesn't work, as it will end-up adding a line ListenAddress  with no argument, that breaks the sshd server (this may be a shortsighted design of my ansible role, but that's out-of-scope in this question).
    ssh_listen_to:
      - "{{ ansible_default_ipv4.address | default('') }}"
      - "{{ ansible_default_ipv6.address | default('') }}"

How can I update the above yaml block such that it will skip adding the values to the array if the value isn't defined? Note that this is being done in my playbook's vars: section, so no tasks-based solutions please.

**Tags:** ansible, yaml

> **Comments:**
> **username_0**: See also https://stackoverflow.com/questions/66677527/how-to-match-a-regex-on-an-array-name-itself-in-jmespath-json-element-string-re  

---

## 3 answers

### Answer by username_1 on 2021-03-17. Score: 1 | Accepted answer

Use select() with the defined test.
ssh_listen_to: "{{ [ansible_facts.default_ipv4.address, ansible_facts.default_ipv6.address] | select('defined') | list }}"

As for the linked post about matching dictionary keys in JMESPath, I wouldn't use JMESPath at all for that in Ansible. As I've grown more accustomed to Jinja I've found json_query() less and less necessary.
ssh_listen_to: "{{ ansible_facts | select('match', '.+_ipv[46]$') | map('extract', ansible_facts, 'address') | select('defined') | list }}"

ansible_facts | select('match', '.+_ipv[46]$') filters the keys using a regex. Iterating over a dictionary returns its keys, which are then matched against the provided regex.
Passing this filtered list of keys to map('extract', ansible_facts, 'address') extracts their values and looks up 'address' in each value. You could do this in two steps as map('extract', ansible_facts) | map(attribute='address'), if that makes things clearer.
select('defined') gets rid of any undefined values, then list converts the generator returned from select() into a list (this generator unrolling happens automatically in ansible-base/ansible-core >= 2.10, but a lot of people are still on Ansible 2.9.)

> **Comments:**
> **username_0**: wonderful answer, thank you! Would you mind elaborating on that second command? What is `46`?  
> **username_1**: `[46]` is a regex character class; it matches a single '4' or '6' in the string.  

---

### Answer by username_2 on 2021-03-17. Score: 1

Create the list in Jinja2 and convert it, e.g.
- hosts: localhost
  gather_facts: true
  tasks:
    - set_fact:
         ssh_listen_to: "{{ _ssh_listen_to|from_yaml }}"
      vars:
        _ssh_listen_to: |
          {% if ansible_default_ipv4.address is defined %}
          - {{ ansible_default_ipv4.address }}
          {% endif %}
          {% if ansible_default_ipv6.address is defined %}
          - {{ ansible_default_ipv6.address }}
          {% endif %}
    - debug:
        var: ssh_listen_to|type_debug
    - debug:
        var: ssh_listen_to

gives
TASK [debug] ********************************************************************
ok: [localhost] => 
  ssh_listen_to|type_debug: list

TASK [debug] ********************************************************************
ok: [localhost] => 
  ssh_listen_to:
  - 10.1.0.27


If you put the name of the variables into the list, e.g.
    vars_addr:
      - default_ipv4
      - default_ipv6

the task below gives the same result
    - set_fact:
         ssh_listen_to: "{{ vars_addr|
                            map('extract', ansible_facts, 'address')|
                            select('defined')|
                            list  }}"


---

### Answer by username_0 on 2021-03-17. Score: 0

This doesn't answer the question directly, but I found that I could query the entire ansible facts dictionary as a json document using the json_query filter and a jmespath expression.
For example, the following produces a list of all the ip addresses.
ssh_listen_to: "{{ ansible_facts | json_query('*.address') }}"

You can make this safer by just taking the default ipv4 & ipv6 addresses like so:
ssh_listen_to: "{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') }}"

And you can even further check the sanity of the options by removing anything from the list that's not an IP address
ssh_listen_to: "{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') | ansible.netcommon.ipaddr }}"

> **Comments:**
> **username_2**: Be careful `shell> export address="This is not what I want."` will render the code unusable.  
> **username_0**: @VladimirBotka I updated the answer with some sanity checks :)  

---

