jqでノードへのパスと値を並べたファイルを作りたい

下記のようなJSONファイルがあるとする。

```JSON
{"会員名" : "文具 太郎",
 "購入品" : [ "はさみ",
              "ノート(A4,無地)",
              "シャープペンシル",
              {"取寄商品" : "替え芯"},
              "クリアファイル",
              {"取寄商品" : "6穴パンチ"}
            ]
}
```

予め取り出すべきデータへのパス(下の例だと```."会員名"```や.```"購入品"[0]```など)が分かっている場合、JSONからCSVやTSVを作るのは容易だ。
しかし、それがわからないときにはどうするか？

ノードを特定する情報（パス）とその値をスペースなり、カンマなりタブなりで並べた値を各行とするファイルを作りたくなる。
例えばこんなような。

```
.会員名 文具 太郎
.購入品[0] はさみ
.購入品[1] ノート(A4,無地)
.購入品[2] シャープペンシル
.購入品[3].取寄商品 替え芯
.購入品[4] クリアファイル
.購入品[5].取寄商品 6穴パンチ
```

その上で```grep```とか```cut```とか```uniq```とか```wc```とかで色々したいわけだ。
例えばはさみを買った会員は何人いるかな〜とかそういう作業だ。


# 既存の方法

Qiita内の人気記事に「[jq、xmllintコマンドさようなら。俺はパイプが好きだから](https://qiita.com/richmikan@github/items/e051b5d882c3dd2a39c6)」というのがある(以下、本稿内では「さようなら記事」と呼ぶ)。
実を言えば例にあげているデータもここから拝借したものである。

めんどうなので、この方法の問題点には立ち入らない。

# 本稿で紹介する方法

「さようなら記事」によると正味69行のスクリプト(長大なawkワンライナーが一行含まれており実質400行だと思うが・・・)で、この目的を達することができるそうだが、実のところ```jq```では本来一行でできることだ。

```
jq -r -c '. as $content|paths(scalars)|. as $path|tostring|. as $ps|$content|[$ps,getpath($path)]|@tsv' <JSON_FILE>
```
JSON_FILEが各行に一個のJSONを格納している場合は```-s```オプションをつけて欲しい。

ただし、得られる出力はこのようになる。

```
["会員名"]	文具 太郎
["購入品",0]	はさみ
["購入品",1]	ノート(A4,無地)
["購入品",2]	シャープペンシル
["購入品",3,"取寄商品"]	替え芯
["購入品",4]	クリアファイル
["購入品",5,"取寄商品"]	6穴パンチ
```

左側の配列は何かと言うと、[jq Manual (development version)](https://stedolan.github.io/jq/manual/)では```PATHS```と書かれている配列の一要素で入力内の位置を示す「パス」だ。
（```PATHS```は二次元配列になっており、各要素たる配列は入力のJSONノード内での特定の位置を表している）
この配列は、```getpath```・```setpath```の引数、```paths```の出力などに用いられている。

つまり```jq```ではドキュメント内の位置を示すやり方が2つあるのだ。

- 一つは普段、コマンドラインで使う```."購入品"[0]```のような記法。(```path_expression```と呼ばれる)
- もう一つは上に示した形式"array representation"(配列形式パスとでも呼んでおこう)

しかし、この配列形式パスを文字列化してしまうと、もう一度```jq```でパースしないとパスを表す情報として使えないし不便だ。
だから```."会員名"```や```."購入品"[0]```のように、```jq```に与えられる形式で書き出したいものだ。

そして問題は前者から後者の変換はできるが、その逆はできないことなのだ。([Issue-1949 of jq](https://github.com/stedolan/jq/issues/1949))
少なくとも令和元年12月現在では。

「できない」と言ったが、それは```jq```に最初からついているビルトインではできないというだけで、自分で```jq```の関数を書けばできる。

```bash
echo '{"会員名" : "文具 太郎",
 "購入品" : [ "はさみ",
              "ノート(A4,無地)",
              "シャープペンシル",
              {"取寄商品" : "替え芯"},
              "クリアファイル",
              {"取寄商品" : "6穴パンチ"}
            ]
}' | jq -r -c 'def path2pexp($v):
  $v | reduce .[] as $segment (""; . + ($segment | if type == "string" then ".\"" + . + "\"" else "[\(.)]" end));
. as $content|paths(scalars)|. as $p|path2pexp(.)|. as $pexp|$content|[$pexp,getpath($p)]|@tsv'
```

上記のコマンドラインは以下の出力を与える。

```
."会員名"	文具 太郎
."購入品"[0]	はさみ
."購入品"[1]	ノート(A4,無地)
."購入品"[2]	シャープペンシル
."購入品"[3]."取寄商品"	替え芯
."購入品"[4]	クリアファイル
."購入品"[5]."取寄商品"	6穴パンチ
```

タブの左側はそのまま```jq```に渡すことができる文字列になっている。

# 解説

上述のコマンドラインのうち、ここに掲げる2行が配列形式パスをpath_expressionに変換する関数である。
これは上記のIssue-1949 of jqに投稿されたコードスニペットを私が改良したものである。

1-2行目

```jq
def path2pexp($v):
  $v | reduce .[] as $segment (""; . + ($segment | if type == "string" then ".\"" + . + "\"" else "[\(.)]" end));
```

変数```$segment```に代入される配列形式パスの各要素を左から順に繋いでいく。ただし、型が文字列のときにはダブルクォートで括る

3行目

```jq
. as $content|paths(scalars)|. as $p|path2pexp(.)|. as $pexp|$content|[$pexp,getpath($p)]|@tsv'
```

ビルトイン関数```paths```に同じくビルトイン```scalars```を与え、文書内すべてのスカラー（非オブジェクト、非配列）への配列形式パスを得る。
各配列形式パスを```$p```に代入(```. as $p```)。これを```path2pexp```によりpath_expressionに変換したものを```$pexp```に代入(```path2pexp(.)|. as $pexp```)
あとは```$pexp```とそれに対応する位置にある値を```getpath($p)```で得て配列に格納し、TSVに変換する。(```[$pexp,getpath($p)]|@tsv```)

# まとめに代えて

まとめに代えて以下を指摘しておく。

- そもそもJSONはその[仕様](https://www.json.org/json-en.html)上、行指向のデータとも言える。
- ```jq```は入力をJSONとし、出力は整形されたJSONまたは行指向のデータ(JSON、CSV、TSV等)である。
- ```jq```はC99でコンパイルできる（試したことはないがそういうことになっている）
- JSONをUNIXやUNIX-likeなOSで使うなら```jq```の文法は覚えた方がいい。

# 参考
* [jqコマンドでJSONからキー名をいい感じに取得する](https://qiita.com/kai_kou/items/a982682131d1ad93a259)
* [jq、xmllintコマンドさようなら。俺はパイプが好きだから](https://qiita.com/richmikan@github/items/e051b5d882c3dd2a39c6)
* [jqコマンドでJSON形式をkey-value形式に変換](https://qiita.com/bb-kazu/items/8b55f01c3610695fdbd2)