TL; DR
sample.jq
def in_a(pathexps):
.a | pathexps;
in_a(.b)
$ echo '{"a": {"b": 1}}' | jq -f sample.jq
1
はじめに
9/7に、jqの新バージョンが5年ぶりにリリースされました
便利な関数もたくさん追加されており、例えば pick
では指定したパスの要素のみを抜き出せます。
$ jq -n '{"a": 1, "b": {"c": 2, "d": 3}, "e": 4} | pick(.a, .b.c, .x)'
{
"a": 1,
"b": {
"c": 2
},
"x": null
}
ところで、JS等 正格評価の言語では上記の関数は実装できません 。
正格評価の場合 pick(.a, .b.c, .x)
は pick(1, 2, null)
に評価されてから関数呼び出しされるため、関数側ではパスが何であったか知ることができません。
そこで本記事では、jqの引数の評価について調べてみました。
関数に渡されるのはパスの式
結論としては、jqの引数は遅延評価されていました1。
以下のように、存在しないパスを指定してもエラーは発生しません。ということは、関数呼び出しの時点ではパスの式は値に評価されていません。
sample.jq
# 引数のpathexpsは捨てる
def capture(pathexps):
100;
capture(.a.b.c)
$ echo '{"a": {"b": 1}}' | jq -f sample.jq
100
また、引数は ただのパスの式 なので、パイプの後に持ってくることも可能です。
sample.jq
$ cat sample.jq
def in_a(pathexps):
.a | pathexps;
# .a | .b と同じ
in_a(.b)
$ echo '{"a": {"b": 1}}' | jq -f sample.jq
1
ただし、ずっとパスとして扱えるわけではなく、関数本体の式で使用すると値に評価されます。
sample.jq
def idx(pathexps):
# ここでパスの値に評価される
pathexps;
idx(.a.b)
$ echo '{"a": {"b": 1}}' | jq -f sample.jq
1
引数を「パスを表す値」として持ちまわりたい場合は path
を使用します。
# パスの式を配列に変換(配列は第一級なのでそのまま変数等に代入可能)
$ jq -n 'path(.a.b[1])'
[
"a",
"b",
1
]
# 再びパスとして使用する場合は getpath
$ echo '{"a": {"b": 1}}' | jq 'getpath(path(.a.b))'
1
# もちろん直接配列を使用してもよい
$ echo '{"a": {"b": 1}}' | jq 'getpath(["a", "b"])'
1
pickの実装
冒頭のpickの実装は以下のようになっています。
src/builtin.jq
def pick(pathexps):
. as $in
| reduce path(pathexps) as $a (null;
setpath($a; $in|getpath($a)) );
reduceを使って、パスの場所にその値を格納しています。パスに指定した場所だけ格納されるため、結果指定したパスのみが抜き出されるという仕組みです。
おわりに
以上、jqの引数の評価についての紹介でした。jqは名前的にJavaScriptに似た特徴を持つと思い込んでいたので、この挙動には驚きました。
色々応用が効きそうなので、ぜひjq芸に使っていきましょう! (保守性は見なかったことにする)