1. 目的
テキストファイル内の、特定の文字列を変換し、新たなファイルに保存したい。
2. 問題
例えば、あるディレクトリ中の HTML ファイルを対象にして、img 要素の属性を変更したい。ただし、元のファイルを直接変更せず、拡張子を .php に変更したファイルを新たに作成する。
img 要素の属性を変更する仕様は、
<img class=”XXXXX” src=”YYYYY” alt=”ZZZZZ” />
と記述があったら、
<img clas=”XXXXX” src=”<?php echo PATH . "YYYYY"; ?>" alt="ZZZZZ" />
とする。
3. 方法
ファイルの操作。
エンコーディング。
- magic comment : スクリプトのエンコーディングを指定。
- Encoding.default_external : 入出力のエンコーディングを指定。
特殊な記法。
4. ソースコード
# -*- coding: utf-8 -*- Encoding.default_external = 'UTF-8' Dir.glob("*.html") do |f| File.open(f, "r") do |rf| File.open(File.basename(f, ".*") + ".php", "w") do |wf| rf.each do |line| if %r|(.*<img.+src=")(.+?)"| =~ line wf.puts %Q|#$1<?php echo PATH . "#$2"; ?>"#$'| else wf.puts line end end end end end
上記は、UTF-8 で記述。
5. 変更したい点
上記は、以下の点が固定されている。
- 対象とするファイル
- ファイル名の変換の方法
- 特定の文字列の変換の仕方
これらの値と処理を変更できるようにしたい。
手続きオブジェクトの利用
処理を変更可能にする場合、関数の引数に、関数を渡せるように変更すれば良い。シンプルな処理なので、ストラテジーパターンを適用するほどでもない。 Ruby は関数を直接渡せないので、この場合、手続きオブジェクト を代わりに与える。
手続きオブジェクトとはブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクトしたものです。Proc クラスのインスタンスとして実現されています。
関数の引数に、関数を渡せる言語では、素直に与えられた関数を呼び出すだけ良い。これに対して、Ruby で手続きオブジェクトを呼び出すには、Proc#call メソッドを使う。例えば、f を手続きオブジェクトだとすると、
f.call(引数)
と書いて呼び出す。
上記 f に対応した、無名関数に相当する手続きオブジェクト作るには、
lambda { |引数| 式 }
と記述。lambda の代わりに proc と書くことができるが、lambda の方がシンプルのようだ。
( cf. Ruby のブロックと Proc )
正規表現にマッチした変数を遅延評価
パターンマッチに成功した値を格納する $1 のような特殊な変数の値は、パターンマッチが行われたタイミングに依存する。そのため、$1 を含む正規表現リテラルを交換できるように処理から抽出する場合、変数の評価が、パターンマッチ後に行われるようにしなければならない。
評価を遅延させるためには、上記の手続きオブジェクトを利用する。
( cf. メタプログラミングRuby, p123 )
オープンクラス
拡張子を除いたファイル名を取得するメソッド名が一見したところわかりづらいので、既存の File クラスにメソッドを追加する。
変更したソースコード
# -*- coding: utf-8 -*- Encoding.default_external = 'UTF-8' class File def self.without_ext(filename) self.basename(filename, ".*") end end def convert(targets, cnv_filename, f) Dir.glob(targets) do |filename| File.open(filename, "r") do |read_file| File.open(cnv_filename.call(filename), "w") do |write_file| read_file.each do |line| write_file.puts(f.call(line)) end end end end end re = %r|(.*<img.+src=")(.+?)"| rplc = lambda{ %Q|#$1<?php echo PATH . "#$2"; ?>"#$'| } convert("*.html", lambda { |filename| File.without_ext(filename) + ".php" }, lambda { |line| if re =~ line then rplc.call else line end })
矢印ラムダ
Ruby 1.9 より、lambda の代わりに矢印ラムダを使える。
使い方は、Arrow Lambdas, a Ruby 1.9 Vignette | Rails Fire によると、
-> { } # the basic form ->(x) { x * 2 } # with an argument -> x { x * 2} # (without parenthesis, too) ->(x, y) { x * y } # multiple arguments ->(x, y = 2) { x * y } # default arguments ->(x, y, &z) { x * y * z.call } # take a block
cf. The Ruby programming language - Google ブックス
こちらの書き方のほうが、引数が、関数の引数らしく見えるかも。