jqが使えなくても大丈夫なJavaScriptシェル芸


jqをインストールできなかったり、jqで複雑な加工をさせたいが構文が難しくて覚えられなかったりと、
なんらかの事情でjqを使わずjavascript(Node.js)でJSONを処理したいことが稀にあるかもしれません。

Node.jsは標準入力に弱い？そんな事はありません。ワンライナーで処理できます！

## JSONファイルが小さいとき
下記のようなJSONを想定します。[^1]

```json:sample.json 
{
  "menu": {
  "id": "file",
    "value": "File",
    "popup": {
      "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
      ]
    }
  }
}
```

ワンライナーで処理します。

```bash
$ cat sample.json | xargs -0 -i node -pe '({})'
{ menu:
   { id: 'file',
     value: 'File',
     popup:
      { menuitem:
         [ { value: 'New', onclick: 'CreateNewDoc()' },
           { value: 'Open', onclick: 'OpenDoc()' },
           { value: 'Close', onclick: 'CloseDoc()' } ] } } }
```

menuitemのvalueだけを抽出してみます。

```bash
## 少しフィルタしてみる
$ cat sample.json | xargs -0 -i node -pe '({}).menu.popup.menuitem.map(_=>_.value)'
[ 'New', 'Open', 'Close' ]
```

簡単ですね。

## 大きなJSONを処理できない問題

しかし、この方法だと実は限界があります。
下記のような単純で少しサイズの大きなJSONファイルがあるとします。

```json:sample.big.json
{ docs:
   [ { "_id": "14cd7192b2d699a606bde01c3123c750" },
     { "_id": "14cd7192b2d699a606bde01c3123d685" },
     { "_id": "14cd7192b2d699a606bde01c3123ddd4" },
     { "_id": "14cd7192b2d699a606bde01c3123f441" },
     { "_id": "14cd7192b2d699a606bde01c3123f6df" },
     { "_id": "14cd7192b2d699a606bde01c31240655" },
     { "_id": "14cd7192b2d699a606bde01c31241075" },
     { "_id": "14cd7192b2d699a606bde01c31241d31" },
     { "_id": "14cd7192b2d699a606bde01c31242a44" }, 
     // 省略
    ]
 }
```

確認すると69KBあります。

```bash
## ファイルサイズ(byte)
$ cat sample.big.json | wc  -c
69656
```

先程の方法でJSONを処理しようとしてみます。

```bash
$ cat sample.big.json | xargs -0 -i node -pe '({})'
xargs: argument line too long
```

引数(与えられたJSONの文字列)が大きすぎると怒られます。

これはシェルが展開した文字列が OSの引数の制限 (正確には execve(2) の制限) を越えると動かなくなるためです。[^2]
OSの引数に指定可能な文字数の最大値は下記で確認できます。

```bash
$ getconf ARG_MAX
32000
```
Git Bash上で実行すると32KBくらいまでみたいです。
ちなみにGCPのCloud Shellだと2MB(2097152byte)でしたので、Git bash相当小さいようですね。。。

## 大きなJSONに対応する

OSの制限にかからないワンライナーにします。

```bash:support-large-json
$ cat <(sed '1s/^/_=/' sample.big.json) <(echo _) | node -p -

{ docs:
   [ { _id: '14cd7192b2d699a606bde01c3123c750' },
     { _id: '14cd7192b2d699a606bde01c3123d685' },
     { _id: '14cd7192b2d699a606bde01c3123ddd4' },
     { _id: '14cd7192b2d699a606bde01c3123f441' },
     { _id: '14cd7192b2d699a606bde01c3123f6df' },
     ... 791 more items ]
 }
```

docsの_idだけを抽出してみます。

```bash:support-large-json
$ cat <(sed '1s/^/_=/' sample.big.json) <(echo '_.docs.slice(0,3).map(v=>v._id)') | node -p -
[ '14cd7192b2d699a606bde01c3123c750',
  '14cd7192b2d699a606bde01c3123d685',
  '14cd7192b2d699a606bde01c3123ddd4' ]
```

## jqのようにきれいに整形

今まで紹介した方法でシンプルに処理できますが出力されるJSONは、ダブルコーテーションが外れ、インデントが微妙になり、大きなリストは「... xxx more items」のように省略されてしまいます。

出力がきれいになるように修正します。

```bash:beautifully
$ cat <(sed '1s/^/_=/' sample.big.json) <(echo 'JSON.stringify(_,null,"\t")') | node -p -
{
        "docs": [
                {
                        "_id": "14cd7192b2d699a606bde01c3123c750"
                },
                {
                        "_id": "14cd7192b2d699a606bde01c3123d685"
                },
                {
                        "_id": "14cd7192b2d699a606bde01c3123ddd4"
                },
                {
                        "_id": "14cd7192b2d699a606bde01c3123f441"
                },
                {
                        "_id": "14cd7192b2d699a606bde01c3123f6df"
                },
                {
                        "_id": "14cd7192b2d699a606bde01c31240655"
                },
        //省略しますが全部出力されます
        ]
}
```

きれいに出力されてますね。
docsの_idだけを抽出してみます。

```bash:beautifully
$ cat <(sed '1s/^/_=/' sample.big.json) <(echo 'JSON.stringify(_.docs.slice(0,3).map(v=>v._id),null,"\t")') | node -p -
[
        "14cd7192b2d699a606bde01c3123c750",
        "14cd7192b2d699a606bde01c3123d685",
        "14cd7192b2d699a606bde01c3123ddd4"
]
```

## まとめ

~~jq を使ったほうが早そうな気……~~これでNode.jsさえあればjq使わなくても大丈夫ですし、
複雑な加工でも**学習コスト0で気軽にJSONを加工できるようになりますね！**

他に良い方法があればコメントお願いします。

## 備考

**jq is awesome !**

```bash
$ cat sample.big.json | jq [.docs[0:3][]._id]
[
  "14cd7192b2d699a606bde01c3123c750",
  "14cd7192b2d699a606bde01c3123d685",
  "14cd7192b2d699a606bde01c3123ddd4"
]
```


[^1]: https://json.org/example.html
[^2]: http://x68000.q-e-d.net/%7E68user/unix/pickup?xargs