UNIX USER 2002年02号 Ruby特集

序論

ここ一、二年ほどの間に、 筆者のまわりではRubyユーザがずいぶんと増えてきている。 それは筆者自身がRubyの普及につとめ、ことあるごとに 「Ruby、Ruby」と言ってきたからである―と言いたいところだが、 よろこばしいことにそういうわけではない。

幸運にも筆者のまわりにはハッカーと呼ばれるような人々が何人かいる。 彼らハッカーは、たとえ筆者が宣伝文句を連呼したとしても ただそれだけのことではそう簡単に使ってみてはくれないだろう。 もちろん友人であったり知人であったりするわけだから、 一定の関心を示してはくれはする。 しかし、その後に続く一歩があるかどうかは Ruby自身の魅力にかかっていることは明らかだ。 その点において、筆者のまわりで Rubyユーザが増えてきている*1のは大変よろこばしいことなのである。

ところで、読者の皆さんにおかれてはどうだろうか。 この一年のうちに出版されたRubyを冠する書籍は、 国内ではすでに10冊を越えているし、 海外でも(出版予定のものを含めれば)6冊にも及ぶ。 このような状況であるから、 名前くらいは聞いたことがあるという人も少なくはないだろう。 そこで、ここではRubyというプログラミング言語が どのような機能や特徴を持っていて、 いったいどういうところに人をひきつける、 つまり皆さんの手を動かさせてしまう要素があるのかについてお話ししようと思う。 「Rubyというのは聞いたことがあるけど、 めんどうだしさわってみてはいないなあ」 「新しい言語なんてもういいよ。 Perlがあればこと足りるじゃないか」 そう考えている人々にRubyの魅力の一端だけでも示せれば幸いである。

Rubyってなんだ

Rubyはプログラミング言語の一種である。 先程、Perlをひき合いに出したことからわかるかもしれないが、 いわゆるスクリプト言語だ。特徴はいろいろあるが、 最も大きな特徴は最初からオブジェクト指向言語として 設計、実装されたことだろう。

現状のPerlのように後付けオブジェクト指向とは違って、 Rubyは生粋のオブジェクト指向言語である。 このため、プログラム上に出てくるものはほとんどすべてがオブジェクトであり、 Rubyのプログラムはオブジェクトに対する操作に終止すると言える。

オブジェクト指向と聞くと それだけで迂回しようとする人もなかにはいるだろうが、 ここで読み進めるのを止めるのは少しだけ待ってほしい。 なんと言ってもRubyはスクリプト言語でもあるということを忘れてはいけない。 AWKやPerlに代表されるスクリプト言語の気軽さ、 便利さをも兼ね備えたオブジェクト指向言語、それがRubyなのである。

これより詳しい解説に入っていくが、 何分紙面の都合があるため解説できる内容も限られてくる。 そのため解説に入るまえにいくつかの情報源を示しておこう。 もしもこの記事を読んでRubyに興味を持ったら、 次のステップに進むための手がかりとしてほしい。

また、以下では文法については要所だけを解説することとし、 詳部を含む文法全般についての解説していないので、 必要に応じて各種情報を参照するようしていただきたい。 もっとも文法上の難しい表現というのはRubyにはあまりないし、 特にこの章で扱っているサンプルについては 他の言語でのプログラミング経験があれば 雰囲気を感じとってもらえる程度のものとしたつもりである。

Webサイト
メーリングリスト

以下のようなメーリングリストが運営されている。

購読の仕方などはRubyホームページに記載されている。

書籍

前述の通り、多くの書籍が出版されているが、 これからRubyをはじめるという方には以下のようなものはいかがだろうか。

体験してみよう

ちょっとしたコードを実行してみるときには ruby-eオプションが便利である。 これはperl-eオプションと同じ意味を持っており、 コマンドライン上で-eに続く部分を各言語のスクリプトとして実行する。 つまりこういうことだ。

$ perl -e 'print "Hello, world!\n"'
Hello, world!
$ ruby -e 'print "Hello, world!\n"'
Hello, world!

このようなやり方で実行されるスクリプトのことを、 特にワンライナーと呼ぶこともある。 筆者がrubyを使うシーンで最も多いのは、 そうしたワンライナーを実行するときかもしれない。 もちろんたいていは10進数を16進数に直すとどうなるか などといったくだらないことだったりするのだが、 時にはあるWebページにアクセスするといったことを行ったりもする。

$ ruby -rnet/http -rzlib -e 'Net::HTTP.start("www.example.com"){|h| print Zlib::Inflate.inflate h.get("/foo/").last}'

このままではいささか手抜きではあるが、 ある種のサイトにはこのようにして アクセスすることが可能ではある(上の内容は 今この時点で理解できなかったとしても別に問題はない)。

コマンドラインで一〜二行におさまる程度あれば このようなやり方も悪くはない。なんと言ってもお手軽である。 しかし、コードがある程度以上の分量になってきたり、 条件分岐が入ってくるととたんに見通しが悪くなってしまうし、 特にこれからRubyをはじめようとしている人にとって ワンライナーでガシガシ試すのがおすすめかというと、 決してそんなことはない。 われわれにはirbという強い味方があるのである。

対話的なRuby

irbはRubyスクリプトを対話的に実行するためのツールである。 普通にRubyをインストールすると いっしょにirbもインストールされるため、 単にirbとタイプすると起動できるはずである。

$ irb
irb(main):001:0> 

実行例の二行目に表示されているのがirbの出しているプロンプトで、 Rubyのコードであればなんでも入力することができる。 適当に入力してエンターを押すと、 そのタイミングで入力されたものが評価され、評価結果が表示される。 以下は実行例である。

irb(main):001:0> 3*3 + 6
15
irb(main):002:0> "abc"*3 + "def"
"abcabcabcdef"

入力したのは3*3 + 6"abc"*3 + "def"の部分で、 それぞれの次の行にあるのがその評価結果になる。 いずれもコードからの想像とかけはなれているということはないだろう。 一つ目は普通の算術演算だし、二つ目は文字列の連結などを行っている。 このような単純なことばかりではなく、 irb上でメソッド(ここでは「関数」のことだと 考えてもらえばよい)を定義することももちろん可能である。

irb(main):007:0> def greeting(time)
irb(main):008:1>   case time.hour
irb(main):009:2>   when 6...9
irb(main):010:2>     puts "おはよう"
irb(main):011:2>   when 9...17
irb(main):012:2>     puts "こんにちは"
irb(main):013:2>   when 17...24
irb(main):014:2>     puts "こんばんは"
irb(main):015:2>   when 0...6
irb(main):016:2>     puts "おやすみ"
irb(main):017:2>   end
irb(main):018:1> end
nil
irb(main):019:0> greeting(Time.now)
こんばんは
nil
irb(main):020:0> greeting(Time.now + 60*60*5)
おやすみ
nil

この例ではgreetingというメソッドを定義している。 greetingは時刻を引数にとり、 与えられた時刻が何時であるかによって 様々なあいさつを表示するという動作をする。 caseendはC言語でのswitch文に近いが、 判断材料とするものには任意のオブジェクトを指定できる。

以上のように、irbではRubyできることのほとんどを試すことができる。 ここから先はこのirbを使って実例を示しつつ、 Rubyがどのようなものであるかについて解説して行きたい。

ところで、やや余談になるが実はirb自体 Rubyによって記述されたスクリプトであり、 Rubyの文法をRuby自身によって解釈させている。 実体としてはrubyコマンドとはまったくの別物であるわけだ。 このためRubyと若干違う動きをするケースもあるのだが、 それらについては後程改めて触れることにする。

それでは、はじめよう。

オブジェクトの世界

Rubyは手軽ながら本格的なオブジェクト指向言語である。 そのこころはRubyの世界にあるモノすべてが オブジェクトだというところにある。 と、ここまで聞いて顔をしかめている人もいるかもしれない。 「オブジェクト指向なんて…」と。 しかしモノは考えようだ。 ややこしげなところはとりあえず無視して、 オブジェクト指向の良いところだけを 使ってやればよいわけだ。

たとえば、非常に良く使われるデータ構造に 配列とハッシュ(連想配列)がある。 AWKやPerlでもおなじみのこれらのデータ構造は、言ってみれば、 プログラム上で必要な「なにか」をつっこんでおく格納場所、コンテナである。 Rubyの世界においては「なにか」とはとりもなおさず オブジェクトということになる。 ところでRubyでは配列やハッシュすらも特別扱いはされていない。 それら自体が一個のオブジェクトであるからして、 配列の中に配列を入れることも、 ハッシュの中に配列を入れることも可能である。 ましてやハッシュのキーとしてすら同様なのだ。

irb(main):022:0> ary = [1, "2", 3.0, [4, 5, [6, "7"], 8, 9]]
[1, "2", 3.0, [4, 5, [6, "7"], 8, 9]]

aryは配列であり、その中では二つの配列が入れ子になっている。 内容物は数値だけではなくさまざまである。 では、これをふまえて次の例を見てみよう。

irb(main):023:0> hash = {}
{}
irb(main):024:0> hash[ary] = Time.now
Sun Nov 25 01:21:07 JST 2001
irb(main):025:0> hash[/(foo)+/] = 1
1
irb(main):026:0> hash[/(bar)+/] = ary
[1, "2", 3.0, [4, 5, [6, "7"], 8, 9]]

最初にhashに対してハッシュを割り当てている。 次の行では先程のaryをキーとして使用し、 さらに続く二行では正規表現をキーとしている。 これまで数度にわたって言ってきたことだが、 Rubyにおいてはほとんど例外なくすべてがオブジェクトであるということが わかっていただけるだろう。

irb(main):027:0> hash
{/(bar)+/=>[1, "2", 3.0, [4, 5, [6, "7"], 8, 9]], /(foo)+/=>1, [1, "2", 3.0, [4, 5, [6, "7"], 8, 9]]=>Sun Nov 25 01:21:07 JST 2001}

hashの中身は以上の通りである。 さて、このままでは実に漠然とした話で終わってしまうので、 先のサンプルで定義したgreetingを ハッシュを使って書き換えてみることにしよう。

irb(main):035:0> def greeting(time)
irb(main):036:1>   h = { 6... 9 => "おはよう",
irb(main):037:2*         9...17 => "こんにちは",
irb(main):038:2*        17...24 => "こんばんは",
irb(main):039:2*         0... 6 => "おやすみ"}
irb(main):040:1>   for r in h.keys  
irb(main):041:2>     if r === time.hour
irb(main):042:3>       puts h[r]
irb(main):043:3>       break
irb(main):044:3>     end
irb(main):045:2>   end
irb(main):046:1> end
nil

これでまったく同じ動作をするはずである(ただしこのままでは効率が悪い)。

x...yというのは配囲を表すオブジェクトで、 ここではそれをキーとしたハッシュテーブルをhに作っている。 forendは繰り返しの一種であり、 この場合、hに含まれる全てのキーがh.keysによって取り出され、 その各々に対して繰り返しが適用される。 ifの条件式として使われている ===というのが見なれないかもしれないが、 これは右辺が左辺に含まれれば真、含まれなければ偽という動作をするものだ。 結局time.hourで得られた時刻が 4つのキーの中のいずれかの範囲に入っていたら、 それに対応する値を取り出して表示するということになるのである。

イテレータとブロック付きメソッド

さっきはgreetingをハッシュを使う形に書きなおしたみたが、 この際さらにRubyらしいスタイルになるよう書きなしてみたい。 どこが変わるのかというとforendの部分である。 この部分をRubyでは次のように記述することが可能なのだ。

h.each {|r, val|
  if r === time.hour
    puts val
    break
  end
}

これはどういうことかというと、 ハッシュhの各コンテンツに対して {}の部分を適用するという意味になる。 そしてその際にペアとなっているキーと値を rおよびvalに代入してから適用する。

このような抽象化された繰り返しのことをイテレータといい、 ハッシュ以外にも文字列や配列、範囲オブジェクトにおいて提供されている。

イテレータと呼ばれるメソッドにはeach以外にもいろいろある。 たとえばcollectというメソッドは 次のような動作をする。

irb(main):001:0> [1, 2, 3, 4, 5].collect{|i| i**2}
[1, 4, 9, 16, 25]

さて、このあたりでRubyのメソッドについて説明しておく必要があるだろう。 Rubyはオブジェクト指向言語であるからして、 オブジェクト中心の考え方がその根底にある。 メソッドと関数の違いは、メソッドがオブジェクトに内包されているのに対して、 関数はデータとは完全に分離されているというところにある。 つまり、関数は処理の対象が何であってもその動作自体はかわらないが、 オブジェクト指向におけるメソッドの場合、 たとえその名前が同じであってもオブジェクト自体が別のモノであれば その動作もまた別のものであり得るということになる。

たとえばRubyのオブジェクト全般に定義されている to_sというメソッドがある。 これはオブジェクトを文字列で表現するためのメソッドで、 次のような挙動をする。

irb(main):026:0> 1.to_s
"1"
irb(main):027:0> 1.1.to_s
"1.1"
irb(main):028:0> [1, 2].to_s
"12"
irb(main):029:0> Time.now.to_s
"Sun Nov 25 02:18:55 JST 2001"

例中の.to_sの前の部分がメソッドの適用対象となるオブジェクトである。 逆に言うと11.1[1, 2]Time.nowで 表される各オブジェクトにおいて定義されている to_sというメソッドを発動している様子を観察した結果が この例の意味するところになる。 このように各オブジェクト(あるいはその設計図であるところのクラス)は、 自分自身を文字列に変換するとするならどのように表現すべきか ということをよく知っていて、 各自に合ったやり方で文字列表現を返してきてくれているわけだ。

同じことがメソッドeachについても言える。 eachメソッドを持っているオブジェクトは配列、ハッシュ、 文字列などいくつもあるが、いずれにしてもなんらかの形での 繰り返し的動作を行うよう定義されている。

以上をまとめるとこのようになる。 よく見るとブロックの中で引数を受け取る部分が ハッシュ(hash)と文字列、配列で異っていることがわかる。

irb(main):001:0> str = "abc\ndef\nghi"
"abc\ndef\nghi"
irb(main):002:0> ary = [1, 2, 3]
[1, 2, 3]
irb(main):003:0> hash = {"string" => str, "array" => ary}
{"array"=>[1, 2, 3], "string"=>"abc\ndef\nghi"}
irb(main):004:0> hash.each {|type, example|
irb(main):005:1*   puts type
irb(main):006:1>   example.each {|x| puts x}
irb(main):007:1> }
array
1
2
3
string
abc
def
ghi

ここであらためて注意しておきたいのは、 以上のようなイテレータは 繰り返しを表現するための文法ではないということだ。

Rubyにはブロック付きのメソッド呼び出しという技がある。 ブロックというのは上の例で言うと{}の部分のことで、 一連の手続きをカプセル化したもののことを言う。 そして、メソッドを呼ぶ時に、引数といっしょに このブロックをも一種の引数としてメソッドに与えるというやり方が ブロック付きのメソッド呼び出しなのである。

つまり、hash.eachの後に続く部分は、 一つのかたまりとしてeachメソッドに渡されていて、 eachの内部においてハッシュのコンテンツ一つ一つに対して このブロックを発動させているために、 結果的に繰り返し処理が行われているというわけだ。 イテレータというのはブロック付きメソッド呼び出しの ある一つの形態にすぎないのである。

ファイル入出力とブロック付きメソッド

イテレータ、あるいはブロック付きでのメソッド呼び出しの 興味深い用法にファイル入出力がある。 Rubyでも他のプログラミング言語と同様の ファイルのオープン・読み書き・クローズという操作は可能である。

irb(main):001:0> io = open("/etc/passwd", "r")
#<File:0x402a723c>
irb(main):002:0> while line = io.gets
irb(main):003:1>   print line
irb(main):004:1> end
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
〜(略)〜
nil
irb(main):005:0> io.close
nil

openでファイルをオープンしている。 この場合は読み込み専用("r"を指定)でオープンしている。 openが返してくるのはファイル入出力を司るオブジェクトで、 このオブジェクトにgetsというメソッドを投げてやると ファイルから一行(\nまで)読み出してきてくれる。 そしてそれをそのまま表示しているのがこの例の内容だ。 ちなみにopenの際に"w"を指定すると 書き込み専用でのオープンとなり、 getsの代わりにputsを投げると ファイルに書き込むことが可能である。 読み書きモードの指定はC言語のfopen(3)に同じだ。

さて、以上はなんの変哲もないファイル入出力の手順であるが、 ここにもRubyらしさを追求する余地が残されている。 前述したブロック付きメソッド呼び出しという機能によって、 上の例をこのように書きかえることが可能なのだ。

irb(main):001:0> io = open("/etc/passwd", "r")
#<File:0x402a723c>
irb(main):005:0> io.each {|line|
irb(main):006:1*   print line
irb(main):007:1> }
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
〜(略)〜
nil

そう入出力オブジェクトにもまたeachメソッドが用意されているのである。 このケースでのeach(つまり入出力オブジェクトにおいて 定義されているeach)は、 対象となる入出力オブジェクトから一行ずつ読み込んでは ブロックを適用するという動作をしてくれる。 これによってより簡潔でわかりやすい書き方が可能になる。

またopenすらもブロックを受け付けるメソッドであることを 紹介しておこう。

irb(main):006:0> open("/etc/passwd", "r") {|io|
irb(main):007:1*   while line = io.gets
irb(main):008:2>     print line
irb(main):009:2>   end
irb(main):010:1> }

この場合、ブロックにはopenによって得られた 入出力オブジェクトが渡されるので、 ブロック中ではそれを使ってファイルにアクセスすることができる。 このやり方でうれしいのは、ブロックの処理が終わって ブロックをぬけると、勝手にファイルをクローズしてくれることだろう。

手続きオブジェクト

ブロックについての解説をしたついでに、 手続きオブジェクトについても解説しておこう。

イテレータに代表されるブロック付きのメソッド呼び出しで使うブロックは、 ブロック付きでの呼び出しに対応したメソッドの内部において発動されている。 それと同じようなことを自分でもやりたくなることがしばしばある。 たとえば、対話的なやり取りをするプログラムを作っていて、 特定の正規表現にマッチしたらこれこれの動作をさせるといった場合である。 単純に考えると次のようなプログラムを書けばよさそうだ。

case input
when /foo/
  〜処理〜
when /bar/
  〜処理〜
end

これも一つのやり方であるには違いないのであるが、 このやり方ではプログラムの動作中に 反応のパターンを加えたりすることが非常にやりずらい。 そうした時に、だれしもがこのようなことを考えるだろう。

C言語ならば関数ポインタのおでましとなるようなシーンであろう。 このシーン、Rubyにおいてはどのように切りぬけられるかというと、 このように記述することが可能である。

hash = {
  /foo/ => proc {|x| 〜処理〜}, 
  /bar/ => proc {|x| 〜処理〜}, 
}

hash.each {|regexp, procedure|
  if regexp === input
    procedure.call(input)
  end
}

procもまたブロック付きの呼び出しを受けつけるメソッドで、 受け取ったブロックを手続きオブジェクトにパックして返す。 ひとたび手続きオブジェクトになったならば、 callメソッドを投げてやることによって いつでも「手続き」を実行することができるのである。 なお、その際callメソッドの引数がそのままブロックに渡される。 この例ではhashのキーとして登録された 正規表現についてマッチを試み、 もしマッチしたら入力内容を引数にして あらかじめハッシュに入れられていた手続きを呼び出すのである。

こうしてみてみると、 イテレータのようにふるまうメソッドは次のようにしてやれば 自分でも作れることがわかるだろう(本当のイテレータの作り方については コラムを参照のこと)。

irb(main):001:0> def pseudo_each(array, procedure)
irb(main):002:1>   for x in array
irb(main):003:2>     procedure.call(x)
irb(main):004:2>   end
irb(main):005:1> end
nil
irb(main):006:0> pseudo_each([1, 2, 3, 4], proc {|i| puts (i*i).to_s})
1
4
9
16
[1, 2, 3, 4]

簡単永続化

毎日継続的に行う必要のある処理をしているとき、 「今日までの状態」を保存しておいて 明日はその「状態」から続きの処理を始めたいということはないだろうか。 ログファイルから統計情報をまとめるような処理などではよくある話だと思う。 このようなときに便利なのがMarshalという機構である。

MarshalはRubyのオブジェクトをバイト列に変換し、 また変換されたバイト列から同じ内容のオブジェクトを復元する ということを可能とする。

irb(main):003:0> io = open("access.log")
#<File:0x40287940>
irb(main):019:0> bytes = 0
0
irb(main):020:0> stat = {}
{}
irb(main):021:0> io.each {|line|
irb(main):022:1*   bytes += line.size
irb(main):023:1>   if /\?cmd=view;name=([^\s"]+)/ =~ line
irb(main):024:2>     stat[$1] ||= 0
irb(main):025:2>     stat[$1] += 1
irb(main):026:2>   end
irb(main):027:1> }
#<File:0x40287940>

これはApacheのログをオープンし、 特定のパターンにマッチするアクセスについての 統計情報をstatにまとめているものである。 stat[$1] ||= 0というのは stat[$1] = 0 unless stat[$1]の略記法で、 statの中にキーが$1(正規表現でマッチした部分文字列)である エントリがなければ0で初期化するという意味になる。 要するにパターンにマッチした行数を 部分文字列毎にカウントしようとしているわけである。 またそうすると同時に、読み込んだバイト数を カウントしてもいる(bytes += line.size)から、 ログファイルそのものが置きかわらない限りは 途中から作業を再開することが可能である。

つまり、ここでstatの内容とbytesの内容を 後日の作業のために保存しておくことができれば良い。 そんなとき、Rubyではこのようにすることができる。

irb(main):028:0> open("/tmp/stat", "w") {|mio|
irb(main):029:1*   Marshal.dump([bytes, stat], mio)
irb(main):030:1> }
#<File:0x4023d8b4>

Marshal.dumpは任意のオブジェクトをバイト列にし、 指定されたファイル入出力オブジェクトに書き出してくれる。 後日、処理を再開するときには次のようにしてやるとよい。 Marshal.loadによって、先程保存した状態が 復元されているのを確認できるはずである。

irb(main):003:0> bytes = nil
nil
irb(main):004:0> stat = nil
nil
irb(main):005:0> open("/tmp/stat", "r") {|io| bytes, stat = Marshal.load(io)}
〜略〜
irb(main):006:0> io = open("access.log", "r")
#<File:0x4027158c>
irb(main):007:0> io.seek(bytes)
〜略〜

なお、上の例ではMarshal.dumpの際に入出力オブジェクトを指定していたが、 これを指定しないでおくとバイト列化した内容を文字列として受けとることもできる。

irb(main):008:0> Marshal.dump("test")
"\004\006\"\ttest"

このように使い方によってはたいへん便利なMarshalだが、 すべてのオブジェクトをバイト列化できるわけではないので注意してほしい。 たとえばファイル入出力オブジェクトや 手続きオブジェクトがその例である。

例外処理

では、バイト列化できないものを バイト列化しようとしたら何が起こるだろう。 あるいは存在しないファイルをオープンしようとしたら。

irb(main):002:0> Marshal.dump(open("/dev/null"))
TypeError: can't dump File
        from (irb):1:in `dump'
        from (irb):1
irb(main):003:0> io = open("/dev/nullnull", "r")
Errno::ENOENT: No such file or directory - "/dev/nullnull"
        from (irb):3:in `open'
        from (irb):3

これは実際に試してみた様子である。 どうやら「エラー」になってしまったようだ。 表示は「エラー」に関する情報を irbが気を効かせてしてくれているのだろうか。

実はそういうわけではない。 Rubyにおいては、処理中に「異常」な状態に遭遇すると その時点で「例外」というものを発生させるようになっている。 何を「異常」とみなすかは、その時勤での処理の主体である オブジェクト自身である。 openの場合について考えてみると、 オープンせよとして指定されたファイルが存在しないのだから、 これは明かに異常な状態である。 Rubyの処理系はその異常を検知して処理を中断してしまう。

「なに、それんなことをされては困る」と思った方もいるだろう。 だが安心してほしい。単に処理を中断するだけではどうしようもなく 使いものにならないだろうが、当然のことながらRubyはそんなことでは終わらない。 ちゃんと救済措置をとれるようになっているのである。 たとえば先の例であれば、このようにするとよい。

irb(main):001:0> begin
irb(main):002:1*   io = open("/dev/nullnull", "r")
irb(main):003:1>   print "OK?"
irb(main):004:1> rescue Errno::ENOENT
irb(main):005:1>   # エラーに対処する処理
irb(main):006:1* end   
nil

先程のように例外によって中断されていないことがわかる。 ただし、openのところで例外が起こっていることにはかわりないので、 OK?が表示されることはない。 では発生した例外はどこにいってしまったのだろう。 Rubyではあらかじめbeginendでくくったおくと、 その中で起った例外をrescueによって捕獲することができるようになる。 rescueの引数は捕獲すべき例外の種類で、 ここに何も指定しないとかなり多くの例外を捕獲してしまう。 一般になんでもかんでも捕獲してしまうと プログラムのバグの発見を遅らせることにつながるので、 例のように、ある程度限定的にした方がよいだろう。

beginendの効能はそれだけではない。 次のコードを見てほしい。

irb(main):001:0> begin
irb(main):002:1*   io = open("/dev/nullnull", "r")
irb(main):003:1>   while l = io.gets
irb(main):004:2>     # ここに処理が入る
irb(main):005:2>   end
irb(main):006:1> rescue Errno::ENOENT
irb(main):007:1>   # エラー処理
irb(main):008:1* ensure
irb(main):009:1*   io.close if io
irb(main):010:1> end

新たに増えたのはensureという部分だ。 この部分に書かれたコードは beginendの中の処理が どのような形であれ終った後で実行されるコードである。 つまり、処理が正常に終わた場合でも、 なんらかの例外が発生して処理が中断された場合でも、 どちらにしてもio.close if ioが実行されることになり、 この例で言えばオープンされたファイルは 確実にクローズされることが保証される。 また必要であればrescueを追加することで、 例外の種類ごとに対処方法を記述することもできる。

演算子…に見えて実はメソッド

ここまで、かなり無頓着に+===使ってきた。 説明もしなかったので、これらは演算子であるかのように思えたかもしれない。 文法的にはその通り。 ところがRubyにおいてはこれらもまたメソッドなのである。 1 + 1という式は、実際には1.+(1)のように解釈されるし、 同様に"abc"*3というのも"abc".*(3)として解釈される。 こうした例は他にもあって、たとえばハッシュの中にある エントリを参照するためにhash["abc"]としたり、 値を設定するためにhash["abc"] = 1としたりするが、 これらもそれぞれhash.[]("abc")hash.[]=("abc", 1)というメソッド呼び出しにひとしい*2

数値 + 数値、文字列 + 文字列、配列 + 配列、…などが それぞれの状況にうまくマッチした形で実行されることの背景には こうした事実があったわけである。

この特性、つまり特定に名前のメソッドについては その簡略型とも言える形での呼び出しが可能である、 ということを積極的に活用したおもしろいライブラリが標準で添付されている。 shell.rbというのがそれで、 以下はこのライブラリのドキュメントから抜粋したスクリプトである。

require 'shell' # shellライブラリを使用する
sh = Shell.new  # Shellオブジェクトを生成する
sh.cat("/etc/passwd") | sh.tee("/tmp/log1") > "/tmp/log2"

これを実行すると/etc/passwdの内容が /tmp/log1/tmp/log2にコピーされる。 ちょうどshスクリプトにおいて cat /etc/passwd | tee /tmp/log1 > /tmp/log2 としたのと同じ動きをするわけだ。

このように|><などをうまく定義して、 shスクリプトにおけるそれらの意味や動作をエミュレートしているのである。 もちろんこういったことは特殊な例で、 ごく普通に行われているわけではないが、 こうした実例を見ると一見して演算子に見えるものが 実はそうではないということを実感できるだろう。

スレッド

Rubyには独自のスレッド機能が備わっている。 ここで独自といったのはRuby自身において実装されていることを意味しており、 各種OSが提供するスレッド機能に対するインタフェースではないからだ。 したがってスレッド一般に言われる利点は、Rubyの場合、通用しない。 たとえば速度的なアドバンテージはほとんど期待できないだろう。

それではRubyのスレッド機能に存在意義がないのかというと、 そういうわけでもない。まずRubyレベルでの実装であるがゆえに、 Rubyが動きさえすればどんなOS、どんなアーキュクチャでも 基本的にまったく同じ動作をすることが期待できる。 すなわちスレッド機能を遠慮なく活用できるということで、 状況によってはスクリプトをより簡潔に記述できるようになる。

例としてPOPサーバにアクセスして メールを取得するスクリプトについて考えてみよう。 単一のサーバにアクセスする場合、 このようなコードになる(詳細については後の章で解説しているので、そちらを参照)。

require "net/pop"

def pop_get(server, user, pass, outfile)
  Net::APOP.start(server, 110, user, pass) {|p|
    p.each_mail{|m| outfile.print m.pop}
  }
end

open("/tmp/mbox", "w") {|io|
  pop_get("pop-server.example.com", "username", "password", io)
}

このスクリプトを実行すると、 ホストpop-server.example.comにAPOPでアクセスし、 もしユーザusernameのメールボックスにメールがあれば すべて取り出して/tmp/mboxに格納してくれる。 これを少し拡張して複数のサーバに同時にアクセスできるようにしてみよう。

まず、サーバ名と対応するユーザ名/パスワードのテーブルを用意する。

hash = {"pop-server.example.com" => ["username", "password"],
        "pop-server.example.jp"  => ["username2", "password2"]}

そしてこれを元にサーバへのアクセスを行うのだが、 次のようにしてしまうと各サーバに順番にアクセスをするだけになってしまう。

hash.each {|server, info|
  open("/tmp/" + server, "w") {|io|
    pop_get(server, info[0], info[1], io)
  }
}

そこで、hash.eachのブロックの中で 各サーバ用の処理スレッドを生成し、 アクセスを同時進行させてやろうとしたのが次のものである。

threads = []
hash.each {|server, info|
  threads << Thread.new {
    open("/tmp/" + server, "w") {|io|
      pop_get(server, info[0], info[1], io)
    }
  }
}

threads.each {|thread| thread.join} # 全スレッドの終了を待つ

スレッドの生成はThread.newによって行い、 これは生成されたスレッドを表すオブジェクトを返す。 スレッド生成時に与えられたブロックはすぐさま実行が開始されるのだが、 その一方で、hash.eachの繰り返しも継続される。 ここでは生成されたスレッドオブジェクトを配列threadsに保存しながら、 必要な数だけスレッドを生成するようにした。 すべてのスレッドを生成し終ったら、 後は全スレッドが終了するのを待つだけである。

このように、スレッド同士が独立に動作する場合には それほどなやむこともなく、 ごく簡潔に並列動作を記述することが可能となる。

ここまでのまとめ

これまでのところはどうだっただろうか。 以上はRubyの特徴のうち、ごく日常的に使われるものにしぼって 紹介してきたつもりである。 いずれも当り前のように使われるものであり、 どんなRubyスクリプトを見ても このうちのどれかが使われていたりするはずだ。

前にRubyの文法にとりたてて難しいものは少ないと述べたのは、 Rubyの世界には「特別な存在」が非常に少ないためである。 オブジェクトにしばられない、純粋な「演算子」はおよそないようなものだし、 「これとそれとあれだけはオブジェクトではない」といった例外もない。 もちろんメソッドの動作そのものは覚えて行くほかないかもしれないが、 少なくとも文法的な要素でもって「びっくりさせられる」ことはまれであろう。

irbを使いこなそう

せっかくirbを紹介したので、 このツールについてもう少し話しておきたい。

まず基本となる使い方だが、これはすでに説明した通りである。 irbが表示するプロンプトに対してRubyのコードを入力すると、 irbは入力された内容を逐次評価する。 そして評価結果をわかりやすい形で表示した上で次の入力を待つ。

irb(main):001:0> 1
1

上の例では1というRubyの式を評価して、 その評価結果(当然1になる)を表示してくれている。

irb(main):003:0> puts "abc"
abc
nil

こちらはputs "abc"を評価した際の様子を表している。 abcと表示されているのはputsによるものであり、 irbとは関係ない。irbで行った表示は その下にあるnilの方だ。 この結果からputsは与えられた文字列を表示し、 その結果としてnilを返していることが観察できる。

irb(main):008:0> a = 3
3
irb(main):009:0> if a%2 == 1
irb(main):010:1>   "odd"
irb(main):011:1> else
irb(main):012:1*   "even"
irb(main):013:1> end
"odd"

a = 3というコードは右辺値である3をそのまま返している。 その後のifendではaが奇数か偶数かを判定しているのだが、 if全体としてはその判定によって実行されることとなった部分が 返した値を返しているのが"odd"という表示からわかる。

プロンプトの読み方

ここでirbのプロンプトに注目してみよう。 irbのプロンプト(irb(main):001:0>)は5つの部分から成っており、 それぞれに解体すると以下のようになる。

なお、irbのプロンプトはカスタマイズすることが可能である他、 いくつかのパターンから選択することもできる。 たとえばirbのオブションで--prompt-mode simpleを指定すると、 その名の通りよりシンプルな表示がなされるようになる。

$ irb --prompt-mode simple
>> 3*3 + 6
=> 15
>>

より進んだ使い方

irbを使っていて直接的に便利に感じるのは、 入力したコードがどう評価されるかが即座にわかることだろう。 しかし、そうした直接的な便利さ以外にも irbならではの便利さがいくつかある。

その一つは入力に関する補完機能だ。 bashにはコマンド名やファイル名の入力途中でタブを押すと、 その時点での入力内容から推測できる文字列をbash自身が 補ってくれるという機能がある。 これと同じような機能をirbでも提供してくれている。 ただしirbの場合はファイル名などを補完するのではなく、 変数名やメソッド名を補完してくれるのだ。 しかも、オブジェクトが何であるかを判断した上で、 今そこで必要なものの中から補完してくれるのである。

この機能を使うためには一つだけ準備が必要となる。 それはirb/completionを読み込んでおくことだ。 読み込ませ方には次の3つのやり方がある。

以上、いずれかの方法での準備ができたら 次のようにタイプしてみてほしい。

irb(main):001:0> ary = [1, 2, 3]
[1, 2, 3]
irb(main):002:0> str = "abcdef"
"abcdef"
irb(main):003:0> 

そしてさらに、プロンプトに対してaとタイプした後に タブを二回ばかり押してみよう。 次のように入力に対する補完候補がリストされたはずである。

irb(main):003:0> a
abort     alias     and       ary       at_exit   autoload  

この状態でrをタイプし、もう一度タブを押す。 するとaryと補完されるだろう。 また、今補完されたaryに続けて.をタイプし、 くどいようだがまたタブが押す。 そうすると今度は大量に候補が表示されるはずだ。

irb(main):003:0> ary.
Display all 116 possibilities? (y or n)
                        ary.flatten             ary.protected_methods
ary.__id__              ary.flatten!            ary.public_methods
〜(略)〜
ary.find_all            ary.pop                 ary.untaint
ary.first               ary.private_methods     
irb(main):003:0> ary.

これは何かというと、配列オブジェクトであるaryが 受け付けることのできるメソッドがこれだけあるということを表している。 ここでeaとタイプし、タブを押すと、 もはやおなじみとなったeachが入力されるはずである。

ではstrについて同様のことをするとどうなるだろうか。 やはり大量の補完候補が表示されるのだが、 よく見るとaryの場合とは内容が異っていることがわかる。 このようにirbは補完の対象となるオブジェクトを見て 補完リストを作ってくれているのであり、 使いようによってはたいへん便利な機能となる。

irb(main):003:0> str.
Display all 116 possibilities? (y or n)
                        str.gsub                str.send
str.__id__              str.gsub!               str.singleton_methods
〜(略)〜
str.frozen?             str.scan                str.upto
str.grep                str.select              

サブirb

irbのもう一つの便利な機能はサブirbである。 名前の通り、すでに動作中のirbの中で もう一つ別のirbを動かしてしまうというのがその本質だ。 ところでirbがたくさん動くとどううれしくなるのだろうか。

サブirbはRubyのスレッドで実装されているため、 サブirbによれば複数のスレッドを切り換えながらの作業が可能となる。

irb(main):001:0> irb_jobs
#0->irb on main (#<Thread:0x402875c8>: running)

irb_jobsirbのスレッドが 今いくつ存在しているかを表示するメソッドである。 この段階では#0(0番目)のスレッドが1つあるだけだ。 ここでirbというメソッドを評価すると、 新しいサブirbが生成され、制御がそちらに移る。

irb(main):002:0> irb

たった今生まれたサブirb上でirb_jobsしてみると、 たしかにスレッドが増えていることを確認でる。 なお、#0の方にstopと出ているのは、 そのスレッドが休止状態にあることを示している。

irb#1(main):001:0> irb_jobs 
#0->irb on main (#<Thread:0x402875c8>: stop)
#1->irb#1 on main (#<Thread:0x402ba328>: running)

ここでサブirb上で配列aryを設定し、 もう一つのirbに戻ってみよう。

irb#1(main):002:0> ary = [1, 2, 3]
[1, 2, 3]
irb#1(main):003:0> irb_fg 0
#<IRB::Irb: @scanner=#<RubyLex:0x402c4fd0>, @context=#<IRB::Context:0x402c6628>, @signal_status=:IN_EVAL>

irb_fgというのはちょうどシェルでのfgと同じような意味を持つ。 この場合#0の(サブ)irbに制御を移そうとしているのである。

#0に戻ったところでこちらでもaryを設定してみよう。 その上でまた#1に戻りaryがどうなっているかを確認する。 すると#1で設定したaryがそのまま保存されていることがわかるだろう。 これはRubyのスレッドでは変数はそのスレッドにローカルなものとなるためである。

irb(main):003:0> ary = ["1", "2", "3"]
["1", "2", "3"]
irb(main):004:0> irb_fg 1
#<IRB::Irb: @scanner=#<RubyLex:0x402ba2c4>, @context=#<IRB::Context:0x402ba2d8>, @signal_status=:IN_EVAL>
irb#1(main):004:0> ary
[1, 2, 3]

ただしdefで定義されるメソッドや グローバル変数*3は、 本質的にグローバルな状態に変化を与えるものであるから この例のようにサブirb間で独立にすることはできない。

その代わりといってはいけないかもしれないが、こういうことができる。 以下はirbのドキュメントにあるサンプルセッションを元にしたものである。 Rubyでクラスの定義をする際にはclass Fooendで くくったところにコードを書いてやることになっている(ここで Fooはクラス名である)。

irb(main):001:0> class Foo      ← Fooクラスに関する定義開始
irb(main):002:1>   def hello
irb(main):003:2>     puts "Hello, World!"
irb(main):004:2>   end
irb(main):005:1> end            ← Fooクスラに関する定義ここまで
nil
irb(main):006:0> foo = Foo.new  ← Fooオブジェクトを生成
#<Foo:0x4029fcbc>
irb(main):007:0> foo.hello
Hello, World!
nil

一旦定義してしまったクラスにメソッドを追加することも可能だが、 その場合にもやはりclass Fooendでくくっておかなくてはならない。

irb(main):009:0> foo.byebye     ← 定義されていないので例外が起きる
NameError: undefined method `byebye' for #<Foo:0x4029fcbc>
        from (irb):9
irb(main):010:0> class Foo
irb(main):011:1>   def byebye
irb(main):012:2>     puts "byebye"
irb(main):013:2>   end
irb(main):014:1> end
nil
irb(main):015:0> foo.byebye     ← さっき定義したので今度は例外が起きない
byebye
nil

エディタで編集しているときは別として、 このような記述をirb上で繰り返すのは なかなかめんどうなものである。 そこでサブirbの登場だ。 先程の例では単にirbとだけタイプしてサブirbを起動したが、 実はこのときにサブirbを起動する コンテキストを指定することが可能となっている。 ここでいうコンテキストとは、 任意のオブジェクトやクラスのことであり、 そのようにして起動されたサブirbは そのオブジェクトの中にいるかのようなふるまいをするようになる。 言葉で説明すると難しいので次の例を見てほしい。

irb(main):001:0> def x           ← メソッドxを定義
irb(main):002:1>   puts "XYZ!"
irb(main):003:1> end
nil
irb(main):004:0> class X
irb(main):005:1>   def x         ← クラスXのためのメソッドxを定義
irb(main):006:2>     puts "xyz!"
irb(main):007:2>   end
irb(main):008:1> end
nil
irb(main):009:0> obj = X.new     ← Xオブジェクトの生成
#<X:0x4029e86c>
irb(main):010:0> irb obj         ← objに対するサブirbを起動
irb#1(#<X:0x4029e86c>):001:0> x  ← メソッドxを呼ぶと…
xyz!                             ← クラスXの方のメソッドxが実行されている
nil
irb#1(#<X:0x4029e86c>):002:0> irb_fg 0
#<IRB::Irb: @scanner=#<RubyLex:0x402c4fd0>, @context=#<IRB::Context:0x402c6628>, @signal_status=:IN_EVAL>
irb(main):011:0> x               ← #0に戻ってメソッドxを呼ぶと…
XYZ!                             ← 最初に定義したメソッドxが実行されている
nil
irb(main):012:0> obj.x
xyz!
nil

よく見るとサブirb(#1)の方はobjの内部に 入り込んで動作しているかのようみえることがわかるだろう。 ここでさらにサブirb(#1)の方で メソッドを定義するとどうなるか見てみよう。

irb(main):013:0> obj.y                 ← 存在しないメソッドなので例外が起きる
NameError: undefined method `y' for #<X:0x4029e86c>
        from (irb):13
irb(main):014:0> irb_fg 1              ← サブirb(#1)にスイッチ
#<IRB::Irb: @scanner=#<RubyLex:0x402bebe4>, @context=#<IRB::Context:0x402bec20>, @signal_status=:IN_EVAL>
irb#1(#<X:0x4029e86c>):003:0> def y    ← サブirb(#1)側でメソッド定義すると…
irb#1(#<X:0x4029e86c>):004:1>   puts "abc!"
irb#1(#<X:0x4029e86c>):005:1> end
nil
irb#1(#<X:0x4029e86c>):006:0> irb_fg 0 ← サブirb(#0)にスイッチ
#<IRB::Irb: @scanner=#<RubyLex:0x402c4fd0>, @context=#<IRB::Context:0x402c6628>, @signal_status=:IN_EVAL>
irb(main):015:0> obj.y                 ← きちんと定義されている!
abc!
nil

irb上でクラスを定義して、あるいはrequireして 使えるようにしたクラスの動作を確認したり、 修正したりするような場合、この機能は活用しがいがあるだろう。 ちなみにinstall_aliasesというメソッドを実行しておくと irb_fgirb_jobsの別名として fgjobsが使えるようになる。

irb(main):016:0> jobs
NameError: undefined local variable or method `jobs' for #<Object:0x4028bce0>
        from (irb):16
irb(main):017:0> install_aliases
Object
irb(main):018:0> jobs
#0->irb on main (#<Thread:0x402875c8>: running)
#1->irb#1 on #<X:0x4029e86c> (#<Thread:0x402bf0e4>: stop)

必要なら~/.irbrcに書き加えておくとよいだろう*4

Rubyとの違い

irbのドキュメントにも書いてあることだが、 irbではコードを逐次評価していくために 本来のRubyの挙動とは異った動作をしてしまうことがある。 それは以下のようなケースである。

irb(main):002:0> eval "foo = 0"
0
irb(main):003:0> foo
0

同じことをRubyでやろうとするとこのようになる。

$ cat x.rb
eval "foo = 0"
foo
$ ruby x.rb
x.rb:2: undefined local variable or method `foo' for #<Object:0x4028bce0> (NameError)

これは式の評価のタイミングが違うために起こる問題で、 現状ではirbの制限事項となっている。

Rubyをとりまく世界

この章のしめくくりとしてRubyで プログラムを書くのに便利なツールや、 Ruby用に書かれた各種のライブラリなどの集積地について紹介しておこう。

開発支援ツール

Rubyの標準配布物にはruby本体および標準ライブラリと 前述のirb以外にもいくつかのツールやが含まれている。

以下は標準添付ではないが、 インストールして使えるようにしておくと非常に便利なツールである。

RAA

RAA(Ruby Application Archive)はユーザたちがRubyで書いた 作品を持ち合い紹介している場所だ。

規模の面ではまだまだCPANなどには及ばないものの、 広い範囲のアプリケーションやライブラリが登録されている。 「これこれこういうことするの、ないかなあ」と思ったときには 一度のぞいてみるのもよいだろう。

ちなみにこのRAAにも登録されている SOAP4RXML-RPCには、 それぞれSOAPとXML-RPLでRAAにアクセスできる サンプルプログラムが同梱されている。 興味のある人は一度ためしてみるとおもしろいだろう。

まとめ

以上、Rubyの特徴について筆者なりの解説を試みてみたわけだが、 いかがだっただろうか。

Rubyの設計の根底には「使う人が楽できるよう インタプリタの実装で苦労しよう」というような部分がある。 作者のまつもとゆきひろさんをはじめとするコミュニティの人々は、 Rubyの世界の中での整合性がとれるよう、 また、全体として合理的な仕様になるよう常に腐心されており、 ユーザが不必要におどろかされることがないようになっている。 そうであるからこそわれわれユーザはプログラミングに集中でき、 「楽しいプログラミング」をすることができるようになる。

こうした特徴はRubyを使うほどに実感できてくるはずである。 まだRubyをさわったことはないが関心は持っていたという方は ぜひともこの楽しいRubyの世界を体験してほしいし、 すでにRubyを使い始めている人は、より深くRubyの世界にのめりこんでほしい。

この記事が何かのきっかけになれば筆者としてはよろこばしい限りである。

コラム

インストール(UNIX編)

Rubyのインストール方法には二つの方法がある。 一つはソースから構築する方法で、 もう一つはバイナリパッケージを利用する方法だ。

前者には開発環境が必要となるのに加えて、 お手元のOSなどに関する知識が必要となる。 そのため、もしもバイナリパッケージが提供されているのであれば、 何か特別な事情があってそうしたくない限り、 バイナリパッケージを利用した方がよいだろう。 たとえば現行のDebian GNU/LinuxやVine Linuxであれば 以下を実行すればよい。

# apt-get install ruby irb

必要に応じて関連パッケージもインストールしておくとよいだろう。

一方、ソースから構築する場合には次の手順をふむ必要がある。

$ ./configure
$ make
$ make test
$ make install

ただし環境などによってはこの限りではないので、 詳しくはソースに同梱されているREADMEファイルや Rubyのホームページにある インストールガイドを 参照してほしい。

インストール(MS-Windows編)

MS-Windows上でソースから構築するには開発環境が別途必要となる。 そうした環境をお持ちの方がどれくらいおられるかわからないが、 手元にはそのような環境がないので、 ここではバイナリパッケージについてのみ触れることにする。

MS-Windows環境用のバイナリパッケージにはいくつかの種類がある。 Cygwin版、mingw32版、mswin32版などで、 それぞれCygwin環境用、MinGWなGCCを使って構築されたもの、 VC++を使って構築されたものを指している。

インストール手順的に手軽なのはmingw32版とmswin32版だろう。 Cygwin版はCygwin環境を作らねばならないし、 そもそもCygwin環境の安定性にも気を遣わねばならない。 その点ではCygwin版は不利であるが、 一方で一部の機能はCygwin版でなければ利用できないというところもある。 結局、どれを選ぶかは使い方次第ということになる。

最後にRDEを紹介しておこう。 RDEはsakazuki氏によるMS-Windows用のRuby開発環境で、 スクリプトの作成と編集、 実行やデバッグを一連の動作として行うことができるよう作られている。 使用にはmswin32版のRubyがインストールされている必要があるので、 それをインストールして試してみてほしい。

イテレータを作ろう

本文中ではイテレータらしきものの定義をしてみているが、 ここで本当のイテレータ(実際には繰り返し的動作をする ブロック付きのメソッド呼び出しに対応したメソッド)を作ってみよう。 と言っても何か難しい儀式があるわけではない。 こんな具合に書き換えるだけのことである。

def my_each(array)
  for x in array
    yield(x)
  end
end

実はRubyのメソッドは、常に暗黙のブロックを受け付けることができる。 そうして受け付けた(受け付けてしまった)ブロックは yieldによって呼び出すことが可能である。 本文中のpseudo_eachの定義と比較すると、その関係がわかるだろう。

また、意味的にはほとんど同じと言ってよいのだが、 ブロックをより明示的に受け付けるやり方もある。

def my_each(array, &block)
  for x in array
    block.call(x)
  end
end

メソッド定義の引数の部分に&を前置した引数を 置くのである(ただし一番最後に一つだけしか置けない)。 こうすると呼び出し時に付けられたブロックを、 &付きの引数(この場合ならblock)によって受け取ることができる。 この時blockは手続きオブジェクトそのものであり、 手続きの発動にはcallメソッドを用いることになる。

実践プログラミング〜メール編

ここではRubyによってメールを送受信する方法について解説する。 より具体的には標準ライブラリであるnet/popnet/smtpの使い方についての説明となる。

メール送受信概説

実際のプログラミングを始める前に メールの送受信に関係する主なプロトコルについての 概要をおさらいしておこう。

SMTP、POP、IMAP、…

一口にメールの送受信と言っても、そこには様々な要素が関っている。 システムの構成要素という意味ではMTA、MDA、DNSサーバ、 POPサーバ、IMAPサーバなどはいわゆるメールサーバに関する 直接的な構力要素であるし、ユーザにとってはMUAが必要とされる。 ネットワークプロトコルという点について言えば、 少なくともSMTPとPOPまたはIMAPあたりはほとんどどこででも必要であり、 実際にそれらプロトコルをベースとしてメールシステムが成り立っている。

図: mail.png

各種プロトコルは次のような用途で使いわけられている。

SMTP

メールを送信するためのプロトコル。 詳細はRFC2821。

POP

リモートホストにあるメールボックスからメールを取り込むためのプロトコル。 サーバには単一のメールボックスしか置けない。 詳細はRFC1939。

IMAP

リモートホストにあるメールフォルダを操作するためのプロトコル。 同時に複数のフォルダを扱えたり、 メールの一部分にだけアクセスすることができるなどといった点で POPに対する優位性がある。 詳細はRFC2060、RFC2061。

Rubyとの関係

Rubyによるメール関係のプログラミングという場合、 どちらかというとMUAに近いサイドのプログラムを作成することが多いだろう。 MTAやMDAについては実績のある専用プログラムがすでにあるし、 IMAPサーバやPOPサーバについても同様であるからだ。 そうした場合、扱うべきプロトコルがSMTPやPOP、 IMAPであるということにはかわりはないが、 それぞれのクライアント側の処理をRubyで実装することになる。

Rubyではこれらの問題に対応するために 次のような標準添付ライブラリが提供されている。

これらはいずれもクライアント側の機能を提供するものである。 以下ではこれらのライブラリを使って、 SMTPおよびPOPによるメールの送受信をするための プログラムを作成して行きたい。 手順としては、まずnet/popを使ったプログラミングについて解説し、 その後でnet/smtpについて解説する。 その上で両方を使った応用を示す形とする。

なお、それぞれのプロトコル自体については必要に応じて触れるつもりだが、 詳細についてはRFCなどのドキュメントにあたるようにしてほしい。 またこの記事ではRubyによってメールをやり取りするところに主眼を置いており、 たとえばMUAはこうあるべきであるという話題について扱うものではない。 問題を簡単にするためにある程度の前提条件をおく場合があるので、 その点にも注意してほしい。

POPクライアントプログラミング

POPによってリモートサーバ上のメールボックスを読み取る場合、 以下のようなステップを経ることになる。

  1. 接続を開始する。
  2. ユーザ認証を受ける。
  3. 実際の操作を行う。
  4. 接続を終了する。

各ステップでのやり取りはすべてPOPでの規定に従って行われる。 これをサポートするライブラリnet/popではNet::POPや Net::APOPを提供する。これらは内部的にTCPSocketを使っていて、 POPのコマンドをソケットに書き込んでは サーバからの応答を受け取って解釈する―というのをある程度隠蔽してくれている。

もちろん、プロトコルそのものについてのある程度の知識は必要となるが、 Net::POPを使うことによってソケットの操作をしたり、 POPに合うようにコマンド文字列を構成したりする必要は なくなるわけである。

POPサーバに接続〜認証

まずは単純なところから実例を示してみよう。

require 'net/pop'

Net::APOP.start('pop-server', 110, 
               'username', 'password') {|pop|
}

これだけのコードで以下のことが行われる。 ここではNet::APOPクラスを使っているので認証はAPOPに従うことになるが、 もしもAPOPでない認証を行いたければNet::APOPの代わりに Net::POPを使うこともできる。

この中のどこかで失敗すると、その時点で例外が発生する。 一般に発生し得る例外は次の通り。

SocketError

ホスト名を解決できない場合にこの例外が発生する。

Errno::ECONNREFUSED

指定したサーバのポートでPOPサーバが起動されていないか、 あるいはアクセス制限などのために接続そのものに失敗した。

Net::ProtoAuthError

ユーザ名とパスワードの組みが正しくない場合に発生する。 またAPOPに対応していなPOPサーバに APOPで接続しようとした場合にもこの例外が発生する。

必要に応じて、これらの例外をrescueで捕獲してやるとよいだろう。

リモートメールボックスに対する操作

普通はPOPサーバにログインするだけで処理が終わることはなく、 ログイン完了をうけてメールを取り込んだり、 不要になったメールを削除したりするなどといった作業をする必要がある。

Net::APOPおよびNet::POPstartメソッドに与えたブロックは POPサーバへのログインが完了したのちに実行される。 実行時にはPOPサーバへの接続を司るNet::APOPないしは Net::POP3オブジェクトが渡され、 これに対してメソッドを投げてやることでPOPでのやり取りが可能となる。 定義されているメソッドとしては次のようなものがある。

mails

メールボックスにあるメールを表すオブジェクトの配列を返す。

each

上の配列に対するイテレータで、各メールに対して ブロックを適用することができる。

delete_all

すべてのメールに削除マークを付ける。

reset

メールの削除マークを取り消す。

またmailseachによって提供される メールオブジェクト(Net::POPMailオブジェクト)には 次のようなメソッドが用意されている。

pop

そのメール全体を取り出し、文字列で返す。

header

そのメールのメールヘッダだけ取り出す。

top(n)

そのメールのメールヘッダと、本文の最初のn行を取り出す。

delete

そのメールに削除マークを付ける。

deleted?

そのメールに削除マークが付けられていればtrue、 付けられていなければfalseを返す。

uidl

そのメールのUIDLを返す。

ここで注意しておきたいのは、削除マークが付けられたメールは startに対するブロックをぬけるタイミングで 自動的に削除されてしまうことである。 POP的に言うとstartブロックをぬける時に QUITコマンドを送っていることになる。 よって、もしも削除を取り消したければ、 resetメソッドを呼んでおかなくてはならない。

メールボックスの操作をしてみよう

ここで二つばかりサンプルを示そう。 まず一つ目はメールボックス中の あるサイズ以上のメールを削除してしまうというものだ。

require 'net/pop'

# ここでserver, port, user, passおよびsizeを設定する

Net::APOP.start(server, port, user, pass) {|pop|
  pop.each {|mail|
    mail.delete if mail.size >= size
  }
}

メールボックス中の全メールについて、 そのサイズがsize以上であるかどうかを判定し、 もしも大きければ削除マークを付ける。 途中何事もなければそのままブロックをぬけるため、 マークが付けられたメールについては削除が実行されることになる。 万一のことを考えるなら、削除マークを付けているところで 例外を捕獲するようにしておき、 何か例外が発生したらresetを呼ぶようにしておくとよいだろう。 いずれにせよこれを試すときには十分に注意してほしい。

二つ目のサンプルはメールボックスの内容をリストするというものだ。 以下のような内容をpopls.rbというファイル名で作っておく。

メールボックスにメールがあれば各メールのサイズを表示し、 もしメールがなけれればno mailと表示して終了する。 全体で60行ばかりになっているが、 本質的な部分はその半分にもみたないシンプルなものである。

SMTPクライアントプログラミング

SMTPはMTAに対してメールを送りつけるために使われるプロトコルで、 MUA->MTA間およびMTA<->MTA間で使われている。 特にMTA<->MTA間の場合にはDNSのMXレコードを参照しなくてはならないことや、 多数のメールを効率良く配送するための工夫が必要なことが多ことから、 MUA->MTA間の場合よりも処理が複雑になりがちではあるものの、 プロトコルとしてはまったく同じである。

SMTPにおける認証

以前は何も考えずにメールを送りつけ、 また受け側もほとんど無条件にメールを受け付けては さらなる転送をするという状況であった。 しかし最近では悪意をもって他サイトのリソースを かすめとろうとする悪質な業者が増えてきてしまったために、 MTAの設定は基本的にメールの転送を行わないものとなってきている。 つまり、組織内から組織外への、およびその逆向きの メール転送については許可するが、 組織外から組織外へのメール転送を禁止することによって、 自分のリソースを不正に使用されないようにするわけである。 ところがそうなると困ったことが起ってくる。 企業やISPなどにおいて、たとえば出先からメールを送りたいというケースでは 組織外から会社のサーバを経由してのメール送信となるわけであるが、 これが単純な方法では不正な組織外→組織外へのメールと区別できない。 結果的に本質的には不正使用でないにもかかわらず 不正使用であるとみてされてしまうのだ。

そのような背景のもと、じょじょに利用されるようになってきたのが POP befor SMTPやSMTP AUTHといった、 SMTP接続に関するある種の認証機構である。

POP befor SMTPはその名の通り(直接は関係ないが併設されがちな)POPによる認証を SMTPの前に行なうというもので、 どちらかというと運用上の工夫によって この問題に対処しようとするものである。 これに対してSMTP AUTHの方は SMTP自体に認証機構を組み込む拡張ということになっている。 これを導入するためには送信側と受信側の両方でSMTP AUTHに対応しなければならず、 そうするにはソフトウェアのバージョンアップが必要となる。 対するPOP befor SMTPでは、 受け側でだけソフトウェアの調整をすればよく、 少なくともMUA側には変更が必要ない。 メールを送信しようとする前にPOPで接続しておき、 POPでの認証をパスしておくだけでよいというものとなっている。

そうした状況から、現在は両方のやり方を提供しつつ、 じょじょにSMTP AUTHへの移行がされはじめているという時期だといえるであろう。 今回ここで扱うサンプルではSMTP AUTHについては触れていないが、 net/smtpにはSMTP AUTHに対応するコードも入っているようなので 読者のみなさんの手によってSMTP AUTH対応に改良してもらうのもよいだろう。

SMTPによるメール送信手順

SMTPによるメール送信についてのプロトコル的な手順は次のようになる。

  1. サーバに接続する。
  2. サーバに対して「挨拶」を送り、応答を待つ。
  3. 送信者のアドレスを送信し、応答を待つ。
  4. 受信者(送信先)のアドレスを送信し、応答を待つ。
  5. ヘッダおよびメール本文を送信し、応答を待つ。
  6. 接続を終了する。

「応答を待つ」と書いてあるところでは もちろんただ単に待っているだけではダメである。 届いた応答を読んで次のステップに進んで良いかどうかを判断し、 もしもなんらかのエラーが発生しているのであれば 処理を中断するなりなんなりの対処をしなくてはならない。 「挨拶」や送信先アドレスなどを送るのにしても、 必要な形式にそっていなければたちまち理解できないと言って つき返されてしまう。

こうしてつらつらと書きつらねてみただけでも、 「ちょっとメールを送りたいだけ」にしては 実にめんどうな手順を経る必要があることがわかるだろう。

Rubyによる場合

まず「Rubyならこう書ける」というのを示しておこう。

以下のコードを実行すると、 smtp-server.example.comのMTAを使って bar@example.jp宛のメールを発信することができる。 その際の発信者*5foo@example.comであり、メールの内容としてはbodyがあてられる。

require 'net/smtp'

Net::SMTP.start('smtp-server.example.com', 25) {|smtp|
  smtp.send_mail(body,              # メールの内容
                 'foo@exmaple.com', # 発信者
                 'bar@exmaple.jp)   # 送信先
}

このようにnet/smtpで提供されるNet::SMTPのインタフェースは Net::POPでのそれと良く似ている。

startに与えるメソッドは 上記の(1)および(2)が終ったタイミングで実行され、 その際にサーバとの接続を司るオブジェクトが渡される。 ブロック内では実際にメールを送るための処理、 すなわち(3)〜(5)にあたる処理を行う必要がある。 ブロックをぬけるとともにSMTP接続は切断され、 何も問題がなければメールはSMTPサーバに受理される。 もしも何らかの問題が起きた場合には例外が発生し、 処合は中断されることになる。

一般に発生し得る例外は以下の3つに加え、 POPのところで挙げたSocketErrorErrno::ECONNREFUSEDである。

Net::ProtoSyntaxError

SMTPでのやり取りについてのエラーがあった。 プロトコルで定義されていないコマンドを送ってしまった場合や、 サーバ側で実装されていない、あるいは禁止されているコマンドを 送ってしまった場合などに発生する。

Net::ProtoFatalError

復旧できないエラー状態に陥ってしまった。 送信先アドレスが有効ではない場合や、 ある種のアクセス制限によって拒絶された場合に発生する。

Net::ProtoServerBusy

サーバ側が一時的にサービスできない状態になっているときに発生する。

メール送りCGIプログラム

身近な例としてWebページのFORMへの入力値から メールを送信するCGIプログラムを考えてみることにする。 ここで考えるプログラムの仕様は簡単なものとしよう。

まずCGIの処理を行う部分からとりかかろう。 RubyでCGIプログラミングを行う際には 標準添付のcgi.rbを使うと便利である。 require 'cgi'しておくとCGIクラスが使えるようになり、 CGIオブジェクトを生成することによって CGIのパラメータ(FORMでの入力値など)に簡単にアクセスすることができる。

#!/usr/bin/ruby

require 'cgi'

cgi = CGI.new

output = ''
cgi.params.each {|name, values|  # CGIのパラメータにそれぞれに対する繰り返し
  values.each {|value|
    # パラメータ毎に
    #   名前:<改行>
    #   <空白>名前に対応する内容
    # という形式にする
    output << name + ":\n"
    output << ' ' + value.gsub(/\n/, "\n ") + "\n"
  }
}

cgi.out('type' => 'text/plain') {
  output
}

これはFORMからの入力をそのままブラウザ上に表示するCGIプログラムである。 CGI.newでCGIオブジェクトを生成すると、 paramsメソッドによってCGIのパラメータをハッシュの形で得ることができる。 ここではハッシュに対するイテレータを使って 入力されたすべてのパラメータをoutputにまとめ、 まとめたものをoutメソッドによってブラウザへの出力としている。 前述した通り表示の形式はメールヘッダに似た形である。

メールでも送信

これに改造を加えて次のようにすると 入力された内容をブラウザに表示すると同時に メールでも送信させることが可能となる。

#!/usr/bin/ruby

require 'cgi'
require 'net/smtp'

def sendmail(server, subject, body, sender, recipient)
  mail = ''

  # メールヘッダを作る
  mail << 'To: ' + recipient + "\n"
  mail << 'From: ' + sender + "\n"
  mail << 'Subjetct: ' + subject + "\n"
  mail << 'Date: ' + Time.now.strftime('%a, %d %b %Y %H:%M:%S %z') + "\n"
  mail << "\n"

  # メールの本文
  mail << body

  # 送信!
  Net::SMTP.start(server, 25) {|smtp|
    smtp.send_mail(mail, sender, recipient)
  }
end

cgi = CGI.new

output = ''
cgi.params.each {|name, values|
  values.each {|value|
    output << name + ":\n"
    output << ' ' + value.gsub(/\n/, "\n ") + "\n"
  }
}

sendmail('localhost', 'cgi sendmail', output, 
         'webmaster@example.com', 'info@example.com')

cgi.out('type' => 'text/plain') {
  output
}
例外対策

ただし、このままではメールの送信に失敗した場合のケアがまるでされておらず、 もしも何らかのエラーが発生すると、 ブラウザ上には例のInternal Server Errorが表示されてしまう。 これでは具合が悪いのでsendmailを呼び出している部分を beginendでくくり、適宜例外を捕獲してやると良いだろう。

begin
  sendmail('llocalhost', 'cgi sendmail', output, 
           'webmaster@example.com', 'info@example.com')

  cgi.out('type' => 'text/plain') {
    output
  }

rescue SocketError
  cgi.out('type' => 'text/plain') {
    "smtp server not found.\n" + output
  }
rescue Errno::ECONNREFUSED, Net::ProtocolError
  cgi.out('type' => 'text/plain') {
    "sending mail failed.\n" + output
  }
end

実際の場面では、上のようなシステム側のエラー状態に対処するだけではなく、 入力値の妥当性についてのチェックなども必要となってくる。

メール送信での問題点

ところで、入力された内容をメールで送信しようというわけであるから、 文字コードの問題を注意事項として挙げることができる。 CGIプログラムに対する入力はどのような文字コードで行われるかわからず、 何らかの文字コードを想定することはできない。 この点については状況によっていくつかの回避方法が考えられる。

さらにやり取りする内容によっては、 メールの性質的にどうしても覗き見に対する防御を考えなくてはならなくなる。 そうしたことを考えて、sendmailメソッドの定義を少々変更し メール本文にあたる部分にフィルタ的な処理を施せるようにしたのが以下である。

def sendmail(server, subject, body, sender, recipient)
  mail = ''
  mail << 'To: ' + recipient + "\n"
  mail << 'From: ' + sender + "\n"
  mail << 'Subjetct: ' + subject + "\n"
  mail << 'Date: ' + Time.now.strftime('%a, %d %b %Y %H:%M:%S %z') + "\n"
  mail << "\n"

  if block_given?       # もしブロック付きで呼び出されていたら
    mail << yield(body) # 本文に対してブロックを適用する
  else                  # そうでなければ
    mail << body        # 何もしない
  end

  Net::SMTP.start(server, 25) {|smtp|
    smtp.send_mail(mail, sender, recipient)
  }
end

こうするとsendmailメソッドを呼び出す時に ブロックを与えておくことで、 メール本文に対してそれを適用してから 実際のメール送信することができるようになる。

暗号化についても同じ要領で実現することは可能だろう。 たとえばGnuPGなどを使って上のbodyに対して 暗号化を行うわけである*7

応用編

しめくくりに、メール送受信に関する応用的なプログラムを紹介しておこう。

mailclient.rb

mailclient.rbは元々は前田修吾氏が作成されたプログラムで、 その後にわたなべひろふみ氏らによって改良されたものである。

mailclient.rbsendmailコマンドのかわりに 使うことができるよう作られている。 MUAなどで使用すると、発信されたメールをいったん受けとり、 後でまとめて別のMTA(たとえばISPが提供するMTA)に 送信できるようにあるところに保存しておくという動作をする。 一方、単体のコマンドとして使用すると、 fetchmailのように、指定したPOPサーバにアクセスして メールを取り込んでくれるのと同時に、 「あるところ」に保存されていたメールを改めて送信する。

300行にみたない小さなプログラムであるが、 こまわりのきくなかなか便利なツールだと言えるだろう。

使用方法

使用するにあたっては設定ファイルを記述しておく必要がある。 設定ファイルは~/.mailclient.rbで、 ファイル名からわかる通りRubyのスクリプトとして 記述する仕様となっている。 Rubyのスクリプトとは言っても難しいものではなく、 通常は以下のような内容を書いておけばよいだろう。

SMTP_SERVER = "smtp-server.example.com"
MDA = "/usr/bin/procmail -d %s"
POP[0] = {
  :TYPE     => APOP,
  :SERVER   => "pop-server.example.com",
  :USER     => "username",
  :PASSWORD => "password"
}

設定ファイルの準備ができたらあとは実行するだけだ。 ファイル名をsendmailimputとして実行するか、 オプション--sendmailを付けて実行すれば sendmailコマンドのかわりとして動作してくれる。 そうでなければ保存されていたメールの送出と、 POPサーバからのメール取得を同時進行で行う。

内部構造

内部的にはnet/popnet/smtprequireしているが、 ここで解説したブロックを使ったやり方とは別のやり方で 各ライブラリを使っている。おおまかには以下のようなイメージである。

pop = Net::POP.new(server)
pop.start(user, passwdrd)
begin
  pop.mails.each {|mail|
    ...
  }
ensure
  pop.finish
end

pop.startpop.finishの間が ちょうどブロックの中身に相当する。 ブロックを使うやり方とそうでないやり方の どちらのやり方が正しいというのは特になく、 単にプログラミングのスタイルの問題だと言える。 好みに応じて、あるいは必要に応じて、書き方を選べばよいだろう。

ただし、このケースについては ブロックを使わない場合にpop.finishを忘れがちなので、 その点に注意してほしい。 プロトコル的にはfinishメソッドによって QUITコマンドが発行される。 そのため、特にdeleteしている場合にfinishし忘れると 消したつもりのメールがまるで消えてくれない*8という 現象に合うことになる。

メールの送信の実務を受けもつのはsendqueueメソッドであり、 POPサーバからの取得を受けもつのはfetchmailメソッドである。 mailclient.rbの最後のあたりを見ると、 オプションなどを何も指定せずにプログラムを実行した場合には sendqueueを別スレッドとして実行しているのがわかる。 この順序で処理が進むとすると、 前述したPOP befor SMTPを使ったアクセス制御がされているシステムにおいては メールを送ることができなくなってしまう。 この問題に対処するためにはsendqueueメソッドを 呼び出すよりも前にPOPでのやり取りをはじめれば良いわけだから、 mailclient.rbをPOP befor SMTP対応にするのは それほど難しいことではないことがわかる。 練習問題としてmailclient.rbの改造にチャレンジしてみるのはどうだろうか。

コラム

cdbiff

メールと言えば、Rubyで書かれたおもしろいツールがある。 Namazu、Migemo、ttyrecなどでおなじみの高林哲氏が作成された cdbiffというのがそれだ。このツールを使うとどうなるかは 次の写真を見てもらえば一目瞭然だろう。 要するにメールの到着をCD-ROMドライブでお知らせするというものだ。

動作としては、ホスト上のメールボックスを一定時間毎に見に行って、 最後更新時刻が変わっていたらejectコマンドを起動する。 するとCD-ROMドライブのトレイにしこんだCDに書れたメッセージが (場合によっては)目の前に現れるというわけだ。

このままでも十分におもしろいのだが、 これをネットワーク透過にしてしまった人がいる。 dRuby、ERb、RWikiでおなじみのさんである。 彼はcdbiffを元にしてdbiff.rbdcdbiff.rbの 二つのプログラムを作成した。 前者は監視するメールボックスがあるホスト上で動かすもので、 メールボックスの状態をdRubyによって通知することができる。 一方後者はdRubyによる通知を受け取ることができるようになっているので、 両者を合わせるとリモートホストのメールボックスを監視することのできる cdbiffができあがる。

ちなみにdcdbiffはdRuby越しにObservableを使った構成になっており、 dRubyの透明性をうかがわせる良いサンプルとなっていることにも注目したい。


*1実数としてはせいぜい数人とか 十数人といったところだろうが。
*2ただし、 あえてそのような書き方をすることはほとんどあり得ない。
*3Rubyでは頭に$がついているのが グローバル変数の印である。グローバル変数を使いたければ、 その変数名は必ず$で始まる必要がある。 $も変数名の一部である。
*4最近リリースされた irb 0.8ではinstall_aliasesは不要で、 何もしなくてもjobsなどが使えるようになっている。
*5Fromヘッダの内容ではなくSMTP的な発信者アドレスのこと。
*6送り先をFORMから入力させたり、 かくしフィールドで埋め込むのは危険である。
*7その際、 GnuPGにオブション--armorを付けておくと良い。
*8POPではDELEコマンド発行後、 QUITコマンドでセッションを閉じるタイミングで実際の削除が行われる。