sedやawkが覚えられないRubyistのための「rbコマンド」
この記事はRuby Advent Calendar 2018の21日目の記事です。
今年のある日、rubyweeklyで流れてきたのがこれ。どうやらsedやawkみたいなワンラインプログラムが、Rubyで簡単にできるようになるみたいだが…?
-nは覚えられない
一応、Ruby本体にも-nや-pという、ワンライナ向けのオプションがあるんだよな。そう、あることは知っている。でも使い方は覚えてない。
sedやawkも一緒で、使えたら便利なんだろうなーと思いつつ、未だに覚えてない(少なくとも何も見ずに書ける程には)。
そこでrbコマンド?
rbコマンドのいいところは、「普段Rubyを使ってるときの感じのままで」ワンライナが書けるところだ。
とりあえず例を見てもらおう。rbコマンドには1行ずつ処理する「lineモード」と、ファイル全体がEnumeratorとして取れる「linesモード」の2種類がある(命名は筆者)が、よく使うのは前者だろう。とりあえず-l
オプションでlineモードを使ってみる。
psからpidを抜き出す
例として、psコマンドの出力からPID部分だけを抜き出すことを考える。PIDは各行の先頭にあるので、Rubyist的に考えると、「行をsplitして0番目を取ればよさそう」となる。
ruby -pだと行は$_
で取得するのだが、rbコマンドでは「行はどうやって取るんだっけ」と考える必要はない。行に対して何をするかだけ書けばいいからだ。
$ ps | rb -l 'split[0]'
31728
31789
33161
34859
35196
54235
53874
このように、「splitして0番目を取る」という「やりたいこと」だけ書けば動いてくれるのがrbコマンドなのだ。
インストール方法
さっそく使ってみたくなった人のために、インストール方法を解説しておく。といってもインストールはすごく単純だ。rbコマンドはたったの9行しかないからだ。以下の内容をrbというファイル名で作成し、PATHの通ったところに置くだけ。
#!/usr/bin/env ruby
File.join(Dir.home, '.rbrc').tap { |f| load f if File.exists?(f) }
def execute(_, code)
puts _.instance_eval(&code)
rescue Errno::EPIPE
exit
end
single_line = ARGV.delete('-l')
code = eval("Proc.new { #{ARGV.join(' ')} }")
single_line ? STDIN.each { |l| execute(l.chomp, code) } : execute(STDIN.each_line, code)
実装を読む
というわけで上のプログラムがrbコマンドの全貌なのだが、どういう実装になっているのだろうか?先ほどの実行例をもう一度見てみる。
$ ps | rb -l 'split[0]'
_.instance_eval(&code)
という部分が肝で、各行を表すStringを_
として、与えられたプログラムcode
がinstance_evalで実行される。
これにより、code
内ではStringクラスのメソッドが好きに呼べるし、またself
で文字列全体を取ることもできる。以下のようにローカル変数を宣言してもちゃんと動く。
$ ps | rb -l 'a = split; "#{a.first} #{a.last}"'
linesモード
最後に「linesモード」について補足しておこう。-l
を付けなかった場合は、入力全体を行ごとのEnumeratorにしたものがself
になる。
例として、csvファイルのうち「2018-09」が入った行だけを出力することを考えよう。普通のRubyプログラムならlines.select{...}
などと書くところだが、rbコマンドの場合はlines.
より後だけを書く。
$ cat log.csv | rb 'select{|l| l =~ /2018-09/}'
2018-09-01,...
2018-09-02,...
個人的にはlineモードの方が使いたい機会が多そうなので、そっちがデフォルトでも良かったような気がする。がしかし、rbコマンドはたった9行のスクリプトなので、そう思うならそのように変えてしまえば良い。いろいろカスタマイズして、君だけの最強のrbコマンドを作り上げよう!