このページの内容

高度な機能

ほとんどのプログラミング言語では変数が不可欠ですが、jqでは「高度な機能」として扱われます。

ほとんどの言語では、データを受け渡す手段は変数だけです。値を計算して2回以上使いたい場合、変数に保存する必要があります。プログラムの別の部分に値を渡すには、その部分で、関数の引数やオブジェクトのメンバーなど、データを置く変数を定義する必要があります。

jqでも関数を定義できます。ただし、この機能の主な用途はjqの標準ライブラリの定義です。map や select など、多くのjq関数は、実際にjqで書かれています。

jqには集約演算子があり、非常に強力ですが、少し扱いにくいものです。これらも主に、jqの標準ライブラリの便利な部分を定義するため、内部で使われています。

最初は分かりにくいかもしれませんが、jqの中心にあるのはジェネレーターです。他の言語でもよく見られるものです。ジェネレーターを扱うための補助機能がいくつか提供されています。

標準入力からJSONを読み込み、標準出力へJSONを書き出す以外にも、最小限の入出力機能があります。

最後に、モジュール/ライブラリの仕組みがあります。

変数/記号的束縛演算子:... as $identifier | ...

jqでは、すべてのフィルターに入力と出力があるため、プログラムのある部分から次の部分に値を渡すための手動の受け渡し処理は必要ありません。たとえば a + b のような多くの式は、入力を2つの異なる部分式に渡します。ここでは、a と b の両方に同じ入力を渡します。そのため、値を2回使うために変数が必要になることは、通常ありません。

たとえば、数値の配列の平均値を計算するには、ほとんどの言語でいくつかの変数が必要です。少なくとも配列を保持する変数が1つ、場合によっては各要素やループのカウンターを保持する変数も必要です。jqでは、単に add / length と書きます。式 add は配列を受け取って合計を生成し、式 length は配列を受け取って長さを生成します。

そのため、jqでは、ほとんどの問題に対して変数を定義するよりすっきりした解決方法があります。それでも、変数で簡単になる場合があるため、jqでは expression as $variable を使って変数を定義できます。変数名はすべて $ で始まります。配列の平均の例を、少し見通しの悪い形で書くと、次のようになります。

length as $array_length | add / $array_length

変数を使うことで実際に楽になる場面を見つけるには、もう少し複雑な問題が必要です。

"author"と"title"フィールドを持つブログ記事の配列と、著者のユーザー名を実名に対応付ける別のオブジェクトがあるとします。入力は次のようになります。

{"posts": [{"title": "First post", "author": "anon"},
           {"title": "A well-written article", "author": "person1"}],
 "realnames": {"anon": "Anonymous Coward",
               "person1": "Person McPherson"}}

次のように、authorフィールドに実名が入った記事を生成したいとします。

{"title": "First post", "author": "Anonymous Coward"}
{"title": "A well-written article", "author": "Person McPherson"}

変数 $names にrealnamesオブジェクトを保存し、後で著者のユーザー名を調べる際に参照できるようにします。

.realnames as $names | .posts[] | {title, author: $names[.author]}

式 exp as $x | ... は、式 exp の各値に対して、元の入力全体を使い、$x をその値に設定して、パイプラインの残りを実行するという意味です。そのため、as は一種のforeachループのように機能します。

{foo} が {foo: .foo} の便利な省略形であるのと同様に、{$foo} は {foo: $foo} の便利な省略形です。

1つの as 式で複数の変数を宣言できます。入力の構造に一致するパターンを指定します。これは「分割代入」と呼ばれます。

. as {realnames: $names, posts: [$first, $second]} | ...

配列パターン内の変数宣言(たとえば、. as [$first, $second])は、索引0から順に配列の要素を束縛します。配列パターンの要素に対応する索引に値がない場合、その変数には null が束縛されます。

変数のスコープは、その変数を定義した式の残りの部分に及びます。そのため、

.realnames as $names | (.posts[] | {title, author: $names[.author]})

は動作しますが、

(.realnames as $names | .posts[]) | {title, author: $names[.author]}

は動作しません。

プログラミング言語の理論の用語では、jqの変数は字句的なスコープを持つ束縛と呼ぶ方が正確です。特に、束縛の値を変更する方法はありません。同じ名前で新しい束縛を作ることだけができますが、以前の束縛が見えていた場所で新しい束縛が見えるようになるわけではありません。

実行例 1

コマンド

jq '.bar as $x | .foo | . + $x'

入力

{"foo":10, "bar":200}

出力 1

210

実行例 2

コマンド

jq '. as $i|[(.*2|. as $i| $i), $i]'

入力

5

出力 1

[10,5]

実行例 3

コマンド

jq '. as [$a, $b, {c: $c}] | $a + $b + $c'

入力

[2, 3, {"c": 4, "d": 5}]

出力 1

9

実行例 4

コマンド

jq '.[] as [$a, $b] | {a: $a, b: $b}'

入力

[[0], [0, 1], [2, 1, 0]]

出力 1

{"a":0,"b":null}

出力 2

{"a":0,"b":1}

出力 3

{"a":2,"b":1}

分割代入の代替演算子:?//

分割代入の代替演算子は、複数の形式のいずれかを取り得る入力を、簡潔に分割代入するための仕組みを提供します。

リソースとそれに関連するイベントのリストを返すAPIがあり、各リソースの最初のイベントのuser_idとtimestampを取得したいとします。このAPIは、XMLから不器用に変換されたため、リソースに複数のイベントがある場合だけ、イベントを配列で囲みます。

{"resources": [{"id": 1, "kind": "widget", "events": {"action": "create", "user_id": 1, "ts": 13}},
               {"id": 2, "kind": "widget", "events": [{"action": "create", "user_id": 1, "ts": 14}, {"action": "destroy", "user_id": 1, "ts": 15}]}]}

分割代入の代替演算子を使うと、この構造の違いを簡単に扱えます。

.resources[] as {$id, $kind, events: {$user_id, $ts}} ?// {$id, $kind, events: [{$user_id, $ts}]} | {$user_id, $kind, $id, $ts}

あるいは、入力が値の配列なのかオブジェクトなのか、はっきりしない場合は、次のように書けます。

.[] as [$id, $kind, $user_id, $ts] ?// {$id, $kind, $user_id, $ts} | ...

各代替パターンで、すべて同じ変数を定義する必要はありません。ただし、名前の付いたすべての変数は、その後の式で使えます。成功した代替パターンで一致しなかった変数は null になります。

.resources[] as {$id, $kind, events: {$user_id, $ts}} ?// {$id, $kind, events: [{$first_user_id, $first_ts}]} | {$user_id, $first_user_id, $kind, $id, $ts, $first_ts}

さらに、その後の式がエラーを返す場合、代替演算子は次の束縛を試みます。最後の代替パターンで発生したエラーは、そのまま伝えられます。

[[3]] | .[] as [$a] ?// [$b] | if $a != null then error("err: \($a)") else {$a,$b} end

実行例 1

コマンド

jq '.[] as {$a, $b, c: {$d, $e}} ?// {$a, $b, c: [{$d, $e}]} | {$a, $b, $d, $e}'

入力

[{"a": 1, "b": 2, "c": {"d": 3, "e": 4}}, {"a": 1, "b": 2, "c": [{"d": 3, "e": 4}]}]

出力 1

{"a":1,"b":2,"d":3,"e":4}

出力 2

{"a":1,"b":2,"d":3,"e":4}

実行例 2

コマンド

jq '.[] as {$a, $b, c: {$d}} ?// {$a, $b, c: [{$e}]} | {$a, $b, $d, $e}'

入力

[{"a": 1, "b": 2, "c": {"d": 3, "e": 4}}, {"a": 1, "b": 2, "c": [{"d": 3, "e": 4}]}]

出力 1

{"a":1,"b":2,"d":3,"e":null}

出力 2

{"a":1,"b":2,"d":null,"e":4}

実行例 3

コマンド

jq '.[] as [$a] ?// [$b] | if $a != null then error("err: \($a)") else {$a,$b} end'

入力

[[3]]

出力 1

{"a":null,"b":3}

関数の定義

"def"構文を使って、フィルターに名前を付けられます。

def increment: . + 1;

その後は、increment を組込み関数と同様にフィルターとして使えます。実際、多くの組込み関数はこの方法で定義されています。関数は引数を受け取ることもできます。

def map(f): [.[] | f];

引数は、値では_なく_、フィルター(引数のない関数)として渡されます。同じ引数を、異なる入力で複数回参照できます。ここでは、f を入力配列の各要素に対して実行します。関数への引数は、値の引数よりも、コールバックに近い動作をします。この点を理解することが大切です。次の例を考えてください。

def foo(f): f|f;
5|foo(.*2)

結果は20になります。f は .*2 であり、最初の f の呼出しでは . が5、2回目では10(5 * 2)になるため、結果は20です。関数の引数はフィルターであり、フィルターは呼び出されるときに入力を必要とします。

単純な関数を定義するために、値の引数としての動作が欲しい場合は、変数を使えばよいだけです。

def addvalue(f): f as $f | map(. + $f);

または、次の省略形を使います。

def addvalue($f): ...;

どちらの定義でも、addvalue(.foo) は、現在の入力の .foo フィールドを配列の各要素に加えます。addvalue(.[]) を呼び出すと、呼出し位置の map(. + $f) の部分が、. の値に含まれる各値に対して1回ずつ評価されることに注意してください。

同じ関数名を使った複数の定義が認められています。再定義すると、関数の引数の数が同じ以前の定義を置き換えますが、再定義より後の関数またはメインプログラムからの参照にだけ適用されます。後述のスコープの節も参照してください。

実行例 1

コマンド

jq 'def addvalue(f): . + [f]; map(addvalue(.[0]))'

入力

[[1,2],[10,20]]

出力 1

[[1,2,1], [10,20,10]]

実行例 2

コマンド

jq 'def addvalue(f): f as $x | map(. + $x); addvalue(.[0])'

入力

[[1,2],[10,20]]

出力 1

[[1,2,1,2], [10,20,1,2]]

スコープ

jqの記号には、値の束縛(「変数」とも呼ばれます)と関数の2種類があります。どちらも字句的なスコープを持ち、式が参照できるのは、自分より「左側」で定義された記号だけです。この規則の唯一の例外は、再帰関数を作れるように、関数が自分自身を参照できることです。

たとえば、次の式には、定義位置の「右側」では見えますが、「左側」では見えない束縛があります。... | .*3 as $times_three | [. + $times_three] | ... です。次の式を考えてみてください。... | (.*3 as $times_three | [. + $times_three]) | ... です。ここでは、束縛 $times_three は、閉じ括弧より後では_見えません_。

isempty(exp)

exp が何も出力しない場合にtrueを返し、それ以外の場合にfalseを返します。

実行例 1

コマンド

jq 'isempty(empty)'

入力

null

出力 1

true

実行例 2

コマンド

jq 'isempty(.[])'

入力

[]

出力 1

true

実行例 3

コマンド

jq 'isempty(.[])'

入力

[1,2,3]

出力 1

false

limit(n; expr)

関数 limit は、最大 n 個の出力を expr から取り出します。

実行例 1

コマンド

jq '[limit(3; .[])]'

入力

[0,1,2,3,4,5,6,7,8,9]

出力 1

[0,1,2]

skip(n; expr)

関数 skip は、最初の n 個の出力を expr から飛ばします。

実行例 1

コマンド

jq '[skip(3; .[])]'

入力

[0,1,2,3,4,5,6,7,8,9]

出力 1

[3,4,5,6,7,8,9]

first(expr), last(expr), nth(n; expr)

関数 first(expr) と last(expr) は、それぞれ、expr の最初と最後の値を取り出します。

関数 nth(n; expr) は、expr が出力するn番目の値を取り出します。nth(n; expr) は、n の負の値に対応していないことに注意してください。

実行例 1

コマンド

jq '[first(range(.)), last(range(.)), nth(5; range(.))]'

入力

10

出力 1

[0,9,5]

実行例 2

コマンド

jq '[first(empty), last(empty), nth(5; empty)]'

入力

null

出力 1

[]

first, last, nth(n)

関数 first と last は、. にある配列の最初と最後の値を、それぞれ取り出します。

関数 nth(n) は、. にある配列のn番目の値を取り出します。

実行例 1

コマンド

jq '[range(.)]|[first, last, nth(5)]'

入力

10

出力 1

[0,9,5]

reduce

reduce 構文は、式のすべての結果を1つの答えへ累積することで、まとめられるようにします。形式は reduce EXP as $var (INIT; UPDATE) です。例として、[1,2,3] を次の式に渡します。

reduce .[] as $item (0; . + $item)

.[] が生成する各結果に対して、. + $item を実行し、入力値0から始めて合計を累積します。この例では、.[] は 1、2、3 を生成するため、次のような式を実行するのと似た効果があります。

0 | 1 as $item | . + $item |
    2 as $item | . + $item |
    3 as $item | . + $item

実行例 1

コマンド

jq 'reduce .[] as $item (0; . + $item)'

入力

[1,2,3,4,5]

出力 1

15

実行例 2

コマンド

jq 'reduce .[] as [$i,$j] (0; . + $i * $j)'

入力

[[1,2],[3,4],[5,6]]

出力 1

44

実行例 3

コマンド

jq 'reduce .[] as {$x,$y} (null; .x += $x | .y += [$y])'

入力

[{"x":"a","y":1},{"x":"b","y":2},{"x":"c","y":3}]

出力 1

{"x":"abc","y":[1,2,3]}

foreach

foreach 構文は reduce と似ていますが、limit や中間結果を生成する集約処理を構築できるようにすることを目的としています。

形式は foreach EXP as $var (INIT; UPDATE; EXTRACT) です。例として、[1,2,3] を次の式に渡します。

foreach .[] as $item (0; . + $item; [$item, . * 2])

reduce 構文と同様に、. + $item を、.[] が生成する各結果に対して実行します。ただし、[$item, . * 2] は各中間値に対して実行します。この例では、中間値が 1、3、6 であるため、foreach 式は [1,2]、[2,6]、[3,12] を生成します。そのため、次のような式を実行するのと似た効果があります。

0 | 1 as $item | . + $item | [$item, . * 2],
    2 as $item | . + $item | [$item, . * 2],
    3 as $item | . + $item | [$item, . * 2]

EXTRACT を省略した場合、恒等フィルターを使います。つまり、中間値をそのまま出力します。

実行例 1

コマンド

jq 'foreach .[] as $item (0; . + $item)'

入力

[1,2,3,4,5]

出力 1

1

出力 2

3

出力 3

6

出力 4

10

出力 5

15

実行例 2

コマンド

jq 'foreach .[] as $item (0; . + $item; [$item, . * 2])'

入力

[1,2,3,4,5]

出力 1

[1,2]

出力 2

[2,6]

出力 3

[3,12]

出力 4

[4,20]

出力 5

[5,30]

実行例 3

コマンド

jq 'foreach .[] as $item (0; . + 1; {index: ., $item})'

入力

["foo", "bar", "baz"]

出力 1

{"index":1,"item":"foo"}

出力 2

{"index":2,"item":"bar"}

出力 3

{"index":3,"item":"baz"}

再帰

前述のとおり、recurse は再帰を使い、どのjq関数も再帰関数になれます。組込み関数 while も再帰によって実装されています。

再帰呼出しの左側にある式が最後の値を出力するとき、末尾呼出しが最適化されます。実際には、再帰呼出しの左側にある式が、各入力に対して2つ以上の出力を生成しないようにするという意味です。

例:

def recurse(f): def r: ., (f | select(. != null) | r); r;

def while(cond; update):
  def _while:
    if cond then ., (update | _while) else empty end;
  _while;

def repeat(exp):
  def _repeat:
    exp, _repeat;
  _repeat;

ジェネレーターとイテレーター

jqの演算子や関数の中には、各入力に対して0個、1個、または複数の値を生成できるという意味で、実際にジェネレーターであるものがあります。ジェネレーターを持つ他のプログラミング言語と同様です。たとえば、.[] は入力のすべての値を生成します。入力は配列かオブジェクトでなければなりません。range(0; 10) は0から10の間の整数を生成する、というようになります。

カンマ演算子もジェネレーターです。まずカンマの左側の式が生成する値を生成し、その後にカンマの右側の式が生成する値を生成します。

組込み関数 empty は、出力を0個生成するジェネレーターです。組込み関数 empty は、直前のジェネレーター式へバックトラックします。

すべてのjq関数は、組込みジェネレーターを使うだけでジェネレーターになれます。再帰とカンマ演算子だけを使って、新しいジェネレーターを構築することもできます。再帰呼出しが「末尾位置」にあれば、ジェネレーターは効率的になります。以下の例では、_range が自分自身を呼び出す再帰呼出しが末尾位置にあります。この例は、末尾再帰、ジェネレーターの構築、下位関数という3つの高度な話題を示します。

実行例 1

コマンド

jq 'def range(init; upto; by): def _range: if (by > 0 and . < upto) or (by < 0 and . > upto) then ., ((.+by)|_range) else empty end; if init == upto then empty elif by == 0 then init else init|_range end; range(0; 10; 3)'

入力

null

出力 1

0

出力 2

3

出力 3

6

出力 4

9

実行例 2

コマンド

jq 'def while(cond; update): def _while: if cond then ., (update | _while) else empty end; _while; [while(.<100; .*2)]'

入力

1

出力 1

[1,2,4,8,16,32,64]