4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hello World あたたたた 24日目 jq編

4
Last updated at Posted at 2025-12-23

この記事は Hello World あたたたた Advent Calendar 2025 の20日目の記事です。

今日はjqで「Hello World あたたたた」を実装して解説していきます。

そもそも「Hello World あたたたた」が何かは1日目の記事をご覧ください。

……えっ、“jq”ってあのJSONを処理するコマンドじゃないのか、ですか。はい、そのjqのことです。そしてjqの公式のWikiを見ると「jqは関数型プログラミング言語である」と明言されています。実際、jqには条件分岐も反復処理も変数も関数定義もありプログラミング言語としての機能は揃っています。

コーディング例

jq Playgroundで実際に動かしてみることができます。

hwata.jq
# 乱数生成器の状態; 現在時刻で初期化
{seed: ((now * 1000 | floor) % 2147483647)}
# 出力する予定の文字列を保存する配列
| .output = []
# プログラムを続けるかどうかのフラグ
| .running = true
# 出力した文字をためていく文字列
| .hako = ""

| until(.running | not;
    # 乱数生成器(MINSTD)の状態更新
      .seed |= . * 48271 % 2147483647
    # 乱数(0か1)生成してその値に応じて「あ」または「た」を選ぶ
    | .char = if (.seed / 131072 | floor) % 2 == 0 then "あ"
              else "た"
              end
    # "出力"に追加する
    | .output += [.char]
    # hakoに追加する
    | .hako += .char
    # 最後の5文字が "あたたたた" なら終了
    | if (.hako | endswith("あたたたた")) then
          .running = false
        | .output += ["\nお前はもう死んでいる"]
      else .
      end
  )
# outputを連結したものを出力とする
| .output | add

このjqのスクリプトファイルhwata.jqの実行方法は以下の通りです。

jq -n -r -f hwata.jq
  • -n: 入力としてnullを与えます。つまりecho null | jq ...を実行するのと同等です。jqのプログラムはフィルタであり必ず入力を必要とする1ためダミーの入力値を与えているわけです。
  • -r: 出力の文字列をJSONリテラルでなくそのまま標準出力に出します。
  • -f: コマンドライン引数にスクリプトの文字列(ワンライナー)ではなくファイル名を与えることを指示します。

コードと文法の解説

jqの言語設計

jq ではフィルタを繋いでプログラムを作ります。UNIX系OSのシェルのパイプライン、あるいはオブジェクト指向言語のメソッドチェーンに似ています。

(now * 1000 | floor)
  • floorは数値を入力して小数部を切り捨てた値を出力します。この式ではnow * 1000の値(出力)がfloorに入力され、またfloorの出力がこの式全体の出力となります。
  • 関数や演算子は全てフィルタであり、入力と出力をもちます。nowは現在日時のシリアル値を出力する関数ですが、nowも(無視される)入力をもちます。
  • 入力以外に引数をもつ関数(endswith等)もあり、また演算子は引数をもちます。
  • 1000等の定数表記もフィルタであり、例えば1000は入力を無視して1000を出力します。
  • (now * 1000 | floor)全体が(無視される)入力と出力をもつ一つのフィルタとなっています。

フィルタは必ず単一の入力をもつ2ため、今回のプログラムではループ中の状態を1つのオブジェクトにまとめています。

{
  "running": プログラムを続けるかどうかのフラグ(真偽値),
  "char": 選択された文字(文字列),
  "hako": 累積した文字列(文字列),
  "seed": 乱数生成器の状態(数値),
  "output": 出力すべきデータ(配列)
}

今回のプログラムでは「jqのループ関数(until)を使う」という条件を課した都合で「出力を都度行う」のが困難だったため、出力すべき値の列を内部に溜めることにしました。

代入

jq は関数型言語であり破壊的代入の概念はありません。=演算子は「入力の値の一部(または全部)を更新した値を出力する」という動作になります。

# 入力のオブジェクトの".running"の値をtrueに変更したものを出力
.running = true
  • このフィルタに{"seed": 3}を入力すると{"seed":3, "running":true}が出力されます。
  • 左辺の.runningは「入力オブジェクトのトップレベルの"running"キー」という場所を示し、これをパス式といいます。
  • 代入の対象以外の場面(例えば.seed / 131072等)でパス式が現れた場合、それは「入力に対する当該の場所」のを表します。

|=も代入演算子の一種ですが、これを使うと右辺に対する入力が(式全体への入力でなく)左辺のパス式の値となります。

# これは".seed = .seed * 48271 % 2147483647"と同じ
.seed |= . * 48271 % 2147483647

繰り返し

繰り返しはそれ専用の関数で表されます。関数なのでこれもフィルタの一種です。

until(.running | not;
    処理
)
  • 第1引数の(フィルタを入力に適用した)値が偽である間、入力に第2引数のフィルタを適用し続けます。
  • whileという関数もあるのですが、whileuntilは「条件部が真か偽か」以上に挙動が異なります。問題の仕様ではwhileは適さなかったのでuntilを使いました。
  • 実はuntilの挙動もあまり適してなくて、そのため「出力を一旦状態中(.output)に溜める」処理が必要になっています。再帰関数を自分で定義するのが最適(後述)なのですが、そうすると問題で指定したロジックから大きく外れます。

条件判定

条件判定は専用の構文(if式)で表しますが、これも入力と出力をもつフィルタです。

if (.seed / 131072 | floor) % 2 == 0 then "あ"
else "た"
end
  • 乱数の値(==の左辺の式)により"あ"または"た"が出力されます。

文字の端末(標準出力)への出力

複数のフィルタを繋いでプログラムを作るので、最後のフィルタが出力した値がプログラム全体の出力となり、この値が端末(標準出力)に出力されます。既定はJSON形式で出力されますが、今回は文字列の(リテラルでなく)中身を出力したいので、jqコマンドに-rオプションを付けています。

先述の通り、今回のプログラムでは処理の都合で状態オブジェクトのoutputキーに「端末に出力すべき値の列」を溜めているので、untilの出力のオブジェクトは以下のようになっています。

{ // "output"キーに文字列の配列が入っている
  "output": ["", "", ..., "", "\nお前はもう死んでいる"], ...
}

従って、until関数の後に以下のようにフィルタを繋げれば所望の出力3が得られます。

| .output  # 入力のオブジェクトの".output"パス式の値を出力
| add      # 入力の配列の要素の文字列を連接した1つの文字列を出力

文字列の連結

文字列の連結は+演算子で行えます。

# ".hako"を末尾に".char"を追加した文字列に更新したものを出力
.hako += .char
  • この式は.hako = .hako + .charと同値です。
  • .hako + .charは2つの文字列を連結した値を出力します。

文字列から最後の5文字を取り出す

jq には「指定した部分文字列で終わるか」を判定する関数があります。

endswith("あたたたた")
  • endswithは入力の文字列が第1引数の文字列で終わるかを判定して結果の真偽値を出力します。

乱数生成

jq には乱数の機能はないので、自前で単純な線形合同法の疑似乱数(MINSTD)を実装しています。使っているアルゴリズムをJavaで書くと以下の通りです。

// 乱数発生器の状態を現在日時の値を用いて初期化
long seed = System.currentTimeMillis() % 0x7FFFFFFFL

// 以下の操作で"randomBit"に1ビットの乱数を取得
seed = seed * 48271L % 0x7FFFFFFFL
long randomBit = (seed >> 17) & 1L

このコードの変数seedに相当する値を内部状態オブジェクトのseedキーに含めてあります。

jqの概要と歴史

jqの特徴

  • JSONの処理のためのドメイン特化言語(DSL)。
  • 「JSONの世界のawk」を目指している。
  • 主にワンライナーとしての使用を想定して、読みやすさよりも書きやすさを重視している。
  • UNIXのパイプラインの思想を継承している。

jqの歴史

  • 開発者: jqlang という組織。原作者は Stephen Dolan。
  • 誕生: 2012 年に初版をリリース。
  • 名前の由来: 公式の情報がないため不明。

個人的なコメント

jqの言語についてちゃんと知りたかったので、登録しました:sweat_smile:

おまけ:もう少しマシな実装

ループ関数(until)を使う代わりに、自分で定義した再帰関数を用いてループを行うともう少し単純なコードになります。

# ループの中の処理.
def _iterate:
      .seed |= . * 48271 % 2147483647
    | .char = if (.seed / 131072 | floor) % 2 == 0 then "あ"
              else "た"
              end
    | .char, # 文字を出力する
      (      # その後に後続処理
          .hako += .char
        | if (.hako | endswith("あたたたた")) then
              "\nお前はもう死んでいる"
              # "_iterate"しないのでループ終了
          else _iterate
          end
      );
# 本体.
  [
      { # "running"と"output"は不要
        seed: ((now * 1000 | floor) % 2147483647),
        hako: ""
      } | _iterate
  ] # 出力された文字を集約する
| add  # 連結して終了

こちらのプログラムでは途中でパイプラインを分岐させてループ(再帰関数)が「複数の値を出力する」恰好になっています。

  1. この辺りの事情はAWKでの話と同じです。冗長なecho null |を書かなくて済むように特別にオプションが設けられているわけです。

  2. プログラム実行中に複数のフィルタが動作したり、1つのフィルタが繰り返し使用されることはありますが、1つのフィルタが1回の実行で入力する値は1つです。

  3. なお、jqの端末出力の仕様として、値を出力する際には末尾に改行が追加されます。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?