作者: Kazuhiro Yoshida
日時: 2002/4/30(00:27)
もりきゅうです。

人の書いたRubyプログラムを読めるようになったら一人前です。その実装
に至った過程を想像してみましょう。

■ Rubyカッター

応用として、本講座に出てくるRubyコードを切り取るプログラムを書いて
みましょう。

まず完成品を見てもらいます(もちろん、ほかの実装も考えられます)。

--^ cut0.rb
file = STDOUT
while line = gets
  case line
  when /^--\^ +([\w.+-]+)/
    path = $1
    file = open(path, 'w')
    next
  when /--\$$/
    file.print $`
    file = STDOUT
    next
  end
  file.print line
end
--$

$ ruby cut0.rb rec1.txt rec2.txt

うわ。なんじゃこりゃ。と思われた方は次のように分けてみましょう。

file = STDOUT
while line = gets
  file.print line
end

  case line
  when /^--\^ +([\w.+-]+)/
    path = $1
    file = open(path, 'w')
    next
  when /--\$$/
    file.print $`
    file = STDOUT
    next
  end

上の断片はフィルタの基本形ですね。STDOUTは標準出力(Standard Output)
です。正確に言うと標準出力を示すFileオブジェクトを格納している定数
です。

下の断片はパターンマッチでマークのある行を調べて、fileを切り替えて
います。case when は見慣れないですね。まだ説明していませんでした。

まず上の断片に注目します。
file という変数がずっと STDOUT を指したまま変わらないとすれば

while line = gets
  STDOUT.print line
end

ということになります。これは標準出力に line を出力します。[*1]

下の断片で

    file = open(path, 'w')

    file = STDOUT

のように file への代入が行われているので、file はいつも STDOUT と
いうわけではないようです。出力先を切り替えるために file という変数
を用意したんですね。

次に下の断片に注目します。
when の中を省略すると次のようになります。

  case line
  when /^--\^ +([\w.+-]+)/
    #(A)
  when /--\$$/
    #(B)
  end

case when ... end は、if ... end で書くと

  if /^--\^ +([\w.+-]+)/ === line
    #(A)
  elsif /--\$$/ === line
    #(B)
  end

という意味です。正規表現(Regexp) の場合 === は =~ と同じです。[*2]

/^--\^/
  \^ は ^
  --^ で始まる行にマッチする

/--\$$/
  \$ は $
  --$ で終わる行にマッチする

/ +([\w.+-]+)/
  1個以上の空白 それに続く 1個以上の [\w.+-] にマッチする

つまり、line が行頭の --^ ファイル名 にマッチしたら (A)、行末の --$
にマッチしたら (B) を処理することになります。

(A)
    path = $1
    file = open(path, 'w')
    next

一行ずつ見ていきましょう。

    path = $1

$1 は最後に成功したマッチの1番目の部分文字列です。[*3]
部分文字列とは、正規表現の中で () で囲んだパターンにマッチした部分
の文字列のことです。この例ではファイル名(path)にマッチすることが期
待されます。

    file = open(path, 'w')

pathをファイル名として書き込みモード('w') でファイルを開きます。こ
うすると新しいファイルが作られます。

    next

while に帰って次の処理に移ります。ちなみに、while を抜けるには
break を使います。ほかの言語を使っている方は continue とか last な
どと書かないようにご注意を。^^;

(B)
    file.print $`
    file = STDOUT
    next

ここも一行ずつ。

    file.print $`

$` は最後に成功したマッチの前方の文字列です。[*4] つまりここでは
--$ が(行末にはあるんだけど)行頭にない場合に残りの文字列を file
に出力しています。

    file = STDOUT

標準出力に戻します。

    next

while に帰って次へ。どうして next が必要なのか? 仮に next がなかっ
たとすると

  file.print line

を評価してしまいます。マーク行を出力してしまうんですね。

今日はここまで! 難しかったかな?

*1
  STDOUT.print と print は構文上はかなり違うものですが、結果は同じ
です。今は気にしないでおきましょう。

*2
  === は case 用の演算子(メソッド)。各クラスで定義されています。

*3
  $1 に対応するオブジェクト指向的な表現は Regexp.last_match[1]

*4
  $` は Regexp.last_match.pre_match と同じ

----
YOSHIDA Kazuhiro  moriq@...  http://www.moriq.com/