Chienomi

ちょっとしたスクリプトをRubyで書くメリット

Live With Linux::dailyhack

私はちょっとしたツールを書くとき、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`

であり、このlinesh -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やリダイレクトといった機能だが、よく使うのはchdirexceptionchdirは使いどころが多いだろう。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のような感覚でcommandmodeを指定できる。 例えば、fooというフィルタからテイクバックしたいときは次のようにする。

IO.popen("foo", "r+") do |io|
  io.write data
  io.close_write
  data = io.read
end

書き方がKernel#systemと違うが、引数の問題も起きない。 配列で渡す形だが、Rubyには%wというホワイトスプリットでワードを分割して配列にする機能があるため、結構楽に書ける。

data = IO.popen(%w:ls -l:) {|io| io.read }

なお双方向ストリームに関してはバッファリングの関係で思ったように動かないことがあるため、シェルで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
    w.close
    exec "wc", "-w"
  end

  # Main process
  r.close
  w.write "The quick brown fox jumps over the lazy dog"
  w.close
end

という感じ。 少し長いが、重要なのは非常に複雑で認識しづらくなるパイプの取り扱いが明瞭に書けるということ。

全二重通信の例は次のようなこと。

IO.pipe do |r1, w1|
  IO.pipe do |r2, w2|
    fork do
      w1.close
      r2.close
      $stdout.reopen w2
      r1.each do |i|
        print i.chomp.reverse + "\n"
      end
    end

    r1.close
    w2.close
    %w:The quick brown fox jumps over the lazy dog:.each do |i|
      w1.puts i
      w1.flush
      $stdout.puts r2.gets.upcase
    end
    w1.close
  end
end

Process.waitall

外部コマンドだと、入力を読む単位という問題があり、うまくうごかない場合が多く、こうしたことをする場合は自分で書いたプログラムになるのが普通だろう。実際、このケースもrev2に渡したらうまくいかなかったので、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を使う。 それは全く別の理由だ。 だが、「速度より利便性」という性格が、私の領域に合っているのは間違いない。