cloud_hack
cloud_hack

Reputation: 63

Ansible: Transforming list to dictionary with items2dict

Given the following list:

fruits:
  - fruit: apple
    color: red
    texture: crunchy
    shape: round
  - fruit: grapefruit
    color: yellow
    taste: sour
  - fruit: pear
    color: yellow

How would I use the items2dict filter to change to a dictionary (below)? Problem is there is multiple, and a variable amount of values.

"{{ fruits | items2dict(key_name='fruit', value_name='??????') }}

Desired result:

fruits:
  - apple:
      color: red
      texture: crunchy
      shape: round
  - grapefruit:
      color: yellow
      taste: sour
  - pear:
      color: yellow

I can't seem to find how here: https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#transforming-lists-into-dictionaries

Upvotes: 5

Views: 7545

Answers (3)

Vladimir Botka
Vladimir Botka

Reputation: 68074

The task below does the job

    - set_fact:
        fruits2: "{{ fruits2|default([]) + [{ item['fruit']: value }]}}"
      loop: "{{ fruits }}"
      vars:
        keys: "{{ item.keys()|difference(['fruit']) }}"
        vals: "{{ keys|map('extract', item)|list }}"
        value: "{{ dict(keys|zip(vals)) }}"

gives

  fruits2:
  - apple:
      color: red
      shape: round
      texture: crunchy
  - grapefruit:
      color: yellow
      taste: sour
  - pear:
      color: yellow

A dictionary, instead of a list, can be created by simply changing the concatenation of the lists to the combination of the dictionaries e.g.

    - set_fact:
        fruits3: "{{ fruits3|default({})|combine({ item['fruit']: value }) }}"
      loop: "{{ fruits }}"
      vars:
        keys: "{{ item.keys()|difference(['fruit']) }}"
        vals: "{{ keys|map('extract', item)|list }}"
        value: "{{ dict(keys|zip(vals)) }}"

gives

  fruits3:
    apple:
      color: red
      shape: round
      texture: crunchy
    grapefruit:
      color: yellow
      taste: sour
    pear:
      color: yellow

items2dict can be used to select 2 attributes only (key_name and value_name) e.g.

    - set_fact:
        fruits4: "{{ fruits|items2dict(key_name='fruit', value_name='color') }}"

gives

  fruits4:
    apple: red
    grapefruit: yellow
    pear: yellow

A list (desired result) can't be created by items2dict. items2dict returns a dictionary.


items2dict will fail if the attribute is missing e.g.

    - set_fact:
        fruits5: "{{ fruits|items2dict(key_name='fruit', value_name='taste') }}"

fails

The error was: KeyError: 'taste'

Upvotes: 1

MarianD
MarianD

Reputation: 14141

The items2dict() function is not appropriate for your case, but you may use this simple approach instead:

rows = """\
fruits:
  - fruit: apple
    color: red
    texture: crunchy
    shape: round
  - fruit: grapefruit
    color: yellow
    taste: sour
  - fruit: pear
    color: yellow"""

rows_changed = "\n".join([2*" " + row if row.startswith(4*" ") else row 
                             for row in rows.split("\n")])
print(rows_changed)

The explanation:

  1. rows.split("\n") creates a list of rows from the rows multi-line string,
  2. the expression 2*" " + row if row.startswith(4*" ") else row within [] (the list comprehension) then indents rows starting with 4-space indent by another 2-space indent,
  3. "\n".join() convert that (modified) list of rows back to the multi-line string.

The output:

fruits:
  - fruit: apple
      color: red
      texture: crunchy
      shape: round
  - fruit: grapefruit
      color: yellow
      taste: sour
  - fruit: pear
      color: yellow

Upvotes: 0

Maroun
Maroun

Reputation: 95968

I think you can do something like:

- name: bla
  set_fact:
    res: "{{ res | default({}) | combine({ item['fruit']: item }) }}"
  loop: "{{ fruits }}"

Upvotes: 4

Related Questions