ぶていのログでぶログ

思い出したが吉日

rfコマンド v1.6.0をリリースした / Rubyでかけるワンライナーツール

前回ブログで書いたときはv1.4.0だったのでそこから2つほどバージョンアップした。 意識的にrfコマンドを普段遣いしていて*1気になったところと、jqコマンドのオプションの互換性を意識した変更をした。

github.com *2

github.com

主な変更点

-A オプションを-s(--slurp)オプションに変更

入力を1つの配列にまとめるというオプションとしてrfでは-Aオプションを設定していた。 同等のことをjqで行うオプションとして-s(--slurp)があることを知ったので、互換性を意識してオプション名を同じにした。 ちなみに、slurpとは綴るという意味らしい。なるほど。

match関数の挙動変更1:ブロックの引数を各フィールドに変更した

v1.4.0で追加したmatch(m)関数だが、各入力のフィールドを示す変数 _1, _2...が使えないという問題があった。 $Fに置き換えればいいのだが $Fは0-indexedだったり、match関数のブロックの中だけ $F を書くのはやはり混乱のもとになるので、match関数の中でも_1, _2...を使えるようにした。 具体的にはブロックに渡す引数をフィールドの配列にするようにした。 具体例としては↓な感じ

# barにマッチする行の1フィールド目を出力したい
# v1.4.0まではマッチし部分文字列が返却された
$ echo "foo bar" | rf 'm /bar/ { _1 }'
bar

# v1.5.0以降では1フィールド目が返却される
$ echo "foo bar" | rf 'm /bar/ { _1 }'
foo

本来Regexp#matchではブロックの引数としてMatchDataを渡すのだが、それとの挙動が変わってしまったがこちらのほうが私には便利なのでこの仕様にした*3

match関数の挙動変更2: 引数にStringとRegexp以外も指定できるようになった

今までRegepx#matchと同じようにStringとRegexpを指定して、入力行がマッチしているか判定していた。 今回の変更で、それ以外のクラスのオブジェクトを受け取った場合false/nil以外の場合は、その行全体にマッチするようにした。 文章で書くとわかりづらいがユースケースとしては、2フィールド目にfooが含まれている行だけ抽出したいといった場合に便利にかけるようになった。

$ cat input.txt
1 foo bar
2 hoge fuga
3 piyo piyo

# v1.4.0まではifを使う必要があった
$ rf '_ if _2 =~ /foo/' < input.txt
1 foo bar

# v1.6.0からは以下のようにかける
$ rf 'm _2 =~ /foo/' < input.txt
1 foo bar

# おまけ:awkで書く場合
$ awk '$2 ~ /foo/' < input.txt
1 foo bar

v1.4.0においてもif文を使えば同等のことができていたのだが、ワンライナーの場合右に書いていく関係で後置ifの場合返却値が先に来るのは煩わしい*4、後置を使わずにifを書くと ;end を書く必要がありタイプ数が増えるのでそれならばっとmatch関数の挙動を変更するにいたった。

定義されていない変数に直接Integer,Float,String追加ができるようになった

例えば、2フィールド目の数値をすべて足し合わせて合計値を知りたいとなった場合、v1.4.0までは合算値を格納する変数を事前に0で初期化する必要があった。 しかし、v1.5.0以降は事前に初期化しなくても直接加算できるようになった。

$ cat input.txt
foo 100
bar 200
baz 600

# v1.4.0までは事前に初期化が必要
$ rf -q 's||=0; s+=_2; at_exit{ puts s }' < input.txt
900

# v1.5.0以降は直接加算できる
$ rf -q 's+=_2; at_exit{ puts s }' < input.txt
900

この変更によるデメリットとして、変数名をタイポしていたりすると気がつけないということがある。 しかしそのデメリットを考えたとしても、ワンライナーで使うことを考慮すると短くかけたほうが便利なのでこの仕様にした。 なお、今のところは加算(+)しかできない減算、乗算、除算なども同様の仕様にできるのだが、Stringにそれらの処理が実装されていないのでハンドリングをどうするかとか、実際に加算以外のユースケースが思いつかなかったので未実装となっている。

定義されていない変数に配列の追加(<<)ができるようになった

前述と同じような理由だが、ユースケースとして2フィールド目を1つの配列にまとめて処理したいみたいなことを考えている。 具体例は以下のような感じ

$ cat input.txt
foo 100
bar 200
baz 600

# v1.5.0以降
rf -q 's<<=_2; at_exit{ puts s.join("+") }' < input.txt
100+200+600

最近気がついた便利な使い方

rfを普段使いしていてだいぶ慣れてきたのもあって、これこうやってかけるじゃん!便利!!というのがあったので紹介する。 openstack server listというOpenStack上にあるインスタンスのUUIDとインスタンス名、IPアドレス情報を出力するコマンドがある。 出力例としては以下のような感じ。

# インスタンス名やIPアドレスなどはテキトーに変更し、説明に必要な部分のみ抜粋している
$ openstack server llist
+--------------------------------------+-----------------------------+-----------------------------------------------+
| ID                                   | Name                        | Networks                                      |
+--------------------------------------+-----------------------------+-----------------------------------------------+
| 89b035be-9c7d-451f-9279-7f1e7b3cdf23 | example-proxy-1.example.com | example-lan=10.1.2.11                         |
| 51037011-8141-4297-99e7-c9feb0c564ac | example-proxy-2.example.com | example-lan=10.1.2.12                         |
| d0b94df4-dbe0-4062-a87f-0a9eb12db796 | example-server-1.pepabo.com | example-wan=192.0.2.11; example-lan=10.1.2.21 |
| d901e16e-969c-4db8-b821-3263f81364f9 | example-server-2.pepabo.com | example-wan=192.0.2.12; example-lan=10.1.2.22 |
| 76c9ee81-2a15-4b53-af80-e6c552d9eef9 | example-server-3.pepabo.com | example-wan=192.0.2.13; example-lan=10.1.2.23 |
+--------------------------------------+-----------------------------+-----------------------------------------------+

このとき、インスタンス名とexample-lan(10.1.2.XXの部分)のIPアドレスを抜き出したい。 地味にフォーマットがばらばらでgrepやawkなどでは少し手間がかかる処理になる。 しかし、rfならRegexpを駆使することでさっくり書くことができる。

$ openstack serer list | rf 'm /example-lan=(10.1.2.\d+)/ { [_4, $1] }'
example-proxy-1.example.com 10.1.2.11
example-proxy-2.example.com 10.1.2.12
example-server-1.pepabo.com 10.1.2.21
example-server-2.pepabo.com 10.1.2.22
example-server-3.pepabo.com 10.1.2.23

個人的には結構直感的に書けて便利だなっと思ったので作った甲斐があったし、更新のモチベーションも高まってよい。

今後のアップデート予定

オプションパーサーの見直しをしたい。 というのも今はmruby-optparser(OptionParserのmrubyポーティング)を使っているのだが、例えばjqとyamlで同じオプションが指定できない。 そのため、jsonフィルタを選択(-j)したときには-rオプションを指定できるが、yamlフィルタを選択(-y)したときには-rオプションを指定できないということが起こる。 そういうのもありオプションパーサーを見直そうと思っている。

yamlフィルタにいくつか不具合がありそうなのでそれを見直したい。 例えば、UTF-8なStringを入力すると出力する際にダブルクォーテーション(")がついてしまう。 そのため、前述した-rオプションを追加したいという感じになっている。 また、yamlフィルタに限らないのだが、Hashi::MashのようにHashのキーにメソッドでアクセスできるようにしているのだが、nilのときの挙動がいまいちなので見直したいと思っている。 どういうことかというと以下のようなYAMLがあったときに、attrキーをを持っていないオブジェクトにアクセスしたときにエラーになってしまう。

$ cat input.yml
- id: a
  attr: foo
- id: b
  attr: bar
- id: c
$  cat input.yml | rf -y '_.attr'
foo
bar
Error: undefined method 'attr'

_["attr"] とすればアクセスできるがあまり直感的ではないなっと思うのでこれは修正したい。

最後に--recursiveと-iオプションを追加したい。 前者の--recursiveは指定されたパスを最適的にフィルタするオプションでgrep -R / grep -rのイメージ、後者の-iはファイルの置換オプションでsed -iのイメージである。 ここらへんを実装できればまた1つ使い勝手が上がるのではないかと期待している。

*1:意識していないとすぐにgrepやawkなど使ってしまう。。sedはまだrfでは代替できない

*2:v1.5.0のrelease noteにv1.4.0のコミットログが載っているが、v1.4.0リリースしたときにtagを打つのをミスったせいっぽかった…。大した変更ではないのでそのままにしてある

*3:そのうちdisableにできるようにするかも

*4:私の場合先にif文と条件式を書いてから、返却する値を書くのでそれらを書き終えたあとifの手前までカーソルを戻さないといけない