配列や、辞書の配列の中から特定の条件の要素を抜き出す。
配列操作はselect()
で要素に対してフィルター処理を行い、辞書操作はselectattr()
でkey指定することでvalueにフィルター処理を行う。
ここでいうフィルター処理は、AnsibleやJinja2のフィルターのことではなく、AnsibleやJinja2のTest(被演算子/operatorと言うと個人的にはわかりやすいんだけど、「被演算子」という表記は特に無く、Jinja2だとis
を使った被演算子の方で関数的に使用)を使って要素をフィルタリングすることを指す。
Ansibleで変数名 is HogeHoge(...)
という書式で使われているHogeHoge()
の部分。
これをselect()
やselectattr()
で使って配列・辞書に対してまとめて処理可能になっている (はず)。
select()
とselectattr()
は、どちらもJinja2テンプレートのサイトにBuiltin Filtersとして記載があるのでそちらを参照。
この記事では、以下のバージョンを前提としている。
配列 (select)
指定したTestの演算結果がtrueの場合にその要素を返す。
って書くとなんか分かりづらいけど、要は条件に合致した配列要素の場合のみ、その要素を返す。
数値 (一致・大小)
比較演算子が使える。
vars: integer_values: - 0 - 1 - 2 - 3 - 4 - name: select integer values debug: msg: - '{{ integer_values | select("eq", 2) }}' # 2と同値か => [2] - '{{ integer_values | select("==", 2) }}' # "eq"と同じ - '{{ integer_values | select("le", 3) }}' # 3以下か => [0,1,2,3] - '{{ integer_values | select("<=", 3) }}' # "le"と同じ - '{{ integer_values | select("lt", 3) }}' # 3未満か => [0,1,2] - '{{ integer_values | select("<", 3) }}' # "lt"と同じ
文字列 (一致・大小)
数値の場合と同様、eq
または==
で一致した場合。
(intは==
、strはeq
みたいに、どっちかに統一して欲しかったな、これ。。)
vars: hosts: - web1 - web2 - web3 - db1 - db2 tasks: - name: split filter sample debug: msg: - '{{ hosts | select("eq", "web1") }}' # web1が取れる
文字列に対してはあまり使う頻度は無いと思うが、gt
/>
, ge
/>=
, lt
/<
, le
/<=
も使える。
正規表現
match
、search
を使った正規表現も使える。
正規表現はJinja2のTestでなく、Ansibleで用意されている。
処理内容はPythonのPattern#match()
とPattern#search()
となっている。
vars: hosts: - web1 - web2 - web3 - db1 - db2 - web1 - dns tasks: - name: split filter sample debug: msg: - '{{ hosts | select("match", "web") }}' # webにmatchするもの - '{{ hosts | select("match", "b") }}' # bが含まれるもの…と思いきやre.match("b")っぽい。bで始まるもののみ - '{{ hosts | select("search", "b") }}' # bがどこかに含まれるもの re.search("b")っぽい - '{{ hosts | select("regex", "b") }}' # デフォルトでは"search"と同じように動作。
また、regex
もあり、これはmatch_type
という名前付き引数を指定することで、Pythonのre
で使用するメソッドを指定できる。
'{{ hosts | select("regex", "d.*\d+", match_type="fullmatch") }}' # match_typeでメソッド指定
型でフィルタリング
配列要素として数値や文字列・booleanなどバラバラの型の値が入っている場合に、型でフィルタリングもできる。
型名の文字列のみを引数に指定する。
type_mix: - 1 - 12 - a - ab - curry - 755 - true - false - 3.14 - "true" - "false" - "123" - "3.14" - - subitem1 - subitem2 - key: 1 value: 2 - name: select values type debug: msg: - '{{ type_mix | select("string") }}' # curry, 'trye', ... - '{{ type_mix | select("integer") }}' # 755 - '{{ type_mix | select("number") }}' # 755, true, false, 3.14 - '{{ type_mix | select("boolean") }}' # true, false - '{{ type_mix | select("float") }}' # 3.14 - '{{ type_mix | select("sequence") }}' # リストだけではなく、文字列・リスト・辞書が取れる - '{{ type_mix | select("mapping") }}' # {key, value}
sequence
のみ思ってたのと違った。。
(iterateとして処理できるとtrueになるため、辞書だけでなく文字列もヒットする)
また、number
はboolean値もヒットするので注意。
型というより、数値として処理できる変数であれば対象という感じ?(未確認)
ファイルパス
実行ホスト上に指定されたパスが存在するかどうかでフィルタリングできる。
- name: file path test vars: path_list: - / - /bin - /usr/bin/perl - /usr/bin/python3 - /not-found-path debug: msg: - '{{ path_list | select("directory") }}' # /, /bin - '{{ path_list | select("file") }}' # perl, python3 - '{{ path_list | select("link") }}' # /bin, python3 - '{{ path_list | select("exists") }}' # /not-found-path 以外 - '{{ path_list | select("mount") }}' # /
バージョン文字列
「〇〇が未インストール、または、インストール済みでバージョン□□以下の場合」みたいなことができる。
- name: version sample vars: version_string: - 1.0.0 - 1.0.99999999999999999 - 1.1.0 - 1.1.1 - 1.01.0 - 1.01.1 - 1.9.0 - 1.10.0 - '1.0.1' - '1.1.1' - '1.1' debug: msg: - '{{ version_string | select("version", "1.1.0", "gt") }}' # ['1.1.1', '1.01.1', '1.9.0', '1.10.0', '1.1.1']
1.1.0
との比較で、1.a.0
を入力するとエラーになる。
fatal: [localhost]: FAILED! => msg: 'Version comparison: ''<'' not supported between instances of ''str'' and ''int'''
versonについてはPythonのドキュメントに2021.02.16時点で記載がなぜか抜けているため、詳細はソースを見るしかない?
StrictVersionにすると、<数字>.<数字>
のような形式のバージョン文字列で無いとエラーになる。
デフォルトのLooseVersionであればもう少しユルい書式でバージョン比較できる。
Ansibleで実装されているversoin()
/ version_compare()
についてはこちら。
また、テスト(Test Pluginでなくソフトウェアテストの方のテスト)の内容についてはこちら
PythonのVersion()
は別途もうちょっと調べたいところ。。
含む (in)
Pythonのin
演算子と同じで、別途用意したリスト内に含まれる要素かどうかを、入力としてのリストの全要素に対して検査する。
vars: hosts: - web1 - web2 - web3 - db1 - db2 - dns contain_list: - db1 - web1 - mail1 tasks: - name: contain list debug: msg: '{{ hosts | select("in", contain_list) }}' # ['web1', 'db1']
in
と似たものにcontains
もあるが、配列に対するselect
フィルターに使用する場合、contains
の引数に一致する要素をフィルタリング…しそうだけどそうはならず、配列要素を更にリストへ分解してその要素に含まれていればtrueという判定となっている。(書いててよくわからん)
まぁ一致する要素を抜き出したければeq
を使えばいいので配列要素に対するcontains
はナンセンスのような気もする。(使い方の認識を誤ってる可能性もあるけど…)
下記の場合、"b"
を含む文字列であればtrueになる。
vars: contain_list: - db1 - web1 - mail1 tasks: - name: contain list debug: msg: - '{{ contain_list | select("contains", "b") }}' # ['db1', 'web1']
配列要素がiterableである必要があるため、数値の配列だと型エラーとなる。
動作する例としては以下のような感じ。(見やすいようにインラインリストにしている)
vars: list_in_list: - [1,2,3] - [4,5,6] - [7,8,9] tasks: - name: contain list debug: msg: - '{{ list_in_list | select("contains", 5) }}' # '[4,5,6]
辞書 (selectattr)
辞書の配列(辞書型の変数を要素に持つ配列)に対するselectattr()
は、「辞書のkey名とTest名を指定することで、そのvalueがtrueかどうか」という動作をする。
指定のTestを行い、結果がtrueの場合に要素を返すという機能自体はselect()
と同じ。
配列と若干異なる点としては「指定されたkeyが存在しない場合」に備えてdefined
を使ってガード処理を行ったり、配列の場合の動作がいまいちだったcontains
を使った「配列を要素として持つ場合に、その配列中に指定の要素があるか」などがある。
Test例は配列で一通りやってるので簡単に。。
以下のような、IPアドレスやパッケージ情報のホスト情報っぽい辞書型変数を要素に持つ配列を対象にselectattr()
を使ったフィルタリングを行ってみる。
- hosts: localhost gather_facts: no vars: hosts: - name: web1 addr: 192.168.2.10 mask: 255.255.255.0 pkg: - httpd - php - python - name: web2 addr: 192.168.2.11 mask: 255.255.255.0 pkg: - httpd - ruby - python - addr: 192.168.2.0 mask: 255.255.255.0 pkg: - php - name: db1 addr: 192.168.2.20 mask: 255.255.255.0 pkg: - mysql - postgres - php tasks: - name: selectattr filter sample debug: msg: - '{{ hosts | selectattr("name", "defined") }}' - '{{ hosts | selectattr("name", "defined") | selectattr("name", "eq", "web1") }}' - '{{ hosts | selectattr("name", "defined") | selectattr("name", "match", ".*1") }}' - '{{ hosts | selectattr("pkg", "contains", "php") }}'
キーが存在するか
'{{ hosts | selectattr("name", "defined") }}'
defined
で、指定のkeyが存在する要素のみを返す。
前述hosts
の例であれば、結果は以下の通り。
ok: [localhost] => msg: - - addr: 192.168.2.10 mask: 255.255.255.0 name: web1 pkg: - httpd - php - python - addr: 192.168.2.11 mask: 255.255.255.0 name: web2 pkg: - httpd - ruby - python - addr: 192.168.2.20 mask: 255.255.255.0 name: db1 pkg: - mysql - postgres - php
キーname
の値との一致
'{{ hosts | selectattr("name", "defined") | selectattr("name", "eq", "web1") }}'
前述のdefined
でname
が存在するもののみをフィルタリングした結果に対し、更にname
の値がweb1
のものを返す。
ok: [localhost] => msg: : : - - addr: 192.168.2.10 mask: 255.255.255.0 name: web1 pkg: - httpd - php - python
値に対する正規表現
'{{ hosts | selectattr("name", "defined") | selectattr("name", "match", ".*1") }}'
配列の場合と同様、match
やsearch
を使った正規表現も可能。
結果は以下の通り。
ok: [localhost] => msg: : : - - addr: 192.168.2.10 mask: 255.255.255.0 name: web1 pkg: - httpd - php - python - addr: 192.168.2.20 mask: 255.255.255.0 name: db1 pkg: - mysql - postgres - php
含む (contains)
'{{ hosts | selectattr("pkg", "contains", "php") }}'
指定keyの値がリスト形式の場合、その配列要素に指定した文字列や数値を含む場合の要素を返す。
例の場合、valueに配列を持つkey名pkg
に対して、その配列要素にphp
を含む場合のホスト情報がフィルタリングされる。
ok: [localhost] => msg: : : - - addr: 192.168.2.10 mask: 255.255.255.0 name: web1 pkg: - httpd - php - python - addr: 192.168.2.0 mask: 255.255.255.0 pkg: - php - addr: 192.168.2.20 mask: 255.255.255.0 name: db1 pkg: - mysql - postgres - php
含む (in)
(2021.07.15追記)
指定keyの値が文字列や数値などのスカラー値(ってAnsibleやPythonの文脈でいうの?)の場合で、比較対象として指定リストにその要素を含むか、という場合はin
が使える。
addrチェック用に、次のようなリストを準備して、
contain_list: - 192.168.2.20 - 192.168.2.10 - 192.168.0.89
以下の条件でフィルタを行うと、
'{{ hosts | selectattr("addr", "in", contain_list) }}'
このように、addr
の値がcontain_list
のリスト内に存在する値のみをフィルタリングできる。
ok: [localhost] => msg: - - addr: 192.168.2.10 mask: 255.255.255.0 name: web1 pkg: - httpd - php - python - addr: 192.168.2.20 mask: 255.255.255.0 name: db1 pkg: - mysql - postgres - php
以下過去の記事↓
入力が多重リストになってた。この比較はNGっぽい
前述のselect()
の例で使用したin
は、使い方が悪いのか空リストになった。
- name: selectattr filter sample vars: contain_list: - php - mysql debug: msg: - '{{ hosts | selectattr("pkg", "in", contain_list) }}'
↑の内容のplayで実行すると以下の通り。
ok: [localhost] => msg: - []
(おまけ) reject() / rejectattr()
条件に合致しない要素が欲しい場合は、逆の処理となるreject()
とrejectattr()
を使用する。
select()
の逆: reject(*args, **kwargs)selectattr()
の逆: rejectattr(*args, **kwargs)
見るべきドキュメント等
select() / selectattr() フィルター本体
ソースも確認すると理解が深まる。
実は中身はどちらも(さらに言うとreject()とrejectattr()も)select_or_reject()
をコールしてるのよね。
Jinja2 Tests
まずJinja2 Templateのページ。
ここの「List of Builtin Tests」のリストにあるTestsが使用できる。
一致不一致や型チェックなどの基本的なTestはここで用意されている。
ページ右下でバージョンを選べるが、これはAnsibleで使用しているPythonパッケージのJinja2のバージョンに合わせればよい。
Ansible Tests
Jinja2 TemplateのTest以外にAnsibleで内蔵されているTestもある。
Filterと同じくコレクション等のpluginリストにはないが、このページで一通り確認することができる。
Testsのソースはcallbackやlookupなどのほかのpluginと同様に、testというディレクトリ以下にある。
まとめ
selectattr()
+ map()
で指定キー配下の情報のみ抽出の例はよく見かけるけど、select()
とselectattr()
についての情報があまりなかった気がするのでまとめてみた。
どちらのFilterもAnsibleではなくJinja2の機能で、使用する主要なTest(被演算子)もJinja2のドキュメントに記載があるのがポイント。
また、「select()
・selectattr()
の引数で指定するのはFilterでなくTest」であることをおさえておけば、やりたいことを探しやすいと思う。(自分はここが曖昧だった)
※ 現状FilterとTestはCollection Indexから辿れず、AnsibleのFilterとTest、Jinja2のドキュメントを見るしかないと思う。
select()
・selectattr()
は、Perlだとgrep()
相当でPowerShellだとWhere-Object
相当かな。
今回は扱ってないmap
は、Perlのmap()
相当でPowerShellだとForEach-Object
相当のはず。
比較的単純なフィルタリングであればselect()
+selectattr()
で済ませて良いと思うが、抽出条件が複雑(深い位置にある値でフィルタリングして別要素の値を抜き出す、など)になってくると標準機能だけだとかえって分かりづらくなるため、json_query()
なんかを使った方が良かったりするケースも出てくるので、要件によってやり方は考慮した方がよい。
(疑問 追記あり) builtin 以外の Filter / Test は…
Collection IndexからFilterとTestを辿れないということは、FilterとTestに載っていない(特にbuiltin以外の)Filter / Testの情報はどこから知ればいいか不明…
例えばcommunity.generalにあるjc
Filterとか。
リポジトリのプラグインのディレクトリを見るしかないのかな。
2021.07追記:
community.generalコレクションのフィルタのドキュメントがいつのまにか追加されてた -> community.general Filter Guide — Ansible Documentation
コレクションによってはプラグインごとのドキュメントと別に独自のガイドが用意されるようになってる。