Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 2811

Bashでjqを使わずsedなど基本コマンドのみでJSONを破綻なく解釈

$
0
0
概要 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すれば再帰的に値を取り出していくことができる。

Viewing all articles
Browse latest Browse all 2811

Trending Articles