default(omit)したいのに空文字が定義されてしまうケースをternaryで解決する

2021/05/31追記

ternaryフィルターを使わなくても、defaultフィルターの標準機能でやりたいことが実現できました。defaultフィルターの引数にtrueを指定することで、対象の変数がfalseと評価される場合にdefaultを適用することができます。

以下、サンプルコードと実行結果。

---
- hosts: localhost
  gather_facts: false
  connection: local
  vars:
    foo: ""
  tasks:
    - name: debug
      debug:
        msg: "{{ item }}"
      loop:
        - "{{ foo | bool }}"
        - "{{ foo | default(omit) }}"
        - "{{ foo | default(omit,true) }}"
PLAY [localhost] **********************************************************************************************************************************************************************

TASK [debug] **************************************************************************************************************************************************************************
ok: [localhost] => (item=False) =>
  msg: false
ok: [localhost] => (item=) =>
  msg: ''
ok: [localhost] => (item=__omit_place_holder__34382e50c55cdbf84ac5c85dfb41cba3629ba8f8) =>
  msg: Hello world!

PLAY RECAP ****************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

参考

docs.ansible.com

jinja.palletsprojects.com

以下蛇足。

はじめに

defaultフィルターを使用することで、変数が定義されていない場合のデフォルト値を指定できます。さらに、デフォルト値にomitを指定することで、パラメータそのものを省略することができます。

ただし、default(omit)で省略されるのは、変数が未定義であった場合です。例えば、read_csvモジュールで空のフィールドを含むCSVファイルを読み込んで、debugモジュールで出力した場合、下記のようになります。

path,mode
/tmp/foo,
/tmp/bar,
/tmp/baz,0444
ok: [localhost] => {
    "res_csv": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/libexec/platform-python"
        },
        "changed": false,
        "dict": {},
        "failed": false,
        "list": [
            {
                "mode": "",
                "path": "/tmp/foo"
            },
            {
                "mode": "",
                "path": "/tmp/bar"
            },
            {
                "mode": "0444",
                "path": "/tmp/baz"
            }
        ]
    }
}

CSVファイル上、/tmp/foo/tmp/barmodeは空ですが、変数としては空文字""が定義されています。したがって、defaultフィルタは動作しません。上記のケースで空文字""omitするため、ternaryフィルタを利用します。

Playbook

- hosts: localhost
  gather_facts: false
  connection: local
  tasks:
    - name: Read CSV file
      community.general.read_csv:
        path: file.csv
      register: res_csv

    - name: Touch files with an optional mode
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: touch
        mode: "{{ (item.mode == '') | ternary(omit, item.mode) }}"
      loop: "{{ res_csv.list }}"
      register: res_file

    - name: Debug res_file
      ansible.builtin.debug:
        msg: "{{ item.invocation.module_args }}"
      loop: "{{ res_file.results }}"
      loop_control:
        label: "{{ item.invocation.module_args.path }}"

実行結果

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

TASK [Read CSV file] *************************************************************************************************************************
ok: [localhost]

TASK [Touch files with an optional mode] *****************************************************************************************************
changed: [localhost] => (item={'path': '/tmp/foo', 'mode': ''})
changed: [localhost] => (item={'path': '/tmp/bar', 'mode': ''})
changed: [localhost] => (item={'path': '/tmp/baz', 'mode': '0444'})

TASK [Debug res_file] ************************************************************************************************************************
ok: [localhost] => (item=/tmp/foo) => {
    "msg": {
        "_diff_peek": null,
        "_original_basename": null,
        "access_time": null,
        "access_time_format": "%Y%m%d%H%M.%S",
        "attributes": null,
        "follow": true,
        "force": false,
        "group": null,
        "mode": null,               ★
        "modification_time": null,
        "modification_time_format": "%Y%m%d%H%M.%S",
        "owner": null,
        "path": "/tmp/foo",
        "recurse": false,
        "selevel": null,
        "serole": null,
        "setype": null,
        "seuser": null,
        "src": null,
        "state": "touch",
        "unsafe_writes": false
    }
}
ok: [localhost] => (item=/tmp/bar) => {
    "msg": {
        "_diff_peek": null,
        "_original_basename": null,
        "access_time": null,
        "access_time_format": "%Y%m%d%H%M.%S",
        "attributes": null,
        "follow": true,
        "force": false,
        "group": null,
        "mode": null,               ★
        "modification_time": null,
        "modification_time_format": "%Y%m%d%H%M.%S",
        "owner": null,
        "path": "/tmp/bar",
        "recurse": false,
        "selevel": null,
        "serole": null,
        "setype": null,
        "seuser": null,
        "src": null,
        "state": "touch",
        "unsafe_writes": false
    }
}
ok: [localhost] => (item=/tmp/baz) => {
    "msg": {
        "_diff_peek": null,
        "_original_basename": null,
        "access_time": null,
        "access_time_format": "%Y%m%d%H%M.%S",
        "attributes": null,
        "follow": true,
        "force": false,
        "group": null,
        "mode": "0444",             ★
        "modification_time": null,
        "modification_time_format": "%Y%m%d%H%M.%S",
        "owner": null,
        "path": "/tmp/baz",
        "recurse": false,
        "selevel": null,
        "serole": null,
        "setype": null,
        "seuser": null,
        "src": null,
        "state": "touch",
        "unsafe_writes": false
    }
}

PLAY RECAP ***********************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

実行結果のmodule_argsを見ると、/tmp/foo/tmp/barmodeが、""でなくnullになっています。これは、ternaryフィルタで評価した結果、omitが利いていることを示します。ぶっちゃけfileモジュールはmodeパラメータの指定が""でも動作しますが、空文字の指定が許されないパラメータを持つモジュールでは有用なシーンがあります。

参考

docs.ansible.com