Home > Mobile >  Ansible: split a dictionary with list values to a list of dictionaries with a single item from the l
Ansible: split a dictionary with list values to a list of dictionaries with a single item from the l

Time:07-10

I need to convert a dictionary with list values into a list of dictionaries.

Given:

my_dict:
  key1: ["111", "222"]
  key2: ["444", "555"]

Desired output:

my_list:
 - key1: "111"
   key2: "444"
 - key1: "222"
   key2: "555"

What I've tried:

  - set_fact:
      my_list: "{{ my_list | default([])   [{item.0.key: item.1}] }}"
    loop: "{{ my_dict | dict2items | subelements('value') }}"

And what I've got:

[
            {
                "key1": "111"
            },
            {
                "key1": "222"
            },
            {
                "key2": "444"
            },
            {
                "key2": "555"
            } 
        ]

Thankful for any help and suggestions!

CodePudding user response:

Get the keys and values of the dictionary first

keys: "{{ my_dict.keys()|list }}"
vals: "{{ my_dict.values()|list }}"

gives

keys: [key1, key2]
vals:
  - ['111', '222']
  - ['444', '555']

Transpose the values

    - set_fact:
        tvals: "{{ tvals|d(vals.0)|zip(item)|map('flatten') }}"
      loop: "{{ vals[1:] }}"

gives

tvals:
  - ['111', '444']
  - ['222', '555']

Create the list of the dictionaries

my_list: "{{ tvals|map('zip', keys)|
                   map('map', 'reverse')|
                   map('community.general.dict')|
                   list }}"

gives

my_list:
  - key1: '111'
    key2: '444'
  - key1: '222'
    key2: '555'

Notes

  1. Example of a complete playbook
- hosts: localhost
  vars:
    my_dict:
      key1: ["111", "222"]
      key2: ["444", "555"]
    keys: "{{ my_dict.keys()|list }}"
    vals: "{{ my_dict.values()|list }}"
    my_list: "{{ tvals|map('zip', keys)|
                       map('map', 'reverse')|
                       map('community.general.dict')|
                       list }}"
  tasks:
    - set_fact:
        tvals: "{{ tvals|d(vals.0)|zip(item)|map('flatten') }}"
      loop: "{{ vals[1:] }}"
    - debug:
        var: my_list
  1. You can use a custom filer to transpose the matrix. For example,
shell> cat filter_plugins/numpy.py 
# All rights reserved (c) 2022, Vladimir Botka <[email protected]>
# Simplified BSD License, https://opensource.org/licenses/BSD-2-Clause

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.errors import AnsibleFilterError
from ansible.module_utils.common._collections_compat import Sequence

import json
import numpy


def numpy_transpose(arr):
    if not isinstance(arr, Sequence):
        raise AnsibleFilterError('First argument for numpy_transpose must be list. %s is %s' %
                                 (arr, type(arr)))
    arr1 = numpy.array(arr)
    arr2 = arr1.transpose()
    return json.dumps(arr2.tolist())


class FilterModule(object):
    ''' Ansible wrappers for Python NumPy methods '''

    def filters(self):
        return {
            'numpy_transpose': numpy_transpose,
        }

Then you can avoid iteration. For example, the playbook below gives the same result

- hosts: localhost
  vars:
    my_dict:
      key1: ["111", "222"]
      key2: ["444", "555"]
    keys: "{{ my_dict.keys()|list }}"
    vals: "{{ my_dict.values()|list }}"
    tvals: "{{ vals|numpy_transpose()|from_yaml }}"
    my_list: "{{ tvals|map('zip', keys)|
                       map('map', 'reverse')|
                       map('community.general.dict')|
                       list }}"
  tasks:
    - debug:
        var: my_list
  1. Transposing explained

Let's start with the matrix 2x2

vals:
  - ['111', '222']
  - ['444', '555']

The task below

    - set_fact:
        tvals: "{{ tvals|d(vals.0)|zip(item) }}"
      loop: "{{ vals[1:] }}"

gives step by step:

a) Before the iteration starts the variable tvals is assigned the default value vals.0

vals.0: ['111', '222']

b) The task iterates the list vals[1:]. These are all lines in the array except the first one

vals[1:]:
  - ['444', '555']

c) The first, and the only one, iteration zip the first and the second line. This is the result

vals.0|zip(vals.1):
  - ['111', '444']
  - ['222', '555']

Let's proceed with matrix 3x3

vals:
  - ['111', '222', '333']
  - ['444', '555', '666']
  - ['777', '888', '999']

The task below

    - set_fact:
        tvals: "{{ tvals|d(vals.0)|zip(item)|map('flatten') }}"
      loop: "{{ vals[1:] }}"

gives step by step:

a) Before the iteration starts the variable tvals is assigned the default value vals.0

vals.0: ['111', '222', '333']

b) The task iterates the list vals[1:]

vals[1:]:
  - ['444', '555', '666']
  - ['777', '888', '999']

c) The first iteration zip the first and the second line, and assigns it to tvals. The filer flatten has no effect on the lines

vals.0|zip(vals.1)|map('flatten'):
  - ['111', '444']
  - ['222', '555']
  - ['333', '666']

d) The next iteration zip tvals and the third line

  tvals|zip(vals.2):
    - - ['111', '444']
      - '777'
    - - ['222', '555']
      - '888'
    - - ['333', '666']
      - '999

e) The lines must be flattened. This is the result

tvals|zip(vals.2)|map('flatten'):
  - ['111', '444', '777']
  - ['222', '555', '888']
  - ['333', '666', '999']
  • Related