概要
Bashでjqコマンドなど使わず、sedなどの基本コマンドのみでJSONを破綻なく解釈したい。
破綻なくというのはこの記事では以下を指す。
文字列に記号が入っていても問題ない
配列、オブジェクトのネスト構造を、ちゃんとネスト構造として解釈する
何ができるようになるか
Bashで主にsedを使った30行程度の処理で、JSONを破綻なく解釈できるようになる。
例えば、
に記載されたJSONで、ルートオブジェクトのphoneNumbers配列の2番目のオブジェクトのnumberを取得したい場合、
root=$(cat - | normalize_json | encode_recursively | decode_and_split)
phone_numbers=$(echo "$root" | sed -En 's/^"phoneNumbers":(.*)$/\1/p' | decode_and_split)
phone_number=$(echo "$phone_numbers" | sed -n 2p | decode_and_split)
number=$(echo "$phone_number" | sed -En 's/^"number":"(.*)"$/\1/p')
echo "number: $number"
出力:
number: 646 555-4567
とできるようになる。
この例では記号などは入ってないが、文字列にダブルクォーテーションや括弧などの記号が入っていても問題なく扱える。
アイデア
ネストしていない配列・オブジェクト(最も深い配列・オブジェクト)をエンコードし、それらの表現に[]{},の記号が含まれないようにする
上記を全体で[]{},が含まれなくなるまで繰り返す
この文字列にデコードを1回行うと、最も浅い配列・オブジェクトのみが[]{},で解釈できるようになる
上記をUNIXコマンドで扱いやすい改行区切りにする
子の配列・オブジェクトを扱いたい場合は、その部分のみ取り出し、デコードを行うことで、上記の処理を再帰的に適用できる
必要な実装
「何ができるか」で利用した3つの関数、
normalize_json
encode_recursively
decode_and_split
のみ必要になる。
全実装は以下、
normalize_json() {
cat - \
| sed -z 's/\\"/\\x22/g' \
| sed -z 's/"[^"]*"/\n&\n/g' \
| sed '/^"/!s/ //g' \
| sed -E '/^"/{
s/,/\\x2c/g; s/:/\\x3a/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
| tr -d '\n'
}
encode_recursively() {
data=$(cat - \
| sed -Ez 's/[[{][^][}{]*[]}]/\n&\n/g' \
| sed -E '/^(\[.*\]|\{.*\})$/{
s/\\/\\\\/g; s/,/\\x2c/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
| tr -d '\n'
)
if echo "$data" | grep '[[{]' > /dev/null; then
echo "$data" | encode_recursively
else
echo "$data"
fi
}
decode_and_split() {
printf %b "$(cat -)" | tr -d '[]{}' | sed -z 's/,/\n/g'
}
解説
normalize_json
JSONを今回の処理に都合良いように正規化する。
主に文字列にJSON構造を表す文字が入らないようエスケープ表現に変更している。
normalize_json() {
cat - \
| sed -z 's/\\"/\\x22/g' \
| sed -z 's/"[^"]*"/\n&\n/g' \
| sed '/^"/!s/ //g' \
| sed -E '/^"/{
s/,/\\x2c/g; s/:/\\x3a/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
| tr -d '\n'
}
| sed -z 's/\\"/\\x22/g' \
↑文字列中の\"を\x22に置換していく。
これで、"はJSON全体で文字列を囲うことにしか使われなくなることが保証される。
| sed -z 's/"[^"]*"/\n&\n/g' \
↑文字列が文字列のみで1行となるように改行を入れる。
| sed '/^"/!s/ //g' \
↑文字列でない行では、スペースを削除する。
| sed -E '/^"/{
s/,/\\x2c/g; s/:/\\x3a/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
↑文字列の行では、JSON構造に使われる文字,:[]{}、をエスケープ表現にする。
これで、文字,:[]{}は、JSON全体でJSON構造の表現にしか使われないことが保証できる。
| tr -d '\n'
↑処理都合で追加していた改行と、元からあった不要な改行を削除する。
(JSONでは文字列中には制御文字は使えないため、文字列中に改行はないはず)
例として挙げていた、JSONに、
"strWithSymbols": "test \"{[,: test"
を追加したJSON、
{
"firstName": "John",
"lastName": "Smith",
"isAlive": true,
"age": 27,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-3100"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567",
"strWithSymbols": "test \"{[,: test"
}
],
"children": [],
"spouse": null
}
は、
{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567","strWithSymbols":"test \x22\x7b\x5b\x2c\x3a test"}],"children":[],"spouse":null}
となる。
以降もこのJSONを元に説明を進める。
encode_recursively
ネストしていない配列・オブジェクト(階層が最も深い)をエスケープし、適用対象がなくなるまで繰り返す。
encode_recursively() {
data=$(cat - \
| sed -Ez 's/[[{][^][}{]*[]}]/\n&\n/g' \
| sed -E '/^(\[.*\]|\{.*\})$/{
s/\\/\\\\/g; s/,/\\x2c/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
| tr -d '\n'
)
if echo "$data" | grep '[[{]' > /dev/null; then
echo "$data" | encode_recursively
else
echo "$data"
fi
}
| sed -Ez 's/[[{][^][}{]*[]}]/\n&\n/g' \
↑ネストしていない配列・オブジェクトが単体の行となるよう分割する。
| sed -E '/^(\[.*\]|\{.*\})$/{
s/\\/\\\\/g; s/,/\\x2c/g;
s/\[/\\x5b/g; s/\]/\\x5d/g;
s/\{/\\x7b/g; s/\}/\\x7d/g;
}' \
↑上記で単体の行になった、ネストしていない配列・オブジェクトに対して、\,[]{}をエスケープする。
これによって、これらには,[]{}が含まれなくなる。
また、\もエスケープすることにより、多重にエスケープできるようにしている。
| tr -d '\n'
↑処理都合で挿入していた改行を削除する。
if echo "$data" | grep '[[{]' > /dev/null; then
echo "$data" | encode_recursively
↑まだ、エスケープされていない配列・オブジェクトが残っていた場合は、ここまでの処理を再帰的に繰り返す。
else
echo "$data"
fi
↑そうでなければ、ここまでの結果を結果として返す。
この処理で、前述の例のJSONは、
\x7b"firstName":"John"\x2c"lastName":"Smith"\x2c"isAlive":true\x2c"age":27\x2c"address":\\x7b"streetAddress":"21 2nd Street"\\x2c"city":"New York"\\x2c"state":"NY"\\x2c"postalCode":"10021-3100"\\x7d\x2c"phoneNumbers":\\x5b\\\\x7b"type":"home"\\\\x2c"number":"212 555-1234"\\\\x7d\\x2c\\\\x7b"type":"office"\\\\x2c"number":"646 555-4567"\\\\x2c"strWithSymbols":"test \\\\\\\\x22\\\\\\\\x7b\\\\\\\\x5b\\\\\\\\x2c\\\\\\\\x3a test"\\\\x7d\\x5d\x2c"children":\\x5b\\x5d\x2c"spouse":null\x7d
のようになる。
ネストが深い配列・オブジェクトはそれだけ多重にエスケープされている。
decode_and_split
エスケープを浅い1階層のみ解除し、配列・オブジェクトをUNIXコマンドで扱いやすい1要素1行の表現にする。
decode_and_split() {
printf %b "$(cat -)" | tr -d '[]{}' | sed -z 's/,/\n/g'
}
printf %b "$(cat -)"
↑エスケープを解釈している。echo -eでもよいが上記のほうがPOSIX準拠度が高そうなためそうしている。
これで、最も浅い配列・オブジェクトのみ[]{},で構造が解釈できるようになる。
| tr -d '[]{}'
↑最も浅い配列・オブジェクトの[]{}を削除する。
| sed -z 's/,/\n/g'
↑カンマを改行に変換する。
これで、最も浅い配列・オブジェクトが1要素1行となりUNIXコマンドで扱いやすい形式になる。
例えば、配列の3番目を取り出したい場合は、
echo "$data" | sed -n 3p
になるし、オブジェクトでキーがnameの値を取り出したいときは、
echo "$data" | sed -En 's/^"name":(.*)$/\1/p'
となる。
取り出した値が配列・オブジェクトの場合は、再度decode_and_splitすれば再帰的に値を取り出していくことができる。
↧