jq の復習

jq の復習

jq コマンドってやりたいことができたら満足しがちで、基本的な部分を理解しないまま使っているので、少し振り返ってみる。

要はマニュアルを読めばいいのだけど、ちゃんと理解していなかった部分を抜き出してみる。気になっていたのは

  • 英語の単語が使われているものはまだいいんだけど、記号の理解があいまい(フィルター |、カンマ ,、カッコ ()など)
    • 代入(=|=)もなんか癖がありそう
  • デバッグ用の機能

jq はフィルター

ある程度、jq コマンドを使ってからマニュアルの冒頭を読むと味わい深い。

  • フィルターは入力を受け取り、出力を生成する
  • フィルターは組み合わせることができる。パイプでつないだり、フィルターの出力を配列にまとめたり
  • いくつかのフィルターは複数の結果を出力する。配列を次のフィルターに渡すことで、配列の各要素に次のフィルターが適用される。→他の言語ではループやイテレーションで行われることが、jq では大体フィルターをつなげることで実現される
  • すべてのフィルターには入力と出力がある
    • 文字列や数値の定数ですらフィルター。入力を受け取り、通常は自身と同じ値を出力する
    • フィルターを組み合わせる場合、どのフィルターにも同じ入力が適用され、結果が結合される
      • 平均を求める add / length に配列を入力した場合、配列の各要素が addlength に入力されて除算が行われる

Invoking jq / jq の起動

-f filename / --from-file filename:

awk と同じでスクリプトを別のファイルから読み込める。'#' でコメントもかける。 ワンライナーで長いものを書いていると時々、わけわからなくなるので助かるかも

Basic filters / 基本的なフィルター

Comma: ,

2つのフィルターがカンマで区切られていた場合、同じ入力がどちらにも入力されて、それぞれの出力が連結される。 配列の添え字のカンマ区切りもOK。'.[4,2]' とか。

Pipe: |

左側のフィルタの出力を右側のフィルタの入力に供給する

Parenthesis () / 括弧

括弧は、一般的なプログラミング言語と同様に、グループ化演算子として機能します。

Types and Values

Recursive Descent: .. / 再帰的下降

. の配下を再帰的に探索して、すべての値を生成する。これは、引数なしの recurse 組み込み関数と同じです。これは、XPath// 演算子に似せて作りました。 ..a は動かないので、代わりに ..|.a を使用してください。以下の例では、..|.a? を使って、. の「下」にあるオブジェクトのキー "a " のすべての値を検索しています。

..は、path(EXP)? 演算子と併用すると特に便利です。

Builtin operators and functions / 組み込み演算子と関数

empty

何も返さない。nullすら返さないで空を返す。

error(message)

エラーを発生させる(例外をスローする)

halt

jq プログラムを終了

halt_error, halt_error(exit_code)

jq プログラムをエラー終了

String interpolation: \\(exp) / 文字列への差し込み

文字列リテラルの中に動的な値を入れ込む

Conditionals and Comparisons / 条件と比較

Alternative operator: // / 代替演算子

falsenull だった場合に代わりの値/デフォルトを設定

Error Suppression / Optional Operator: ? / エラー抑止/オプションの演算子

エラーが出ても無視される。try を付けるのと一緒。

Advanced features / 高度な機能

Variable / Symbolic Binding Operator: ... as $identifier | ...

変数宣言

Destructuring Alternative Operator: ?// / 構造分割の代替演算子

オブジェクトの構造が何パターンかあるうちの一つを取る場合に、構造の変化に対応できる。 マニュアルに書いてあるのは、あるAPIが一つのオブジェクトの場合はオブジェクト、複数のオブジェクトの場合はオブジェクトの配列を返すような場合の対応に使えるとある。

Scoping

変数名や関数名のスコープについて説明あり(必要になったら見る…)

I/O

input

一つの入力を出力する

inputs

残りの入力をすべて出力する

debug, debug(msgs)

stderr にメッセージを出力。

stderr

入力をstderrに出力

input_filename

処理中のファイル名

input_line_number

行番号

Assignment / 代入

他の言語と異なり、jq は参照とコピーを区別しません。二つのオブジェクトまたは配列は、それらが同一のオブジェクトか違うオブジェクトかという概念とは無関係に、同じであるか/同じではないかのどちらかになります。

.bar = .foo と代入した後に、.foo に何かを加えたとしても、.barは大きくなりません。他の言語に慣れているなら、jq は代入の前にfull deep copy をやっていると考えていいでしょう。(性能の観点から実際にはfull deep copy をやっているわけではありませんが、概念的にはそう考えることができます。)

これは、jq で循環値 (最初の要素がそれ自体である配列など) を構築することは不可能であることを意味します。これは非常に意図的なもので、jq プログラムが生成するものはすべて JSON で表現できるようにしています。

jq のすべての代入演算子の左側 (LHS: left-hand side) にパス式があります。右側 (RHS: right-hand side) は、LHS パス式で指定されたパスに設定する値を指定します。

jq の値は常に不変(immutable)です。内部的な動作では、代入は、リダクション(reduction) 1 を使用して、必要なすべての代入が . に適用された新たな置換値を計算し、更新された値を出力します。これは、次の例を見ると明らかになるでしょう。{a:{b:{c:1}}} | (.a.b|=3), . これは、 {"a":{"b":3}}{"a":{"b":{"c":1}}} を出力します。最後の部分式 . が変更された値ではなく元の値を参照するためです。

ほとんどのユーザーは、単純な代入=ではなく、|=+=などの更新代入を使用することになるでしょう。

代入演算子の左辺は . の中の値を参照することに注意してください。したがって、 $var.foo = 1 は期待どおりに動作しません ($var.foo.における有効または有用なパス式ではありません)。代わりに以下を使用してください $var | .foo = 1

次の点も注意してください。.a,.b=0 では .a.b は設定されず、両方を設定するには(.a,.b)=0となります。

Update-assignment: |= / 更新代入

|= は「更新」演算子です。これは右辺にフィルタを取り、古い値をこの式に通すことで、. のプロパティの新しい値を計算し、代入します。 たとえば、(.foo, .bar) |= .+1 では、入力fooをプラス 1、入力barをプラス 1したフィールドを持つオブジェクトを構築します。

左側には任意の一般的なパス式を使用できます。path()を参照してください。

代入演算子の左辺は . の中の値を参照することに注意してください。したがって、$var.foo |= . + 1 は期待どおりに動作しません ($var.foo.における有効または有用なパス式ではありません)。代わりに以下を使用してください $var | .foo |= . + 1

右側が値を出力しない場合 (例:empty)、左側のパスは del(path) と同様に削除されます。

右側が複数の値を出力する場合、最初の値のみが使用されます (互換性に関する注意: jq 1.5 以前のリリースでは、最後の値のみが使用されていました)。

Arithmetic update-assignment: +=, -=, *=, /=, %=, //= / 算術更新代入

更新代入と同様になる。例えば、 += 1|= . + 1 と同じ。

Plain assignment: = / 単純代入

これは単純な代入演算子です。他とは異なり、右側 (RHS) への入力は、左側 (LHS) への入力と同じであり、左辺のパスの値が入力ではありません。RHS によって出力されるすべての値が使用されます。

=の右辺が複数の値を生成する場合、jq はそれぞれの値ごとに左側のパスをその値に設定し、変更された . を出力します。例えば、たとえば、(.a,.b) = range(2){"a":0,"b":0} および {"a":1,"b":1} を出力します。

次の例は =|= の違いを示します: 入力データ {"a": {"b": 10}, "b": 20} に以下を行います。

      .a = .b
      .a |= .b

前者は入力の a フィールドに、入力の b フィールドをセットします。結果は {"a": 20, "b": 20} です。 後者は入力の a フィールドに、入力の a フィールドの中の b フィールドをセットします。結果は {"a": 10, "b": 20} です。

Complex assignments / 複雑な代入

jq の代入の左側では、ほとんどの言語よりも多くのものが許可されます。左側で単純なフィールド アクセスをすでに見てきましたが、配列アクセスも同様に機能することは驚くことではありません。

     .posts[0].title = "JQ Manual"

驚かれるかもしれませんが、左側の式は入力ドキュメント内の異なる点を参照して複数の結果を生成する可能性があることです。

    .posts[].comments |= . + ["this is great"]

この例では、入力内の各投稿の「comments」配列に文字列「this is great」を追加します (入力は投稿の配列であるフィールド「posts」を持つオブジェクトです)。

jq が「a = b」のような代入に遭遇すると、a の実行中に入力ドキュメントの一部を選択するために取られた「パス」を記録します。このパスは、割り当ての実行中に変更する入力の部分を見つけるために使用されます。等号の左側では任意のフィルターを使用できます。入力から選択されたパス全てが代入の実行場所になります。

これは非常に強力な操作です。上記と同じ「ブログ」入力を使用して、ブログ投稿にコメントを追加したいとします。今回は「stedolan」さんの投稿のみにコメントしたいと思います。「select」関数を使用して、これらの投稿を見つけることができます。

      .posts[] | select(.author == "stedolan")

この操作によって提供されるパスは、「stedolan」さんが書いた各投稿を指しており、前と同じ方法で各投稿にコメントできます。

      (.posts[] | select(.author == "stedolan") | .comments) |=
          . + ["terrible."]

セミコロン ;

マニュアルにはセミコロンについて書いてない。イシューも出ているようだ。

セミコロンは、import、include、def関数本体の終端や、2つ以上の引数を持つ関数の引数の区切りに必要。


  1. ちょっと reduction がうまく訳せませんでした。