「cURLで取得したJSONの複数フィールドを別々の変数に格納する」を1行で

# やりたいこと
あるAPIが`{"name": "hoge", "age": 23}`のようなJSON形式のレスポンスを返すとして，「cURLでAPIを叩き，シェル変数`name`に`hoge`を，`age`に`23`を格納する」ということをワンライナーでやりたい。

今回は例として郵便番号検索APIを使います。
[郵便番号検索API - zipcloud](http://zipcloud.ibsnet.co.jp/doc/api)

なんの捻りもないですが，`address1`，`address2`，`address3`をそれぞれ別の変数に格納します。

```
$ curl -X GET "http://zipcloud.ibsnet.co.jp/api/search?zipcode=1000014"
{
	"message": null,
	"results": [
		{
			"address1": "東京都",
			"address2": "千代田区",
			"address3": "永田町",
			"kana1": "ﾄｳｷｮｳﾄ",
			"kana2": "ﾁﾖﾀﾞｸ",
			"kana3": "ﾅｶﾞﾀﾁｮｳ",
			"prefcode": "13",
			"zipcode": "1000014"
		}
	],
	"status": 200
}
```

一応bashとzshで動作を確認しています。

# やり方

```
$ URL="http://zipcloud.ibsnet.co.jp/api/search?zipcode=1000014"

# 1-1.別の変数に格納
$ add1= add2= add3=
$ read add1 add2 add3 < <(curl -X GET $URL | \
> jq -r '.results[]|.address1,.address2,.address3' | perl -pe 's/\n/ /g')
$ echo "$add1-$add2-$add3"
>>> 東京都-千代田区-永田町

# 1-2.perlを使わずに
$ add1= add2= add3=
$ read add1 add2 add3 < <(curl -X GET $URL | \
> jq -r '.results[]|[.address1,.address2,.address3]|@tsv')
$ echo "$add1-$add2-$add3"
>>> 東京都-千代田区-永田町

# 2.配列にまとめて格納
$ address=
$ read -a address < <(curl -X GET $URL | \
> jq -r '.results[]|.address1,.address2,.address3' | perl -pe 's/\n/ /g')
$ echo ${address[@]}
>>> 東京都 千代田区 永田町
$ echo ${address[0]}
>>> 東京都
```

# ポイント
cURLでAPIを叩いて`jq`でフィールドを抽出するまでは特に問題ないと思いますが，抽出したフィールドを`read`に渡して変数に格納する部分が多少厄介かなと思います。特に重要なのは以下の点です。

## パイプで渡さない
コマンドの出力を`read`にパイプで渡しても，以下のように意図する結果になりません。

```
$ echo "hoge" | read fuga
$ echo $fuga
>>>
```

これはパイプラインのコマンドがそれぞれ固有のサブシェル内で実行されるため，`read`で定義されたシェル変数を元のシェルから参照できないことが原因です。関数Aで定義された変数はグローバル宣言しない限り関数Bから参照できない，みたいなことですね。詳しくは以下の記事をご覧ください。

[bash: readとパイプと環境変数 - TIM Labs](http://labs.timedia.co.jp/2011/09/shell-read-pipe-and-environment-variables.html)

なので，`read`コマンドは必ず元のシェルで実行されなければならず，そのためにはプロセス置換を標準入力にリダイレクトして`read`に渡す必要があります。その結果，`read ... < <(curl...)`という構造を取ることになるわけです。

プロセス置換はコマンドの実行結果をファイルのように扱うため，実質`read [変数名] < [ファイル名]`と同じことをしています。私もプロセス置換をちゃんと触るのは初めてなので，より詳しくは以下の記事をご参照ください。

[Linuxでのプロセス置換 - Qiita](https://qiita.com/angel_p_57/items/fd1d9a10e2f4aba17669#%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E7%BD%AE%E6%8F%9B)

## 改行を含めない
`read`は標準入力から1行読むというコマンドなので，入力ストリームに改行かEOFがあればそこで終了します。

しかし，cURLの結果から`jq`で複数フィールド抽出する場合，何もしなければ以下のようにそれぞれのフィールドは末尾に改行文字が挿入された状態で出力されます。

```
$ curl -X GET "http://zipcloud.ibsnet.co.jp/api/search?zipcode=1000014" | jq -r '.results[]|.address1,.address2,.address3'
>>> 東京都
>>> 千代田区
>>> 永田町
```

これをこのまま`read`に渡しても1行目以外は捨てられてしまうので，`perl`で改行文字をスペースに置換したり，`jq`の`@tsv`フィルターで出力をタブ区切りにするなどの工夫が必要になります。

`read`は環境変数`IFS`を区切り文字として用いるのですが，`IFS`はデフォルトでは「スペースかタブか改行」となっているため，スペース区切りでもタブ区切りでもちゃんと分割してそれぞれの変数に格納してくれます。ただし改行区切りはダメというのは上で述べた通りです。

# おわりに
需要がありそうな割にクリティカルなノウハウが見当たらなかったので自分で記事を書きました。割と手探りで書いたので，もっといいやり方を知っているシェルの達人がいらっしゃいましたら，教えていただけると非常に助かります。

プロセス置換はなんか難しそうだったので今まで使っていなかったんですが，使い所がわかれば結構便利そう，というのが知れて良かったです。

最後までご覧いただきありがとうございました。

# その他参考にした記事
[jqコマンドで複数フィールドの値を１行に表示させる - 動かざることバグの如し](https://thr3a.hatenablog.com/entry/20190826/1566796327)
[readコマンドで1つの文字列から複数の変数に代入するとき - j3iiifn’s blog](https://j3iiifn.hatenablog.com/entry/2019/07/31/010000)
[【シェルスクリプト】IFSで区切り文字（デリミタ）を変更する方法 | server-memo.net](https://www.server-memo.net/shellscript/ifs.html)