JMESPathで配列から要素を抽出するにはパイプを用いる

###はじめに
[JMESPath](https://jmespath.org/)はJSONをparseすることを目的としたクエリ言語で、[AWS CLI](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-usage-output.html#cli-usage-output-filter)や[OCI CLI](https://community.oracle.com/docs/DOC-1019624#jive_content_id_4_JMESPath)に`--query`オプションとして組み込まれています。
AWS CLI等のJSON出力のparseにはコマンドラインツールである[jq](https://stedolan.github.io/jq/)を利用するパターンも多いかとは思いますが、先述の通りjqはコマンドラインツールであり、AWS CLI等とは別でインストールが必要になります。
AWS CLI等を利用するうえで、あくまでCLIオプションとして利用できる点がJMESPathのメリットの1つです。

さて、JMESPathですが、記法自体は基本的にはJavaScriptでオブジェクトを操作するときのような書き方である程度直感的に書くことはできるのですが、筆者の経験の中で、配列の操作で少し躓いた部分がありましたので共有します。

###前提
- コンソールはpowershell
- AWS CLIを利用
- 操作対象リソースはWAF ClassicのIPSet
- すでに複数のIPSetが存在している状態から開始
- Nameが`ip-set-2`のIPSetのIDを取得したい

###検証
まず存在するIPSetを一覧で表示してみます。そのためには`list-ip-sets`を使います。

```powershell
aws waf-regional list-ip-sets

{
    "NextMarker": "a9347819-ccdd-4e9e-a162-0bb5b9014c49",
    "IPSets": [
        {
            "IPSetId": "8e7a8af9-4a66-4987-b0f0-4be1473895e8",
            "Name": "ip-set-1"
        },
        {
            "IPSetId": "1d394b04-1f10-41dc-a277-71b5c953794f",
            "Name": "ip-set-2"
        },
        {
            "IPSetId": "01df5e3b-d5cc-4e06-a1ac-f405495db030",
            "Name": "ip-set-3"
        }
    ]
}
```
ここから要素`IPSets`だけを出力するためには、--queryオプションで単純に要素名を指定します。

```powershell
aws waf-regional list-ip-sets --query "IPSets"

[
  {
      "IPSetId": "8e7a8af9-4a66-4987-b0f0-4be1473895e8",
      "Name": "ip-set-1"
  },
  {
      "IPSetId": "1d394b04-1f10-41dc-a277-71b5c953794f",
      "Name": "ip-set-2"
  },
  {
      "IPSetId": "01df5e3b-d5cc-4e06-a1ac-f405495db030",
      "Name": "ip-set-3"
  }
]
```

配列が返ってきました。ここで、取得したいオブジェクトのインデックスが分かっている場合はシンプルにインデックスを指定してやればよいですが(最初の要素なら`--query "IPSets[0]"`)、今回はNameが`ip-set-2`のオブジェクトを取得したいため、抽出をかける必要があります。
そのためには[filter](https://jmespath.org/tutorial.html#filter-projections)を利用します。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2']"

[
  {
      "IPSetId": "1d394b04-1f10-41dc-a277-71b5c953794f",
      "Name": "ip-set-2"
  }
]
```

さて、欲しいオブジェクトだけになりましたが配列に包まれてしまっています。先述したように、配列から要素を取り出すにはインデックスを指定すればよかったはずなので、インデックス0を指定してあげれば取り出せそうです。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'][0]"

[]
```

……なぜか空の配列が返ってきてしまいました。困りました。
ここで先程の[JMESPathのチュートリアル](https://jmespath.org/tutorial.html#filter-projections)を見てみると、filterの直後にプロパティ名を指定すれば、配列に包まれた状態ではありますが要素を取り出せそうです。やってみましょう。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'].IPSetId"

[
  "1d394b04-1f10-41dc-a277-71b5c953794f"
]
```

意図通り返ってきました。後はこの配列から文字列を取り出せばよさそうです。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'].IPSetId[0]"

[]
```
しかし、この指定方法ではやはり空配列が返ってきてしまいます。配列から取り出した状態でIDを出力するにはどうしたらいいのでしょうか。

###結論
JMESPathで配列から要素を取り出すときには[パイプ](https://jmespath.org/tutorial.html#pipe-expressions)を利用します。
シェルでもおなじみ、出力を次の入力に渡す、あのパイプです。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'].IPSetId | [0]"

"1d394b04-1f10-41dc-a277-71b5c953794f"
```

やっとほしい結果が取得できました。
しかしなぜパイプを使わずに配列のインデックスを指定する方法では意図した結果が取れなかったのでしょうか。

回答としては[パイプのドキュメント](https://jmespath.org/tutorial.html#pipe-expressions)にしっかりと記載されていました。
>A common scenario is when you want to operate of the result of a projection rather than projecting an expression onto each element in the array.
…
What if you wanted the first element in that list? If you tried people[*].first[0] that you just evaluate first[0] for each element in the people array, and because indexing is not defined for strings, the final result would be an empty array, []. 

つまり、パイプの手前で取得していた`IPSets[?Name=='ip-set-2'].IPSetId`の部分はプロジェクションの結果であり、その出力値である配列にはインデックスが定義されていないので、インデックス指定での出力は空配列になってしまう、ということです。
出力値を次の式に渡せるパイプであれば、配列として読み込みなおせるのでインデックス指定が可能になる、ということですね。

また、上記のことからパイプを用いれば次のような書き方でも出力できることが分かります。

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'] | [0]"

{
   "IPSetId": "1d394b04-1f10-41dc-a277-71b5c953794f",
   "Name": "ip-set-2"
}
```

```powershell
aws waf-regional list-ip-sets --query "IPSets[?Name=='ip-set-2'] | [0].IPSetId"

"1d394b04-1f10-41dc-a277-71b5c953794f"
```

こちらの書き方のほうが、オブジェクトをフィルターし、そこから欲しい要素である`IPSetId`を指定していることが分かりやすいかもしれません。(この辺は好みの差、レベルの話しだと思います)

###最後に
配列から欲しいものが取れずにいろいろ試行錯誤して得た教訓は「きちんと公式ドキュメントを読もう」という、さんざん聞いたような言葉でした。
使い古された言葉というのは真理だからこそ使い古されるんだなぁ、というのを改めて痛感した次第です。