`undef()` cannot be used on dictionary members

undef() behaves unexpectedly when set to a dictionary member. I expected the following code to ensure that more or less foo == {}.

- hosts: localhost
  gather_facts: no
  vars:
    foo:
      bar: baz
  tasks:
    - vars:
        foo:
          bar: "{{ undef() }}"
      debug:
        var: foo

gives:

PLAY [localhost] ******************

TASK [debug] *****************
ok: [localhost] => {
    "foo": "VARIABLE IS NOT DEFINED!"
}

Is this supposed to happen? The documentation does not help in this regard, and I have found no mention of the function in the docs of jinja2.

This is the expected behaviour. You have only partially defined the dictionary (at least one value is undefined, because you have explicitly set it to an undefined value) so it is treated as undefined

If, as you’ve stated, you want foo to be equal to {}, you should just do foo: {}.

1 Like

I see. In this case there is a trivial alternative as you point out, but not when I’m trying to undefine a particular member of a dictionary.

Is there any expression in place of ??? that would unset foo.k2, but leave any other keys intact?

- hosts: localhost
  gather_facts: no
  vars:
    foo:
      k1: v1
      k2: v2
  tasks:
    - vars:
        foo: "{{ ??? }}" # should unset foo.k2
      debug:
        var: foo

Being explicit about which keys to carry over does not work in the obvious way, because of lazy evaluation:

- vars:
    foo:
      k1: "{{ foo.k1 }}" # recursion error

I think this filter would do what you want? ansible.utils.keep_keys filter – Keep specific keys from a data recursively. — Ansible Community Documentation

Thanks. First time encountering that filter, but applying it in the obvious way would result in a recursion error:

- hosts: localhost
  gather_facts: no
  vars:
    foo:
      k1: v1
      k2: v2
  tasks:
    - vars:
        foo: "{{ foo | ansible.utils.keep_keys(target=['k1']) }}"
      debug:
        var: foo # recursion error

I think working around the recursion depends on the rest of your playbook/use case. Heres one solution

- hosts: localhost
  gather_facts: no
  vars:
    _foo:
      k1: v1
      k2: v2
    foo: "{{ _foo }}"
  tasks:
    - vars:
        foo: "{{ _foo | ansible.utils.keep_keys(target=['k1']) }}"
      debug:
        var: foo

Perhaps this or evaluating something using set_fact to reassign the actual value will get you out of most situations, but it feels tedious. But I suppose that’s more about how to elegantly work around the (to me unexpected) properties of lazy evaluation, which is for another topic. Thanks.

This isn’t an issue with lazy variable evaluation, you just cannot refer to a variable to define itself. Use the same variable name to fully override variables defined with lesser precedence, otherwise you should use intermediate var(s).

- hosts: localhost
  gather_facts: no
  vars:
    foo: "{{ _foo | dict2items | rejectattr('key', 'in', foo_exclude) | items2dict }}"
    _foo:
      k1: v1
      k2: v2
    foo_exclude: []
  tasks:
    - debug: msg="{{ foo }}"

    - debug: msg="{{ foo }}"
      vars:
        foo_exclude: [k2]
1 Like

I suppose that’s right, and I was mistaking expressions like (JavaScript)

foo = foo; // works

which works just fine if foo has been defined, for definitions like (again JavaScript):

let foo = foo; // does not work

which will not work.

What I’m after is a way to define a var called foo in terms of whatever foo happens to be in the surrounding scope, which can happen if some role I want to use happens to have a parameter with a name that is already used. Intermediate facts are the most reliable way to do this I suppose.

But if not for lazy evaluation, there seems to be no reason to require the mention of foo in foo: "{{ foo }}" to refer to the variable being defined, as opposed to the value it has at that point. Although this makes me wonder why JavaScript doesn’t work that way.

I think that is a common misconception of the way templating happens. Variable precedence happens before templating, and the variable precedence maintains copies of the var at all different levels it is defined, and precedence does not preserve any context about lower levels of precedence.

So defining foo: "{{ foo }}" at any level, is a self referential assignment, not referencing lower precedence levels.

Effectively we just go through the precedence hierarchy, and merge keys over top of each other, then we pass that on to templating after the merging.

3 Likes

Thanks for clarifying. Sounds like something that may be hard to change, though it seems it would be worthwhile otherwise. I’d love to be able to treat roles like functions, and this is one of the ways the analogy breaks; defining role vars is not like passing arguments to a function in an important way.

I did find a way to wrap include_role in a plugin that eagerly evaluates its parameters and assigns the one called input to include_role’s vars, which I believe gets me what I want. I have some reservations about the particular way I went about wrapping include_role, but I guess the more important question is what I’m giving up by using eager evaluation. I’m thinking the trade-off might be a simpler mental model for less static analysis and more overhead. I suppose I’m stretching this topic a little, but any comments on this would be much appreciated.