The logical filter adds some "if" / "elif" / "else" logical capabilities to your Ansible data.
(Also "and", "or", "xor", "not", and data promotions; see below.)
Usage: "{{ data | logical }}"
The dict keys "if", "elif", and "else" must be the only dict keys at their level. They
must also be list elements – i.e. "- if:" rather than simply "if:" – because order
matters. You can have arbitrarily many sets of if [ elif ... ][ else ] in a list.
Each "if" and "elif" (though not "else") must contain a list, the first element of which
is a boolean. These initial booleans are removed as the "if" and "elif" are evaluated,
and the remaining elements of the "winning" "if", "elif", or "else" replace the "if" in
its list. Example:
---
- name: demo01
hosts: localhost
gather_facts: false
vars:
snowflake_1: "{{ [ True, False] | random }}"
snowflake_2: "{{ [ True, False] | random }}"
data_before:
my_color:
- if:
- "{{ snowflake_1 }}"
- purple:
- the new green
- tastes like chicken
- elif:
- "{{ snowflake_2 }}"
- Purple Rain
- else:
Green
data_after: "{{ data_before | logical }}"
tasks:
- name: A list
copy:
content: "{{ {'data_after': data_after} |
to_nice_yaml(indent=2, width=1337) }}"
dest: "/tmp/{{ ansible_play_name }}.txt"The resulting /tmp/{{ ansible_play_name }}.txt after subsequent runs of the playbook
above may contain:
data_after:
my_color:
- purple:
- the new green
- tastes like chickenor
data_after:
my_color:
- Purple Rainor
data_after:
my_color:
- GreenThe "and", "or", and "xor" dict keys must be the lone keys in their respective
dicts. They replace themselves with their subordinate contents after evaluation. The
immediate subordinates of "and", "or", and "xor" can be a flat
or nested list of booleans. Subordinate lists are evaluated first, then the corresponding
operator is evaluated and replaced by the resulting boolean.
- "
and" is true if all its immediate subordinates are true. - "
or" is true if any of its immediate subordinates is true. - "
xor" is true if exactly one of its immediate subordinates is true and any others is false.
They do not have to appear in the context of an "if"/"elif"/"else" construct.
data_before:
- if:
- and:
- true
- or:
- true
- false
- xor:
- true
- false
- false
- not: false # See below.
- Truth: is beauty
- else:
This should not happen
data_after: "{{ data_before | logical }}"results in
data_after:
- Truth: is beautyJust like "if", "elif", "else", "and", "or", and "xor", "not" must be a lone dict key.
However, the "not" dict key does not require being in a list. It flips the values of all
subordinated booleans, then removes itself from the dict, effectively promoting up one
level all the data below it.
data_before:
alpha:
not:
beta:
- epsilon
- True
gamma:
- zeta
- - True
- False
delta:
- True
- False
- Trueafter passing through the logical filter becomes
data_after:
alpha:
beta:
- epsilon
- false
gamma:
- zeta
- - false
- true
delta:
- false
- true
- falseBecause "if" and friends need to be list elements (to preserve order), and because their
evaluated subordinate data replace the "if" in the resulting data, it follows that they can
only produce lists. But sometimes you want to produce dicts. The "<<" promotion prefix
addresses this issue.
Consider the following playbook and its output.
---
- name: goldilocks
hosts: localhost
gather_facts: False
vars:
basename: goldilocks
soup: "{{ ['too hot', 'too cold','just right'] | random() }}"
chair: "{{ ['too hard','too soft','just right'] | random() }}"
bed: "{{ ['too hard','too soft','just right' ] | random() }}" # That extra space is necessary!
data:
soup:
is: "{{ soup }}"
owner:
- if: ["{{ soup == 'too hot' }}","Papa Bear"]
- elif: ["{{ soup == 'too cold' }}","Mama Bear"]
- else: "Baby Bear"
bed:
is: "{{ bed }}"
owner:
- if: ["{{ bed == 'too hard' }}","Papa Bear"]
- elif: ["{{ bed == 'too soft' }}","Mama Bear"]
- else: "Baby Bear"
chair:
is: "{{ chair }}"
owner:
- if: ["{{ chair == 'too hard' }}","Papa Bear"]
- elif: ["{{ chair == 'too soft' }}","Mama Bear"]
- else: "Baby Bear"
home: "{{ data | logical }}"
tasks:
- name: The Bear's Home
copy:
content: |
{{ { 'before' : data } | to_nice_yaml(indent=2, width=1337) }}
{{ { 'after' : home } | to_nice_yaml(indent=2, width=1337) }}
dest: /tmp/{{basename}}.txtWhat We Get:
after:
bed:
is: too hard
owner:
- Papa Bear
chair:
is: too hard
owner:
- Papa Bear
soup:
is: too cold
owner:
- Mama BearAfter applying the logical filter, the "owner"s will have become single
element lists. We'd like to promote the "owner" values up a level.
What We Want:
after:
bed:
is: too hard
owner: Papa Bear
chair:
is: too hard
owner: Papa Bear
soup:
is: too cold
owner: Mama BearWe can do this by adding some "<<" promotion prefixes to selected dict keys.
A promotion prefix is "<<" followed by an optional modifier:
- "
<<" — (no modifier) pre-existing matching keys are replaced by promoted keys. - "
<<|" — pre-existing and promoted matching keys are deep merged, lists are joined and flattened. - "
<<-" — pre-existing and promoted matching keys are deep merged, lists are joined, flattened, and uniqified.
Here's our example again with promotion prefixes applied to the "owner" keys:
data:
soup:
is: "{{ soup }}"
<<owner:
- if: ["{{ soup == 'too hot' }}","Papa Bear"]
- elif: ["{{ soup == 'too cold' }}","Mama Bear"]
- else: "Baby Bear"
bed:
is: "{{ bed }}"
<<owner:
- if: ["{{ bed == 'too hard' }}","Papa Bear"]
- elif: ["{{ bed == 'too soft' }}","Mama Bear"]
- else: "Baby Bear"
chair:
is: "{{ chair }}"
<<owner:
- if: ["{{ chair == 'too hard' }}","Papa Bear"]
- elif: ["{{ chair == 'too soft' }}","Mama Bear"]
- else: "Baby Bear"
home: "{{ data | logical }}"With the desired result:
after:
bed:
is: too hard
owner: Papa Bear
chair:
is: too hard
owner: Papa Bear
soup:
is: just right
owner: Baby BearExactly how promotion works is subtle. Order matters, and understanding that order can avoid surprises.
-
The "
<<promotion" key is removed from its containing dict, and its contents ("tmp" below) are held for examination.input output workspace a: alpha<<b: (whatever)→tmp: (whatever)c: charlie -
If "
tmp" is not a list, make it one.:|
tmp: ... → tmp: [...]| -
Examine each of the elements of the "
tmp" list. If the examined element is a dict, each of its top-level keys and values move up to the same level as the original "<<promotion" key. NOTE: if an identically named key already exists at that level, the subordinate key and its value either (1) replaces the pre-existing key at the "<<promotion" key's original level (in the case of "<<"), or (2) the subordinate data are merged (in the case of "<<|" or "<<-") with the data of the pre-existing key.input output workspace a: alphaa: alpha<<b:- b1: barb1: bar← tmp: { b1: bar }b1: gonerb1: gonerc: charliec: charlie -
If there are any elements left in the "
tmp" list, (i.e. elements which weren't promoted dicts), and the "<<promotion" key had any characters to the right of "<<", then "tmp" is added back as a value for the "<<promotion" key at the same level as the original "<<promotion" key and with the same key name but with the "<<" removed. If an identically named key already exists at that level, then the promotion key and its value either (1) replace that pre-existing key at the "<<promotion" key's original level (in the "<<" case), or (2) the subordinate data are merged (in the "<<|" or "<<-"cases ) with that of the pre-existing key. If only one element remains in the list, then that element becomes the value for the new key rather than the single element list.input output workspace a: alphaa: alpha<<b:b: keeper← tmp: [ keeper ]- keeper- b1: barb1: bar← tmp: { b1: bar }b1: gonerb1: goner<<c:c:- super- super- supper- supper- c1: pepperc1: pepper← tmp: { c1: pepper }d: charlied: charlie
The result of an - if: is always a list, even if it has a single element. If that
single element is a dict that you want in place of the list produced by the - if:, you
may have to insert a sacrificial dummy "<<promotion" key above the - if: block.
You may also use a sacrificial dummy "<<promotion" key to eliminate empty lists
resulting from logical operations. For example:
promo_no: # Without promotions
aa: alpha
bb:
- if:
- true
- tigers:
- love cheese
- else:
- tigers:
- have fleas
cc: charlie
dd:
- if:
- false
- lose_this:
- lost luggage
promo_si_1: # With some promotions
aa: alpha
<<bb:
- if:
- true
- tigers:
- love cheese
- else:
- tigers:
- have fleas
cc: charlie
<<dd:
- if:
- false
- lose_this:
- lost luggage
promo_si_2: # With additional promotion
aa: alpha
<<bb:
- if:
- true
- <<tigers:
- love cheese
- else:
- <<tigers:
- have fleas
cc: charlie
<<dd:
- if:
- false
- <<lose_this:
- lost luggageResults after applying the logic filter:
promo_no:
aa: alpha
bb:
- tigers:
- love cheese
cc: charlie
dd: []
promo_si_1:
aa: alpha
tigers:
- love cheese
cc: charlie
promo_si_2:
aa: alpha
tigers: love cheese
cc: charlieIn one more example, we'll explore the differences between following "else" with a
scalar (string), single and multi-element lists, and dicts. These results would be
exactly the same following "if" or "elif", except that, unlike "else", "if" and
"elif" must always be followed by a list.
---
# Original input
promo_no:
aa:
scalar, list, dict, dict-in-list
after else sans promotion
bb:
- if: [ false, false ]
- else: tiger0 as scalar; nothing else under bb
cc:
- if: [ false, false ]
- else:
tiger1 as scalar
- if: [ false, false ]
- else:
- tiger2 in single element list
- if: [ false, false ]
- else:
- tiger3a in multi element list
- tiger3b in multi element list
- if: [ false, false ]
- else:
tiger4: bare dict
- if: [ false, false ]
- else:
- tiger5: dict in single element list
- if: [ false, false ]
- else:
- tiger6a: first dict in multi element list
- tiger6b: second dict in multi element list
dd: delta
promo_si:
aa:
scalar, list, dict, dict-in-list
after else with promotion
<<bb:
- if: [ false, false ]
- else: tiger0 as scalar; nothing else under bb
<<cc:
- if: [ false, false ]
- else:
tiger1 as scalar
- if: [ false, false ]
- else:
- tiger2 in single element list
- if: [ false, false ]
- else:
- tiger3a in multi element list
- tiger3b in multi element list
- if: [ false, false ]
- else:
tiger4: bare dict
- if: [ false, false ]
- else:
- tiger5: dict in single element list
- if: [ false, false ]
- else:
- tiger6a: first dict in multi element list
- tiger6b: second dict in multi element list
dd: deltaResults:
---
# Without promption
promo_no:
aa: scalar, list, dict, dict-in-list after else sans promotion
bb:
- tiger0 as scalar; nothing else under bb
cc:
- tiger1 as scalar
- tiger2 in single element list
- tiger3a in multi element list
- tiger3b in multi element list
- tiger4: bare dict
- tiger5: dict in single element list
- tiger6a: first dict in multi element list
- tiger6b: second dict in multi element list
dd: delta# With promption of bb and cc
promo_si:
aa: scalar, list, dict, dict-in-list after else with promotion
bb: tiger0 as scalar; nothing else under bb
cc:
- tiger1 as scalar
- tiger2 in single element list
- tiger3a in multi element list
- tiger3b in multi element list
dd: delta
tiger4: bare dict
tiger5: dict in single element list
tiger6a: first dict in multi element list
tiger6b: second dict in multi element list