ちょっとしたスクリプトをRubyで書くメリット
Live With Linux::dailyhack
- TOP
- Articles
- Live With Linux
- ちょっとしたスクリプトをRubyで書くメリット
序
私はちょっとしたツールを書くとき、Zshが第一、次いでRubyとなり、ほとんどの場合それ以外を選択しない。
これは、Rubyがコマンドラインツールを書くのに非常に利便性が高いからだが、これはRubyを日常的に使っている人でないとその理由がわかりにくいため、その利点を解説しよう。
十分に簡潔
Rubyでツールを書くことは、シェルスクリプトより簡潔なわけではない――だからこそ、私はZshを書く第一の候補にする。 それどころか、Perlほど簡潔に書くことも出来ない。
とはいえ、それでもRubyは大部分の言語よりも簡潔にツールを書くことができる。 「より簡潔に書ける」という理由でRuby以外を採用するのはなかなかに難しい。
ライブラリへの依存性が少ない
簡単なツールを書くとき、そのツールのために専用のディレクトリを切るようなことは馬鹿らしい。 可能なら処理したいファイルと同じディレクトリに1ファイルだけ置けばよいことが望ましいはずだ。
CRubyは外部ライブラリなしにツールを書くことを実現しやすい大きな言語バンドルだ。
外部のライブラリに依存しないことは、可搬性という意味でも優れているし、一度書いたら全くメンテナンスしないツールが、数年後に再び必要になったときに動かない――という事態にもなりにくい。
system
の引数
RubyのKernel#system
は外部コマンドを起動する。
入出力は扱わず、終了ステータスは$?
(=Process::Status
)で取ることができる。
そのフォーマットは
system(command, options={}) -> bool | nil
system(env, command, options={}) -> bool | nil
system(program, *args, options={}) -> bool | nil
system(env, program, *args, options={}) -> bool | nil
である。
これにはいくつもの利点がある。
まず、Perlのような形式は
`line`
であり、このline
がsh -c
の引数として渡される。
ここで、変数展開を伴う場合、予期せぬ動きを発生させる可能性があり、クォートも難しい。その点、Rubyだと
system("command", "arg1", foo, bar)
のように引数として変数をそのまま文字列として渡すことができる。
さらに、環境変数を渡すことができるため、普通ならfork
してからセットアップするような環境変数がsystem
呼び出し一発で可能。
system({"no_proxy" => "chienomi.org"}, "chromium", "--kiosk", "https://chienomi.org/")
さらにoptionsまで使うとだいぶ高度なことが1行で書ける。
:unsetenv_others
これを true にすると、envで指定した環境変数以外をすべてクリアします。 false だとクリアしません。false がデフォルトです。
:pgroup
引数に true or 0 を渡すと新しいプロセスグループを作成し、そこで動きます。整数を渡すと、指定したプロセスグループに属します。 nil を渡すとプロセスグループを変更しません。デフォルトは nil です。
:rlimit_core, :rlimit_cpu, etc
resource limit を設定します。詳しくは Process.#setrlimit を見てください。引数には整数、もしくは整数2つの配列を渡します。
:chdir
指定した文字列をカレントディレクトリにします。
:umask
指定した整数を umask に設定します。
リダイレクト関連
Hash のキーに子プロセス側のファイルデスクリプタを、対応する値に親プロセス側のファイルデスクリプタやファイル名を指定することでリダイレクトを実現できます。
:close_others
これを true に設定するとリダイレクトされていない、0(stdin), 1(stdout), 2(stderr) 以外のファイルデスクリプタをすべて閉じます。 false がデフォルトです。
:exception
Kernel.#system のみで指定できます。これを true に設定すると、nil や false を返す代わりに例外が発生します。 false がデフォルトです。
超強力なのがpgroup
やリダイレクトといった機能だが、よく使うのはchdir
とexception
。
chdir
は使いどころが多いだろう。exception
は、コマンドが失敗することを予見していない状況でセットしておくと、コマンドの失敗を文字通り例外として扱うことができる。
コマンドを待ち合わせる必要がない場合はKernel#spawn
というのもある。
ファイル名問題
Rubyは文字列の扱いが基本的にはバイナリレベルである。 Ruby 1.9以降文字列を文字列として認識するための機能が追加され、それによっていくらかの問題が生じるようになったが、それでも他の言語よりは扱いが簡単だ。
ファイル名以外の要素からファイル名を抽出するケース(例えば、メディアファイルのタイトルをファイル名にするときなど)ではファイル名がおかしなことになりやすい。
言語処理系によってはそもそもこうしたおかしな文字列は読むことすらできず、落ちてしまうということもある。
CRubyを使えば完全ではないが、ファイル名を取り扱うのは他のどの言語よりも優れているだろう。 Rubyでも扱うことができないファイル名1に遭遇したことはあるが、このファイルは他の様々な言語で試してもどうにもならなかった。
ちなみに、PerlのWindows版はシステムのエンコーディングでファイル名をつけるため、Unicode文字列のファイル名がつけられない。 他にも、UTF-16やUTF-32を採用する言語処理系ではLinuxシステムでファイル名をつけたときに思わぬことになる場合が稀にある。
IO.popen
Kernel#system
では難しい、子プロセスとの通信のあるものを簡単に書ける。
popen(env = {}, command, mode = "r", opt={}) -> IO
popen(env = {}, command, mode = "r", opt={}) {|f| ... } -> object
popen([env = {}, cmdname, *args, execopt={}], mode = "r", opt={}) -> IO
popen([env = {}, cmdname, *args, execopt={}], mode = "r", opt={}) {|f| ... } -> object
popen([env = {}, [cmdname, arg0], *args, execopt={}], mode = "r", opt={}) -> IO
popen([env = {}, [cmdname, arg0], *args, execopt={}], mode = "r", opt={}) {|f| ... } -> object
popen(env = {}, [cmdname, *args, execopt={}], mode = "r", opt={}) -> IO
popen(env = {}, [cmdname, *args, execopt={}], mode = "r", opt={}) {|f| ... } -> object
popen(env = {}, [[cmdname, arg0], *args, execopt={}], mode = "r", opt={}) -> IO
popen(env = {}, [[cmdname, arg0], *args, execopt={}], mode = "r", opt={}) {|f| ... } -> object
popen("-", mode = "r", opt={}) -> IO
popen("-", mode = "r", opt={}) {|io| ... } -> object
popen(env, "-", mode = "r", opt={}) -> IO
popen(env, "-", mode = "r", opt={}) {|io| ... } -> object
File.open
のような感覚でcommand
とmode
を指定できる。
例えば、foo
というフィルタからテイクバックしたいときは次のようにする。
IO.popen("foo", "r+") do |io|
.write data
io.close_write
io= io.read
data end
書き方がKernel#system
と違うが、引数の問題も起きない。
配列で渡す形だが、Rubyには%w
というホワイトスプリットでワードを分割して配列にする機能があるため、結構楽に書ける。
= IO.popen(%w:ls -l:) {|io| io.read } data
なお双方向ストリームに関してはバッファリングの関係で思ったように動かないことがあるため、シェルでexec <>3
とかしたほうがうまくいったりする。
ほとんどの場合は「書いて、読む」ために使う。
これにも環境変数とオプションが渡せる。
(オプションはIO.new
の分が加わるのでさらに色々指定できる。)
fork
IO.popen
でも足りないような、複数のファイルディスクリプタで通信したり、全二重通信を必要とするようなケースではfork
が登場するが、このケースでもRubyは比較的簡単に書ける。
まず、fork
して子プロセスで実行させるの自体はとても簡単で、
fork do
puts "I'm co-process"
end
Process.waitall
という感じである。同様に外部コマンドの実行も、Kernel#exec
の形式がKernel#system
と似た感じなので、
fork { exec "ls", "-l" }
という感じで簡単に書ける。で、ポイントになるのはIO.pipe
の扱いやすさで、片方向の例として
IO.pipe do |r, w|
fork do
# Co-process
.close
wexec "wc", "-w"
end
# Main process
.close
r.write "The quick brown fox jumps over the lazy dog"
w.close
wend
という感じ。 少し長いが、重要なのは非常に複雑で認識しづらくなるパイプの取り扱いが明瞭に書けるということ。
全二重通信の例は次のようなこと。
IO.pipe do |r1, w1|
IO.pipe do |r2, w2|
fork do
.close
w1.close
r2$stdout.reopen w2
.each do |i|
r1print i.chomp.reverse + "\n"
end
end
.close
r1.close
w2 %w:The quick brown fox jumps over the lazy dog:.each do |i|
.puts i
w1.flush
w1$stdout.puts r2.gets.upcase
end
.close
w1end
end
Process.waitall
外部コマンドだと、入力を読む単位という問題があり、うまくうごかない場合が多く、こうしたことをする場合は自分で書いたプログラムになるのが普通だろう。実際、このケースもrev
2に渡したらうまくいかなかったので、Rubyで書いた。
日本語ドキュメントの扱い
多くの人にはおなじみだろう。 多くの日本語ドキュメントはMS-CP932で書かれている。これをShift-JISと称したりすることも多い。 (さらに、MicrosoftはこれをANSIと称する。なにごとだ)
しかも、実際に使われているのは、Shift-JISの規格を逸脱した、「Shift-JISの規則と法則を使った独自エンコーディング」であったりする。
この問題は、日本人でなければ想像もできないような話になっている。 PerlやPythonはこの問題にうまく対処できない。 いや、Perlの日本語部分は日本人が書いたのだが、それでも足りないのだ。
テキストファイルならまだしも、メタデータのテキストフィールドなどになると、もうめちゃくちゃなバイト表現が入っていたりする。
Rubyはそれを最大限、意図するようになんとかしようとすることができる。 iconvを使わずにエンコーディングができ、Unicode正規化でき、壊れた文字列を修復できる、というのは非常に大きい。
コマンドライン
標準入出力が扱いやすく、コマンドラインオプション解析用のライブラリも付属している。 私はあまり使わないが、readlineやcursesのライブラリも標準だ。
これは「小さな」スクリプトでは重要ではないが、とりあえず書いたスクリプトが、非常に使うものであるために発展を必要とするということはよくあることだ。
文字列マッチング
あなたは文字列を検証したいときはどうするだろうか?
正規表現? OK、Rubyは最強の正規表現エンジンと言って差し支えないであろうOnigmoを採用している。 もはや魔術どころか、ひとつの世界だ。
そこまでのものを求めない場合、File.fnmatch
はファイルパス向けではあるものの、文字列のグロブマッチにも使える。
さすがにZshのものほど超強力というわけではないが、シンプルにやりたいことができるはずだ。
また、文字列が含まれているかどうかを確認したいだけであれば、String#include?
が便利だ。
str.unicode_normalize(:nfkc).downcase
とかやっておけば表記ゆれも恐るるに足らず。
File.basename
, File.dirname
,
File.extname
などのメソッドも文字列マッチングを楽にする。
ファイルの探索
ファイルを探索したいことはよくある。 それこそZshの無双ポイントだ。
Rubyはそこまでではないが、Dir.glob
はなかなか便利だ。再帰(**/
)が可能なグロブが書ける。
ちなみに、パターンマッチとしては他のグロブでは使わないものだが、{}
でORも書ける。
ZshのようにDir.glob
でファイルの属性まで絞ることはできないが、File::Stat
クラスを利用してファイルの属性による絞り込みも可能だ。
再帰での全検索が前提であれば、Findライブラリも付属している。
もっとも、この場合はIO.popen("find", "-cond", "foo") {|io| io.each {|i| ... } }
とかやったほうが楽であったりする。
日時
昔はRubyのTime
は非力で、標準ライブラリにあるDateTime
を使うことが推奨されていたりしたが、Time
は進化し、DateTime
を使うduplicatedにするに至った。
強力な日時操作があるのは良いことだ。JavaScriptのように月だけ1を足す必要もない。
さらに、日時オブジェクトは日時であるために、日付を扱いにくいこともある。
日付だけを扱うDate
クラスが用意されていることも、かなり大きなメリットだと言えるだろう。
データベース
「小さな」スクリプトでは稀なことだが、ツールがデータを保存する必要があるということは珍しくない。 そして、最近はJSONライブラリが付属することが増えたが、それでもオブジェクトダンプの方法がないというのはごく普通のことだ。 JSONがあっても、JSONに使える型が少ないという問題もある。
Rubyはネイティブなバイナリ形式のMarshalのほか、YAML, JSON, XML, NDBM, GDBM, SDBMのライブラリが付属されている。 さらに、MarshalとYAMLに関しては競合制御のできるデータベースとして使う機能をもつライブラリも付属している。
非常にカジュアルに永続化を扱うことができる。
ついでに、JSONとXMLを扱えるため、他の言語にパスしやすいというのもメリットだ。
open-uri
私はあまり使わないが、ウェブを扱うツールを書くこともあるだろう。
それがシンプルなGETでいいなら、open-uriライブラリは非常に強力で、ファイルを開く感覚でウェブにアクセスすることができる。
全然簡単な話ではないが、フルアクセス可能な(しかしちょっと原始的な)HTTPライブラリや、POP/IMAP/SMTPのライブラリ、RSSのライブラリ、XML-RPCのライブラリ、Telnetのライブラリもある。
並列実行
並列実行は人間には難しい。だが、否応なく必要になるときもあるものだ。
Rubyには簡単に扱えるfork
がある。
さらに、Giant VM Lockを持ち、複数プロセッサは活用できないが難しいことを考えなくても事故になりにくいThread、コンテキスト切り替えを用いてマルチプロセッサ活用が可能なFiber、実験段階のRactorと4つの方法が提供されている。
さらに、Threadを補助するMutexライブラリは組み込みで、スレッドキューはThreadそのものに組み込みで、Monitor, Mutex Moduleのライブラリが標準となっている。
さらに、ネットワーク分散まで必要なのであれば、標準ライブラリとしてDrb(分散Ruby)とRinda(タプルスペース)が活躍する。
Finally
Rubyでやれば「予想外の問題により、予定外の非常に大きな手間がかかる」という事態が非常に発生しにくい。 だいたいのことは、それほど行数を追加せずともなんとかできる。
グルー要素も充実しているので、シェルスクリプト感覚のRubyスクリプトも書きやすい。
以前はシェルスクリプトに適さない場合、最適な言語をちゃんと検討していたのだが、最近は深く考えずにRubyで書き始めるのが一番早いと悟っている。
小さなツールのためにRubyを使う理由を一言で言えば、「Ruby(CRuby)は問題を起こしにくい」のだ。 Rubyの難点は大きく、重く、遅いということだが、この場合そのようなことは全く問題にならない。
もちろん、私はそれ以外でも多くの場合Rubyを使う。 それは全く別の理由だ。 だが、「速度より利便性」という性格が、私の領域に合っているのは間違いない。