ぶていのログでぶログ

思い出したが吉日

rfコマンドv1.29.0をリリースした

前回はこちら。 前回ブログを書いたのが去年の12/23だったので、10ヶ月ぶりのブログにする! その間にもほそぼそと更新していてv1.24.0からv1.29.0になったので主な更新点を紹介。 なお、このブログ記事を書いている間にv1.31.0をリリースしてしまったのでそれは別で記事にします…。

[!NOTE] rfコマンドアドベントカレンダーを作りました!Tipsなんかを書いていくはず…はず https://qiita.com/advent-calendar/2025/rf_command

主な更新点

  • CLIフレームワークをmagniに変更
  • フィルタタイプの切り替えをオプション指定からサブコマンドに移行
  • 複数回-eオプションを指定可能に変更
  • --grepオプションを廃止してgrepサブコマンドに移行
  • JSON <-> YAMLの変換が簡単にできるようになった
  • Array/HashをMarkdownテーブルに変換できるようになった
  • レシーバーをContainerからRecordに変更
  • バイナリマッチングの処理方法を変更
  • OnigRexexp#onの追加

CLIフレームワークをmagniに変更

いままでrfは独自フレームワークを使っていたのだが、自作のCLIフレームワークmagniに変更した。 magniについては前回ブログで書いたとおり。 本当のことを書くとmagniを作ったからrfも乗り換えたというわけではなくて、rfに機能を追加したくてmagniを作ったというのが正しい。 後述するのだけどrfの新機能で複数回オプションを指定できるようにしたくなったのだけど、当時のオプションパーサー周りを書き換えようとすると大規模な変更が必要となり、そのためにゴリゴリコードを変更するのは…っとなり、それならばっとCLIの関心ごとを分離してmgemにしたほうがいいだろうとなってできたのがmagniだった。

実際、rfのためにmagniを作ったので出来は気に入っている。 必要な機能があればmagniに追加すれよいし、関心ごとの範囲が狭まるのでとてもよい。 magniに変えて良いことばかりではなく、magniはThorを真似て作っているのでサブコマンド方式になっている。 これによりrfのオプション指定が変わってしまったというのが次の更新点。

フィルタタイプの切り替えをオプション指定からサブコマンドに移行

magniに変えたことで今までフィルタのタイプを--json(--type json)や--yaml(--type yaml)で指定していたものを、サブコマンドで指定することになった。 具体的には以下の通り。

# rf 1.25.0までのJSONフィルタの指定
$ rf --json _.size test.json
$ rf -j _.size test.json     # 短縮形も指定できる

# rf 1.26.0からのJSONフィルタの指定
$ rf json _.size test.json
$ rf j _.size test.json    # 短縮可能

こうしてみるとサブコマンド化したことで -(ハイフン)が不必要になり、むしろタイプ数が減ったのでは?!っとなったのでこの形式を採用した。 とはいえ、READMEなどのドキュメントを直すハメになったのだった…*1

複数回-eオプションを指定可能に変更

magniに変更して一番やりたかった変更がこれ。 要はsedにおける-eオプションと同じことをしたいというのがモチベーション。 たとえば、ログファイルを特定の時間で抽出してかつ時間部分を削って出力するとか、特定のIPレンジのインターフェイスを持つサーバを列挙してそのホスト名を装飾して出力するとか。

# ChatGPTに生成させたsyslogのサンプル
$ cat sample.log
Jan 25 10:14:32 web01 systemd[1]: Started Session 3245 of user deploy.
Jan 25 10:14:33 web01 sshd[18342]: Accepted publickey for deploy from 203.0.113.24 port 51432 ssh2
Jan 25 10:14:33 web01 sshd[18342]: pam_unix(sshd:session): session opened for user deploy by (uid=0)
Jan 25 10:14:35 web01 sudo[18367]:   deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/bin/systemctl restart nginx
Jan 25 10:14:36 web01 systemd[1]: nginx.service: Starting service...
Jan 25 10:14:36 web01 systemd[1]: nginx.service: Started nginx - high performance web server.
Jan 25 10:15:01 web01 CRON[18410]: (root) CMD (/usr/local/bin/backup --incremental)
Jan 25 10:15:32 web01 kernel: [ 4234.112312] eth0: link up, 1000Mbps, full-duplex, lpa 0xC5E1
Jan 25 10:16:02 web01 kernel: [ 4264.813551] EXT4-fs (vda1): re-mounted. Opts: errors=remount-ro

# Jan 25 10:15のCRONログだけ出力する
$ rf -e '/Jan 25 10:15:01/' -e '/CRON/' sample.log
Jan 25 10:15:01 web01 CRON[18410]: (root) CMD (/usr/local/bin/backup --incremental)

…どちらも正規表現を書けばできてしまう気がするが、-eで分割することで複雑になりすぎなくなるはず。

--grepオプションを廃止してgrepサブコマンドに移行

コマンド引数をRubyのコードではなく正規表現として解釈して、それにマッチしたレコードのみ出力するという--grepオプションがあった。 テキストはもちろんのこと、JSONやYAMLでも使えたのだが、rfを使っていてJSON/YAMLで--grepを使う場面がなかったので、オプションから分離してサブコマンドに変更した。

# 1.25.0までは--grepオプションだった
$ rf --grep sudo sample.log
Jan 25 10:14:35 web01 sudo[18367]:   deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/bin/systemctl restart nginx

# 1.27.0以降はサブコマンドになった
$ rf grep sudo sample.log
Jan 25 10:14:35 web01 sudo[18367]:   deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/bin/systemctl restart nginx

ちなみに、手元のマシンでは alias grep="rf grep" して使っている。これで足りない機能は順次追加している。ドッグフーディング重要。

JSON <-> YAMLの変換が簡単にできるようになった

to_json/to_yamlでオブジェクトをjson/yamlに変換できていたのだが、json/yamlフィルタを使ってファイルを読み出しているときにそのままto_json/to_yamlしてしまうと、それぞらのファーマットにおける文字列として出力されていた。 これはフィルタが出力する形式への変換も担っていたことに起因していたので、フィルタから形式変換する部分を切り出し新たにformatterという層を作った。 これによりto_json/to_yamlしたときにフィルタに依存せずに出力形式を選べるようになった。

$ cat hash.json
{
    "foo": 1,
    "bar": {
        "baz": [
            "a",
            "b",
            "c"
        ]
    },
    "foo bar": "foo bar"
}

# 1.28.0まではputs _.to_yamlみたいにする必要があった
$ rf --json -q 'puts _.to_yaml' hash.json
---
foo: 1
bar:
  baz:
    - a
    - b
    - c
foo bar: foo bar

# 1.29.0からはto_yamlだけで変換できるようになった
$ rf json to_yaml hash.json
foo: 1
bar:
  baz:
    - a
    - b
    - c
foo bar: foo bar

Array/HashをMarkdownテーブルに変換できるようになった

formatter層導入により自由に出力形式を増やすことができるようになったので、前から欲しかったMarkdownのテーブル形式で出力する機能を追加した。 ArrayやHashをto_tableするとよしなにMarkdownにできる、便利〜。

$ cat array.json
[["Name", "Age"], ["Alice", 30], ["Bob", 25]]

# Array#to_table, Hash#to_tableでMarkdownテーブルに変換
$ rf json -s to_table array.json
| Name  |  Age  |
| ----- | ----- |
| Alice |  30   |
|  Bob  |  25   |

Rexexp#onの追加

パーサーの解釈が厳密になったために m /foobar/ { hoge }のような書き方ができなくなってしまった。 この書き方はfoobarにマッチした行のみ選択して、hogeという処理を行うコードでよく使っていた。 これがm(/foobar/){ hoge }と書かないといけなくなり、タイプ数は変わらないものの()をタイピングするのが面倒なのでどうにかしたかった。 そこで正規表現にマッチした行のみ選択するというところに着目してRegexp#onを追加した。 これにより/foobar/.on { hoge }っとかけるようになった。

# mrubyのパーサーの変更によりエラーになった
$ echo "foobar hoge fuga" | rf 'm /foobar/ { _ } '
Error: line 1: syntax error, unexpected '{', expecting end of file

# Regexp#onによりエラーなく記述できる
$ echo "foobar hoge fuga" | rf '/foobar/.on { _ }'
foobar hoge fuga

バイナリマッチングの処理方法を変更

バイナリを含んだ入力の場合、そのまま出力するとターミナルがおかしくなるので、それを回避するためにbinary matchと表示して内容は表示しないようにしている(grepもそうなっている)。 今までは制御文字を含んだ人間の読めない文字をバイナリとして扱っていたのだが、エスケープシーケンス(\e)もバイナリとしてマッチしてしまうと使い勝手が悪かったので、入力を強制的にUTF-8に変換してinvalidだった場合にバイナリとすることにした(たぶん、grepと同じ処理)。

# 1.25.0まではエスケープシーケンスもバイナリ判定だった
$ echo -e "\e[31mA\e[m" | mise exec rf@1.25.0 -- rf _
Binary file matches.

# 1.28.0からはエスケープシーケンスはバイナリ判定にならない
$ echo -e "\e[31mA\e[m" | rf _
A

# NULL文字などは相変わらずバイナリ判定する
$ echo -e "\e[31mA\0\e[m" | rf _
Warning: (stdin): binary file matches
$ echo -e "\e[31mA\xff\xfe\e[m" | rf _
Warning: (stdin): binary file matches

レシーバーをRf::ContainerからRf::Recordに変更

少しわかりづらいのだけど、今までフィルタにしていたコマンドのレシーバーはRf::Containerのインスタンスになっていた。これをRf::Recordのインスタンスに変更した。 この変更で何が嬉しいかというと、レコードに対してgsubなどを実行したいときに、いちいちレシーバーを明示しないといけなかったのが不要になる。

# before
$ echo abc | rf '_.gsub(/abc/, "def")'
def

# after
$ echo abc | rf 'gsub(/abc/, "def")'
def

実はgsubなどのよく使うであろうメソッドは、今回の変更をする前からdelegationしていたので、レシーバーなしでアクセスできていたのだけど、一部のメソッドだけでいざ他のメソッドを使いたくても使えない!ということがあったので、変更した次第。

更新点(詳細)

v1.25.0

v1.26.0

v1.27.0

v1.28.0

v1.28.1

v1.29.0

*1:AIがシュッと直してくれるのでそこまで手間ではない