jq 1.7をリリースしました

jqがjqlang organizationに移譲され、数名の新たなメンテナーを入れた開発体制に移行してから三か月が経ちました。 私にとってこの三か月はとても濃厚で、これまでのOSS活動の中でも特に大変な期間でした。 itchyny.hatenablog.com github.com

リポジトリの管理権限をいただいてからまずやったことは、既存のissueやPRの整理でした。 500ほどのissueとPRに目を通し、ラベルをつけて、解決済みのものを閉じて、直近で入れたいものを独断でリリースマイルストーンに入れていきました。 この整理がついた頃には他のメンテナの活動も活発になり、私の作ったマイルストーンのissueやPRを確認してくれました。

そして先日、ようやく1.7をリリースしました。 1.6から実に五年弱、一時は開発が完全に止まってしまいプロジェクトの存続を危ぶむ声も上がるような状況からの再起をかけたリリースになりました。 github.com

詳しくはリリースノートを参照していただければ良いと思いますが、この記事でも主な変更点をあげておきましょう。

数値の精度の保持

JSONの中やjqクエリの数値の精度を保持し、比較したりソートしたりできるようになりました。 数値形式のIDやREST APIナノ秒など、JSONの中の数値が浮動小数点として解釈されたくない場面で役に立ちます。 この機能は2019年10月には実装されてマージされていましたが、正式にリリースがされていなかったため何度も新しいissueが立てられる問題でした。 なお、単項のマイナスや二項演算子fabsroundなどの数学的なフィルターが絡むと浮動小数点の精度に落ちます。

 $ echo '{"id":100000000000000000}' | jq .
{
  "id": 100000000000000000
}
 $ jq -n -c '[100000000000000001, 100000000000000003, 100000000000000004, 100000000000000002] | sort'
[100000000000000001,100000000000000002,100000000000000003,100000000000000004]

github.com

コマンド出力の色

暗いテーマの端末にてnullの色が見にくいという問題を解消しました。 これまではデフォルトでBold Black (\e[1;30m) が使われていましたが、Bright Black (\e[0;90m) に変更しました。 デフォルト色を変更するというのは難しい判断でしたが、設定のない状態で多くの人に快適に使ってもらえるべきと考えて変更しました。 ⭐️

github.com

オブジェクトのキーの色をJQ_COLORS環境変数で設定できるようになりました。 これまで、nullやBoolean、数値や文字列などの色はJQ_COLORS環境変数によって設定が可能でしたが、オブジェクトのキーだけは設定できないようになっていました。 JSONの型と色付けの設定が一対一対応しているという実装上の制約がありましたが、オブジェクトのキーの色を変更したいという要望は多く、JQ_COLORSの最後の要素で指定できるように拡張しました。 ⭐️

github.com

さらにNO_COLOR 環境変数が設定されていたら色をつけずに出力するようになりました。 多くのコマンドラインツールがサポートしている環境変数であり、色のついた出力を望まないユーザーには便利な機能です。 なお --color-output (-C) が指定された場合はそちらが優先されます。 ⭐️

github.com no-color.org

より多くのプラットフォームをサポート

リリースのビルド済み実行ファイルとして、より多くのプラットフォームのためにクロスビルドして公開するようになりました。 以下のアーキテクチャのビルド済み実行ファイルを公開しています。

  • Linux: amd64, arm64, armel, armhf, i386, mips, mips64, mips64el, mips64r6, mips64r6el, mipsel, mipsr6, mipsr6el, powerpc, ppc64el, riscv64, s390x
  • macOS: amd64, arm64
  • Windows: i386, amd64

github.com

さらに、GitHub Container Registry ghcr.io/jqlang/jq にDockerイメージをpushするようになりました。 マルチアーキテクチャ (386, amd64, arm64, mips64le, ppc64le, riscv64, s390x) のイメージをGitHub Actionsでビルドしています。 ⭐️ github.com

他の人に助言をいただいたのですが、QEMUによるエミュレーションの上でビルドするのは時間がかかるため、ビルド済みのバイナリをscratchイメージにCOPYするだけになっています。 ⭐️ github.com

言語の改善

|= emptyで配列の要素の削除

jqには empty という特殊なフィルターがあり、空のストリームを表します。 更新代入オペレーター (|=) の右辺に使うことでオブジェクトのフィールドを削除できますが、配列の要素に使うと意図しない要素を削除してしまうバグがありました。 削除処理を順番に行っていることでインデックスがズレて行ってしまうのが問題でした。 削除対象のインデックスを集めて最後にまとめて削除することで解決しました。 ⭐️

 $ jq -n '{foo: 1, bar: 2, baz: 3} | .bar |= empty'
{
  "foo": 1,
  "baz": 3
}
 $ jq -n '[range(5)] | (.[] | select(. % 2 == 0)) |= empty' # 1.6 では [1,2,4]
[
  1,
  3
]

ただし、普通は del/1 を使うのが良いと思います。

github.com

try-catchのバグ修正

jq 1.6では、try-catch を更新代入オペレーターの右辺に使うと意図しない挙動をすることがありました。 六件報告されていたこの問題は、エラーハンドリングの内部処理のバグに起因するものでした。

 $ jq -n '["true","[]","foo","}{"] | .[] |= try fromjson catch "x"' # 1.6 では ["x","x","x","x"]
[
  true,
  [],
  "x",
  "x"
]
 $ jq -n '["1", "2a", "3", 4] | .[] |= tonumber? // .' # 1.6 では ["2a",4]
[
  1,
  3,
  4
]

github.com

オブジェクトのキーとして変数をサポート

オブジェクトのキーに変数をそのまま使えるようになりました。 これまでは括弧が必要でした。

 $ jq -n '"test" as $key | {$key:42}' # == {($key):42}
{
  "test": 42
}
 $ jq -n '"test" as $key | {$key}' # == {key: $key} なので注意
{
  "key": "test"
}
 $ jq -n '"test" as $key | {$key:$key}'
{
  "test": "test"
}
 $ jq -n '{key:"test"} | {key}' # == {key: .key}
{
  "key": "test"
}

ドットの連鎖をサポート

オブジェクトや配列のインデキシング (.["foo"], .[0]) やイテレータ (.[], .[]?) を続けるときに、.を省略せずに書くことが可能になりました。

 $ jq -n '{foo:[]} | .foo.[0], .foo.["foo"]?, .foo.[], .foo.[]?'
null

# これまでは . を書かない構文のみサポートしていた
 $ jq -n '{foo:[]} | .foo[0], .foo["foo"]?, .foo[], .foo[]?'
null

else節のないif式をサポート

if式のelse節を省略できるようになりました。 省略した場合は else . と同じです。 jqのように常に入力値のある言語での"何もしない"というのは入力値をそのまま返す操作だからです。

 $ jq -n '5,15 | if . < 10 then . * 2 end'
10
15

NULセパレータの出力オプション

jqは通常出力を改行区切りで出力します。 これは文字列をクォートせずに出力する--raw-output (-r)オプションでも同様ですが、文字列が改行を含みうる場合には他のコマンド (read, xargs) に渡すのが安全ではないケースがあります。

この問題を解決するために、--raw-output0オプションが導入されました。 文字列をクォートせずに出力する上に、セパレータとしてNUL (\x00)を使います。 jqの出力をread -d ''xargs -0で処理するときに便利です。

 $ jq -n --raw-output0 '"a b c", "d\ne\nf"' | xargs -0 printf '[%s]\n'
[a b c]
[d
e
f]

# 文字列自体がNULを含む場合はエラー
 $ printf '{"foo":"foo\\u0000bar"}' | ./jq --raw-output0 .foo
jq: error (at <stdin>:0): Cannot dump a string containing NUL with --raw-output0 option

新しいビルトインフィルター

以下のビルトインフィルターが追加されました。

  • pick/1: 引数で辿った構造を抜き出します。以前のバージョンより {foo}{ foo: .foo } と同じクエリですが、{ foo: { bar: .foo.bar } } に相当する簡潔な記述方法がありませんでした。今後は pick(.foo.bar) と簡潔に書けるようになります。さらに、pick(.. | strings | select(contains("GITHUB_")))といった高度な利用方法もあります。
  • abs/0: 数値の絶対値を計算します。fabslengthなどの同様のフィルターはすでに存在していますが、よりわかりやすい名前とシンプルな実装で導入されました。現在は精度が浮動小数点になりますが、将来的に整数の精度を維持する実装になる可能性があります。
  • debug/1: debug/0と同様、標準エラー出力デバッグ出力を出すフィルターですが、より柔軟に出力を制御できます。例えば debug("id: \(.id), name: \(.name)") のように必要なフィールドのみをデバッグすることが可能です。
  • scan/2: scan/1と同様、入力文字列から正規表現にマッチする部分文字列を抜き出しますが、正規表現のフラグに対応しています。 ⭐️

ウェブサイトのリデザイン

公式のウェブサイト https://jqlang.github.io/jq/ のデザインをリニューアルしました。 最初にBootstrap 3で作られて以来、誰も手をつけていなかったウェブサイトのデザインを一新して、モダンなサイトに作り替えました。 もちろんダークモードやレスポンシブの対応、svgアイコンの作成なども行いました。 ⭐️

github.com

まとめ

五年という歳月を経て、jqの新しいバージョンがリリースされました。 この記事に書いたもの以外にもメモリーリークやセグフォを含む様々なバグが修正されています。 OSS-Fuzzが導入され、スタック保護が有効になるなど、セキュリティ面でも様々な改善が導入されました。 私も⭐️をつけた項目を実装したり、他の人のPRをレビューしたりして、新たなリリースに貢献しました。

jqは現代の開発になくてはならないツールになりました。 私も開発プロセスのあらゆる場面で依存していて、メンテナンスが滞ってセキュリティリスクが高まっても代替を探すのが困難です (gojqに切り替えれば良い説もあります)。 世界中に多くのユーザーがいる中で、バグに見える挙動にも依存しているスクリプトがあることをいつも想像しなくてはいけません。 このように"安定している"ことが期待されるツールを問題なくリリースするのは、かなり緊張感のある作業でした。 二回のRCバージョンを出してユーザーにテストを促しましたが、それでもリリース後に細々としたバグ報告がされています。 新たなメンテナ体制の中で様々なことを学びつつ、この特別なツールの開発を続けていきたいと思っています。

github.com