I need to parse a text file for gaps in a numbered list, then select the first missing number as an Ansible variable.
For example, with a list that looks like this:
100
101
102
103
104
106
107
109
110
My requirement is to identify the first integer missing in the sequence 100..111 from this file, in this case, it would need to select 105.
I am currently using lineinfile to parse the text:
- lineinfile:
dest: target.txt
regexp: "{{ item }}"
state: absent
check_mode: yes
register: search
loop: "{{ range(100, 111) | list }}"
This gives me a dictionary called search that contains, among other things, a found value for each number. I'd like to filter the dictionary to select the first number occurring with value found: '0', but have so far been unsuccessful.
Due to an outdated version of Ansible and Jinja2 (that can't be updated for reasons), I don't have access to the selectattr or rejectattr filters, which I know would be the preferred way of accomplishing this. I'm currently trying to develop a workaround using json_query but haven't had any luck yet.
I don't care how cumbersome the final code is.
You could do a simple loop, with the conditions:
found is zero- set_fact:
first_missing: "{{ item.item }}"
loop: "{{ search.results }}"
when: first_missing is not defined and item.found == 0
loop_control:
label: "{{ item.item }}"
This would yield:
TASK [set_fact] **************************************************************
skipping: [localhost] => (item=100)
skipping: [localhost] => (item=101)
skipping: [localhost] => (item=102)
skipping: [localhost] => (item=103)
skipping: [localhost] => (item=104)
ok: [localhost] => (item=105)
skipping: [localhost] => (item=106)
skipping: [localhost] => (item=107)
skipping: [localhost] => (item=108)
skipping: [localhost] => (item=109)
skipping: [localhost] => (item=110)
And you effectively ends with 105 inside the variable first_missing.
But your idea to have a JMESPath query is pretty clever, and can do it too, the set_fact becomes as simple as:
- set_fact:
first_missing: >-
{{
search.results
| json_query('[?found == `0`].item | [0]')
}}
Where
?found == `0` allows you to select all the elements of the list having found equal to zero.item only select the item attribute of the dictionaries in the list| [0]A third solution, which is faster, is to use the diff mode along with the check_mode on a copy module, recreating the file without the gaps:
- copy:
content: >-
{% for i in range(100, 111) -%}
{{ i }}
{% endfor -%}
dest: target.txt
diff: yes
check_mode: yes
register: file
This will yield:
TASK [copy] *******************************************************************
--- before: target.txt
+++ after: /root/.ansible/tmp/ansible-local-36701z60diga/tmp2ao9r38b
@@ -3,7 +3,9 @@
102
103
104
+105
106
107
+108
109
110
changed: [localhost]
Sadly, the registered result is not as smooth as the output and gives one big before/after state of the file, so you fall back to a solution similar to the one proposed by @Zeitounator:
- set_fact:
first_missing: >-
file.diff.0.after.split()
| difference(file.diff.0.before.split())
| first
You can do this a much simpler way IMO with a very simple set of filters:
file lookup. If the file is remote, fetch it first from the target or slurp its content into a varsplit the content from the file on the new line char to obtain a listint function to each list element to transform the parsed strings to integersdifference filter against the range of numbers you are looking forSimply put as code taking for granted your file is locally available in target.txt
- name: find first missing number
debug:
msg: "{{ range(100, 111) | list | difference(lookup('file', 'target.txt').split('\n') | map('int')) | sort | first }}"
If your original list can contain values outside of the given explored range, you can simply reject the values out-of-range prior to selecting the first result
- name: find first missing number
vars:
first: 100
last: 110
my_range: "{{ range(first, last+1) | list }}"
my_numbers: "{{ lookup('file', 'target.txt').split('\n') | map('int') }}"
my_diff: "{{ my_range | difference(my_numbers) }}"
my_diff_in_range: "{{ my_diff | reject('<', first) | reject('>', last) }}"
my_result: "{{ my_diff_in_range | sort | first }}"
debug:
var: my_result
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With