【Rubyプログラミング狂室】
mewgrep・Mewのフォルダを串刺し検索・コマンド行版
(2002.03.14)

概要

 ついにアイデアがやってきた。

 MewというGNU Emacs 上 のMUAを使っている。なかなか気に入っているのだが、ひとつ不満 に思うのは、1メイル1ファイル方式のため、「ある語句が含まれるメイ ルを検索する」ということが不得手な点。

いや、ぼくが調査探究を怠っているだけで、できるのかも知れない。
でもそうするとRubyでプログラムを書く機会が奪われるので、そんな機 能はないことにしてもらおう。

 そこで、このいわゆる「串刺し検索」を行なうツールを書いてみよう。 まずはコマンド行から使うバージョンを作り、それをRuby/TkでGUI化し てみよう。

着眼

 要するに、ディレクトリを降りていきながらの再帰的grepである。途 中まで書いてふと気づいたのだが、 Ruby本に載って いるサンプルrgrep.rbをちょっと変更すればできてしまうのである。 くやしいのである。だから自己流で行くだけ行ってみるのである。

 rgrep.rbは手続き的に書かれているが、こちらは「フォルダひとつの 処理をひとつのオブジェクト(インスタンス)が受け持つ」ということ にして書いてみた。インスタンスの生成や消滅のコストを考えれば、こ ちらの方が遅い(筈)。でも普通の再帰(?)はもうさんざん書き飽きてい るし。

外部仕様をどうしよう

 駄洒落じゃないんです。

 いちおうこんな風にしておこう。

% mewgrep.rb <-a | foldername> [pattern]
  1. -aを指定した場合は全フォルダから検索する。
  2. foldernameはMewのフォルダ名(ただし"+"はつけない)
  3. サブフォルダはfolder/subfolderのようにして指定 する。
  4. 探索文型patternを省略すると、プロンプトが表示され、標準入力 から入力する。空入力の場合は実行を中断する。

対象の特徴を吟味してやっつけ方(仕様でもある)を考える

 このツールは当然ながらMewに依存している。だから仕様や内部構造 がMewの仕様に依存するのも当然である。そこでまず、Mewの仕様をよく 観察する。

  1. メッセージは所定のディレクトリ(トップディレクトリとここでは 呼ぼう)配下に、フォルダごとに分類されて保存される。
  2. 実際にメッセージが保存されるのはフォルダ(実体はディレクトリ) である。
  3. フォルダの配下にはメッセージその他のファイル、サブフォルダを 置くことができる。
  4. フォルダ名は"+フォルダ名"で表される。実際のディレクトリには "+"はつかない。
    サブフォルダは"+フォルダ/サブフォルダ"のように表記する。
  5. 名前が"."で始まるファイルにはMew自身が用いるデータが記録され ている。
  6. ひとつのファイルにひとつのメッセージが収められる。
    メッセージのファイルは数字のみからなるファイル名を持つ。これ はフォルダ内のメッセージ番号である。
  7. メッセージの文字コードはJIS(ISO-2022-JP)である。

 というところから、次のようにやっつけることにしよう。

コード

 コメントとかメソッド名、変数名などを エスペラントで書いたりしている ところがあるけれど、気にしないでください。

  1: #! c://loka/ruby/bin/ruby
  2: #
  3: # mewgrep.rb -- sercxi sxablonon el mesagxojn de Mew
  4: #
  5: # mewgrep.rb <-a | dosierujanomo> [sxablono]
  6: #
  7: # Se oni specifas opcion -a, gxi sercxas el cxiuj dosierujoj.  
  8: # Se oni funkciigas gxin sen sxablono, gxi postulas enigon de
  9: # uzanto.
 10: #
 11: 
 12: require 'jcode'
 13: $KCODE = 'EUC'
 14: 
 15: class MewFolder
 16:   require 'kconv'
 17:   require 'nkf'
 18: 
 19:   # path .. Mewのメイルフォルダを指すパス
 20:   def initialize(path)
 21:     @path = File::expand_path(path)
 22:     @dir = Dir.new(@path)
 23:     @summary = {}
 24:   end
 25:   attr_reader :path
 26: 
 27:   def retrieve(pattern)
 28:     # .mew-cacheの内容を読み込んでおく
 29:     fari_resumtabelon()
 30: 
 31:     # ディレクトリ内の各ファイルを数値的に整列
 32:     # (ファイル名 = メッセージ番号のため)
 33:     entries = @dir.collect{|name| 
 34:       [name, 
 35: 	path = File::expand_path("#{@path}/#{name}"), 
 36: 	File::stat(path).ftype]
 37:     }.sort{|a, b| a[0].to_i <=> b[0].to_i}
 38: 
 39:     # メッセージがあればそれを検索
 40:     entries.each do |name, path, ftype|
 41:       if /^\./ =~ name then next
 42:       elsif ftype == "file" && /^\d+$/ =~ name then
 43: 	if ekzameni(path, pattern) then
 44: 	  # .mew-cacheから該当するメッセージの行を抜いて印字
 45: 	  printline(akiri_resumon(name))
 46: 	else
 47: 	  # nothing.do
 48: 	end
 49:       end
 50:     end
 51: 
 52:     # サブフォルダは後でまとめて再帰
 53:     entries.each do |name, path, ftype|
 54:       if /^\./ =~ name then next
 55:       elsif ftype == "directory" then
 56: 	print "#{path}: \n"
 57: 	daughter = MewFolder.new("#{@path}/#{name}")
 58: 	daughter.retrieve(pattern)
 59:       end
 60:     end
 61:   end
 62: 
 63:   protected	# 以下、無関係なクラスには見せない
 64: 
 65:   # ファイル内の各行を指定の文型と照合する
 66:   def ekzameni(path, pattern)
 67:     open(path, "r") do |f|
 68:       while(line = f.gets) do
 69: 	if pattern =~ NKF::nkf("-e", line) then
 70: 	  return true
 71: 	end
 72:       end
 73:     end
 74:     return false
 75:   end
 76: 
 77:   def printline(resumo)
 78: 	  print NKF::nkf("-s", resumo)
 79:   end
 80: 
 81:   # 指定のメッセージに対応するサマリーの行を得る
 82:   def akiri_resumon(msgno)
 83:     @summary.key?(msgno) ? @summary[msgno] : 
 84:       "#{msgno}: not found in summary\n"
 85:   end
 86:   
 87:   # サマリー(.mew-cache)の内容を読み込んでおく
 88:   # rescueはトップレベル(~/Mail)にはないため
 89:   # @summaryはメッセージ番号をキーとする、サマリー行のハッシュ
 90:   def fari_resumtabelon
 91:     begin
 92:       sumtmp = []
 93:       open("#{@path}/.mew-cache") do |f|
 94: 	sumtmp = f.readlines
 95:       end
 96:       sumtmp.each do |elem|
 97: 	momo = elem.split(" ", 2)
 98: 	@summary[momo[0]] = elem.sub(/\r.*$/, "")
 99:       end
100:     rescue
101:     end
102:   end
103: 
104: end
105: 
106: # コマンドの側
107: 
108: if $0 == __FILE__
109: 
110: MewTop = "~/Mail"
111: 
112: if(ARGV.size == 0) then
113:   print <<-USAGE
114: 	usage: mewgrep.rb <-a | fildername> [pattern]
115: USAGE
116:   exit 0
117: end
118: 
119: folder = ARGV.shift
120: if folder == "-a" then
121:   folder = MewTop
122: else
123:   folder = "#{MewTop}/#{folder}"
124: end
125: 
126: pattern = NKF::nkf("-e", ARGV.shift)
127: if pattern == nil then
128:   $stderr.print "pattern = "
129:   pattern = gets
130: end
131: if /^\s*$/ =~ pattern then
132:   $stderr.print "Aborted.\n"
133:   exit 0
134: end
135: 
136: pattern = Regexp.new(pattern, 0, "euc")
137: 
138: folder = MewFolder.new(folder)
139: folder.retrieve(pattern)
140: 
141: end # $0 == __FILE__
142: 
143: # end of file

補足

 最初のバージョンを書いてちょっと使ってみたが、遅かったのであれ これチューニングしてみた。早速プロファイラーも使ってみた(^^)  ライブラリをロードするだけでソースには全く手を加えずにプロファ イルがとれるのは、動的言語のうれしさ、なんだろうか。

 測ってみて、遅そうなところを早そうなコードに書き換えてみるが、 有意な差は出なかった。配列の処理などで小細工をしてみても、全体に は大きな影響を与えないようだ。それは当然なので、いちばん時間を食っ ているのがファイルからの読み込みと文型探索だとプロファイラーは報 告している。ここに手を入れなければ大して意味がないのだ。小細工に 血道を上げるよりは、判りやすさ、構造の簡単さを保つ方が引き合うと いう、当たり前の結論に達した。300個のメッセージを検索するのに30 秒かかるのは仕方ないと(笑)

寄り道

 RubyはJISコードによる正規表現は実装していない。作者によれば 「状態を持つコード体系なので原理的に対応できない」のだそうだ。そ うなのかも知れない。だとすると、これは正規表現の致命的な限界を示 唆しているのかも知れない。正規表現自体は数学の裏づけを持つ形式言 語だが、どのような文字コード体系でも実装できるわけではない、とい うことだ。

 あるいはそれは別に驚くべきことではないのかも知れない。正規表現 と文字コード体系は互いに独立であり、特に文字コードは正規表現のこ となどこれっぽっちも気にかけない放蕩息子、父帰る、海原雄山だった りする。いくら貞淑な正規表現としても我慢の限界がある。しかし一方、 正規表現の実装だって、もとはといえばASCIIという1バイト(8ビット) も使わない文字コード体系の上で練り上げられてきたものだ。他の世界 を知らずに育った箱入り娘なのだ。「状態を持つ家系の方とは結婚でき ません」と言い切ってしまうのもいかがなものか。

 ぼく自身は普段の生活でJISコードをそのまま使うことは殆どないから いいけど、メイルの検索なんていう時にはJISのまま文型探索できた方 が効率がいいに決まっている。いつか、どこかに、放蕩息子と箱入り娘 が折り合いをつける地平が開けることを期待したい。(自分で切り開けっ て? ごもっとも)

(2002.03.14)

Rubyプログラミング狂室へ
参考図書へ
コンピューター言語研究所へ
トップページへ
(C) ©Copyright Noboru HIWAMATA (nulpleno). All rights reserved.