はじめに
ご無沙汰しております。田中です。今回は jq と JMESpath について書きます。
AWS CLI の返り値の JSON は、以下のように --query オプションに JMESPath を指定して処理できます。
1 2 3 4 5 6 7 |
# 名前が test- で始まる最新の AMI の id を取得 $ aws ec2 describe-images \ --filters Name=state,Values=available Name=is-public,Values=false \ --query "Images[?starts_with(Name, 'test-')] | sort_by(@, &CreationDate) | reverse(@)[0].ImageId" "ami-xxxxxxxxxxxxxxxxx" |
ですが、JMESPath は jq の構文と似ていて混乱するし、わざわざ覚えるのも面倒なので、jq コマンドさえ覚えておけばそれでいい、と思ってしまいます。
1 2 3 4 5 6 7 |
# jq で処理 $ aws ec2 describe-images \ --filters Name=state,Values=available Name=is-public,Values=false \ | jq '[.Images[] | select(.Name | startswith("test-"))] | sort_by(.CreationDate) | reverse[0].ImageId' "ami-xxxxxxxxxxxxxxxxx" |
しかし、以前に、作業中のサーバに jq がインストールされていなくて困る、ということがありました。そもそも JMESPath で指定できるのに、わざわざ jq を使うというのも何か負けた気がするし、そういえば jq 自体もきちんと理解して使っているわけではなかったので、この際、両者を同時に正しく覚えておこうと思いました。
尚、以下の jq / JMESPath は、それぞれの公式ページの playground で確認しました。
現在の値をそのまま返す
1 2 3 4 5 6 7 8 9 |
入力 : {"foo": "bar"} 出力 : {"foo": "bar"} jq : . JMES : @ 意味はありませんが、こうしても同じです ( | は 左の出力を右の入力にする) jq : . | . | . | . | . JMES : @ | @ | @ | @ | @ |
. / @ は、| や関数に値を渡すときに、現在処理している値を表します。
配列/オブジェクトの要素を取得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
◆ 配列のインデックスを指定して値を取得 入力 : ["a0", "a1"] 出力 : "a1" jq : .[1] JMES : [1] ◆ オブジェクトのキーを指定して値を取得 入力 : {"x": "X", "y": "Y"} 出力 : "Y" jq : .y JMES : y |
jq では . が必要です。JMESPath では @[1], @.y とする必要はありません。
ネストした配列/オブジェクトから値を取得する場合は、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
◆ ネストした配列 入力 : [["a0", "a1", "a2"], ["b0", "b1", "b2"]] 出力 : "b2" jq : .[1][2] JMES : [1][2] ◆ ネストしたオブジェクト 入力 : {"foo": {"x": "X", "y": "Y"}} 出力 : "Y" jq : .foo.y JMES : foo.y |
取得した値を使用して新たに配列/オブジェクトを作成することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
◆ 配列を作成 入力 : {"x": 10, "y": 20} 出力 : [10, 20, 30] jq : [.x, .y, 30] JMES : [x, y, `30`] 入力 : {"foo": {"x": 10, "y": 20}} 出力 : [10, 20, 30] jq : [.foo.x, .foo.y, 30] JMES : foo.[x, y, `30`] 又は [foo.x, foo.y, `30`] ◆ オブジェクトを作成 入力 : ["X", "Y"] 出力 : {"x": "X", "y": "Y", "z": "Z"} jq : {x: .[0], y: .[1], z: "Z"} JMES : {x: [0], y: [1], z: 'Z'} 入力 : [0, ["X", "Y"]] 出力 : {"x": "X", "y": "Y", "z": "Z"} jq : {x: .[1][0], y: .[1][1], z: "Z"} JMES : [1].{x: [0], y: [1], z: 'Z'} 又は {x: [1][0], y: [1][1], z: 'Z'} |
JMESPath の引用符が、少し複雑です。JMESPath では、JSON として評価される値を直接書く場合、` (back quote) で囲む必要があります。よって、数値は `30` のように書く必要があります。文字列は `"Z"` のようになりますが (JSON の文字列は " (double quote) で囲むので) 、 これとは別に文字列リテラルを表す ' (single quote) があり、'Z' と書くこともできます。
(ただ、`Z` と書いても動作します。これは、どう解釈すればいいでしょう......)
配列の map 処理
● jq の [] と JMESPath の [*]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
◆ 配列の配列 入力 : [[0, 1], [2, 3], [4, 5]] 出力 : [1, 3, 5] jq : [.[][1]] JMES : [*][1] 入力 : [0, [[0, 1], [2, 3], [4, 5]]] 出力 : [1, 3, 5] jq : [.[1][][1]] JMES : [1][*][1] ◆ オブジェクトの配列 入力 : [{"x": 0}, {"x": 1}, {"x": 2}] 出力 : [0, 1, 2] jq : [.[].x] JMES : [*].x 入力 : {"foo": [{"x": 0}, {"x": 1}, {"x": 2}]} 出力 : [0, 1, 2] jq : [.foo[].x] JMES : foo[*].x |
jq の [] は iterator 、JMESPath の [*] は projection と呼ばれ、JavaScript で言えば map 関数のようなことを行います。どちらも独特の動作をし、混乱しがちなので、少し詳しく説明します。
● jq の iterator
jq の [] は、配列を要素の行に分解します。上記の一番目の例では、以下の順で処理がされています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
① [] で行に分解 [0, 1] [2, 3] [4, 5] ② [1] で 第 2 要素を取得 1 3 5 ③ [] で囲って配列に戻す # 勿論、配列に戻す必要がない場合は不要 [1, 3, 5] |
iterator の処理結果は複数の JSON の行であり、配列ではないので、配列と考えて要素を取得しようとするとエラーになります。
1 2 3 4 5 6 7 8 9 |
◆ iterator で処理した結果から要素を取得 入力 : [{"x": 0}, {"x": 1}, {"x": 2}] 出力 : 1 【誤】jq : .[].x[1] # エラーが発生 【正】jq : [.[].x][1] # もっとも、この例では、[] を使わず、以下のように書くのが自然ですが jq : .[1].x |
● JMESPath の projection
JMESPath の [*] は、jq の iterator のように行に分解しているわけではありませんが、projection の 結果から要素を取得するときは、やはり、注意が必要です。
1 2 3 4 5 6 |
◆ projection の結果から要素を取得 入力 : [{"x": 0}, {"x": 1}, {"x": 2}] 出力 : 1 【誤】JMES : [*].x[1] # 空配列 [] が返る 【正】JMES : [*].x | [1] |
[*].x の結果は、やはり projection なので、これに対し [1] を指定すると、各要素である数値 0, 1, 2 に対してインデックスを指定していることになります。配列でないものにインデックスを指定した場合は null が返り、また projection において null は無視される仕様(後述)なので、結果として空配列が返ることになります。よって、projection の終了を示すために、間に | を挟む必要があるというわけです。
● オブジェクトに対する iterator / projection
オブジェクトに対しても、 iterator / projection を使用できます。この場合、オブジェクトの値の配列に対して処理されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
◆ 値が配列であるオブジェクト 入力 : {"x": ["a0", "a1"], "y": ["b0", "b1"]} 出力 : ["a1", "b1"] jq : [.[][1]] JMES : *[1] 入力 : [0, {"x": ["a0", "a1"], "y": ["b0", "b1"]}] 出力 : ["a1", "b1"] jq : [.[1][][1]] JMES : [1].*[1] ◆ 値がオブジェクトであるオブジェクト 入力 : {"a": {"x": "X0"}, "b": {"x": "X1"}} 出力 : ["X0", "X1"] jq : [.[].x] JMES : *.x 入力 : {"foo": {"a": {"x": "X0"}, "b": {"x": "X1"}}} 出力 : ["X0", "X1"] jq : [.foo[].x] JMES : foo.*.x |
jq では配列の場合と全く同じ [] ですが、JMESPath では [*] ではなく * を使用します。
● null の処理
iterator / projection では、null の処理に違いがあります。
1 2 3 4 5 6 |
入力 : [{"x": "X0"}, {"z": "X1"}, {"x": "X2"}] 出力 : jq は ["X0", null, "X2"] JMES は ["X0", "X2"] jq : [.[].x] JMES : [*].x |
JMESPath の projection では null が無視されます。jq と同じ結果にしたい場合は、後述の map 関数を使いましょう。
また、jq の iterator の処理の結果から null を除去したい場合は、null でない値のみを選択する関数 values が使えます。
1 2 3 4 |
入力 : [{"x": "X0"}, {"z": "X1"}, {"x": "X2"}] 出力 : ["X0", "X2"] jq : [.[].x | values] |
● map 関数
1 2 3 4 5 |
入力 : [{"x": 0}, {"x": 1}, {"x": 2}] 出力 : [0, 1, 2] jq : map(.x) # [.[] | .x] と同じ JMES : map(&x, @) # [*].x と同じ (但し projection ではない) |
iterator / projection と同様の処理をするのに、map 関数を使うこともできます。
jq では、現在値を処理対象にするので、引数は処理内容のみです。JMESPath では、処理対象の配列(上例では現在値を表す @)も引数に指定します。
&x の & は「x の値ではなく x という JMESPath の構文だ」ということを表すために必要ということみたいですが...... まあ、こういう書き方をすると覚えておきましょう。
尚、JMESPath の map は projection ではないので、結果も配列になります。
他の関数と組み合わせて使用することもできます。
1 2 3 4 5 |
入力 : ["0", 1, "2"] 出力 : [0, 1, 2] jq : map(tonumber) # [.[] | tonumber] と同じ JMES : map(&to_number(@), @) # [*].to_number(@) と同じ (但し projection ではない) |
JMESPath の例において、右の @ は map から見た現在値(["0", 1, "2"])で、左の @ は to_number から見た現在値("0" 又は 1 又は "2")です。
絞り込み
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
◆ 2 以上の要素を選択 入力 : [3, 0, 2, 1, 4] 出力 : [3, 2, 4] jq : [.[] | select(. >= 2)] JMES : [? @ >= `2`] ◆ x の値が "b" で始まる要素を選択 入力 : [{"x": "foo"}, {"x": "bar"}, {"x": "baz"}] 出力 : [{"x": "bar"}, {"x": "baz"}] jq : [.[] | select(.x | startswith("b"))] JMES : [?starts_with(x, 'b')] |
jq には特別な構文はないので、select 関数を使用します。
JMESPath では [? 真偽値を返す式] という構文を使います。これも projection です。[*] の * を条件式と入れ替えたもの、と覚えましょう。
ソート
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
◆ 数値配列を昇順でソート 入力 : [2, 0, 1] 出力 : [0, 1, 2] jq : sort JMES : sort(@) ◆ ソート結果を逆順にする 入力 : [2, 0, 1] 出力 : [2, 1, 0] jq : sort | reverse JMES : reverse(sort(@)) 又は sort(@) | reverse(@) ◆ 配列要素であるオブジェクトの y の値でソート 入力 : [{"x": 1, "y": 2}, {"x": 2, "y": 0}, {"x": 3, "y": 1}] 出力 : [{"x": 2, "y": 0}, {"x": 3, "y": 1}, {"x": 1, "y": 2}] jq : sort_by(.y) JMES : sort_by(@, &y) ◆ 配列要素である配列の第 2 要素の値でソート 入力 : [[1, 2], [2, 0], [3, 1]] 出力 : [[2, 0], [3, 1], [1, 2]] jq : sort_by(.[1]) JMES : sort_by(@, &[1]) |
JMESPath の sort_by の引数の位置、map と逆ですね。何故でしょう......
flatten
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
◆ 深さ 1 まで flatten 入力 : [0, [1], [[2]]] 出力 : [0, 1, [2]] jq : flatten(1) JMES : [] ◆ 全て flatten 入力 : [0, [1], [[2]]] 出力 : [0, 1, 2] jq : flatten JMES : [][] # 深さに合わせて [] を増やす |
JMESPath の [] は、これも projection です。[*] の * がなくなったもの、と覚えましょう。jq の iterator と混同しないように注意して下さい。任意の深さまで全て flatten する方法はないようなので、深さが分からないときは [][][][][][][][][][] ぐらいにしておけば......
スライス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
◆ 始点と終点を指定 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["b", "c"] jq : .[1:3] JMES : [1:3] ◆ 終点を省略 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["d", "e", "f"] jq : .[3:] JMES : [3:] ◆ 始点を省略 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["a", "b", "c"] jq : .[:3] JMES : [:3] ◆ 負値を指定 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["d", "e"] jq : .[-3:-1] JMES : [-3:-1] ◆ 増分値を指定 # JMESPath のみ 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["b", "d"] JMES : [1:4:2] ◆ 増分値に負値を指定 # JMESPath のみ 入力 : ["a", "b", "c", "d", "e", "f"] 出力 : ["e", "c"] JMES : [-2:-5:-2] |
スライスの使い方は、jq と JMESPath で殆ど同じですが、増分値を指定できるのは JMESPath のみです。JMESPath のスライスは、これも projection です。
join
1 2 3 4 5 |
入力 : ["x", "y", "z"] 出力 : "x,y,z" jq : join(",") JMES : join(',', @) |
最後に
どちらの構文も一長一短な気がします。ただ、jq の方が機能が多いです。JMESPath では、例えば以下のことができないようです。
※ 筆者の理解不足かもしれません。方法をご存じの方は、ご教授下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
◆ 数値計算する 入力 : [0, 1, 2] 出力 : [0, 2, 4] jq : [.[] | . * 2] ◆ 文字列を切り取る 入力 : ["abc", "xyz"] 出力 : ["b", "y"] jq : [.[][1:2]] ◆ 出力値をオブジェクトのキーにする 入力 : ["a", "x"] 出力 : {"x": "a"} jq : {(.[1]): .[0]} |
とにかく、ここに書いたことぐらいを覚えておけば、一番始めに挙げた AWS CLI の例ぐらいのことはできるので、とりあえず十分ではないでしょうか。
この記事が皆様の日々の作業の一助となれば幸いです。
尚、本記事の執筆にあたり、上掲の公式サイトを参考にさせて頂きました。