読者です 読者をやめる 読者になる 読者になる

【Ruby】正規表現で文字列を置き換えようとしてハマった

ある文字列について、あるパターンに合致した部分を装飾して置き換えたいことがあった。

例えば、「123hoge456」を「123 + hoge + 456」みたいにhogeをマッチさせて再利用する感じ。

そのためのコードを以下のように書いたのだが思ったように動かない。

str = "123hoge456"
str.sub!(/(hoge)/, " + #{$1} + ") # => "123 +  + 456"

どうやら、$1に何も入っていない? そう思って次の行で$1を出力してみる

1    a = "123hoge456"
2    a.sub!(/(hoge)/, " + #{$1} + ") # => "123 +  + 456"
3    p $1 # => "hoge"

あれ、$1にはちゃんと入っている??
更に、実際には書いてたコードでは2回こういう処理するようになってたのに更に奇妙な挙動になってた

1    a = "123hoge456"
2    b = "123huga456"
3    a.sub!(/(hoge)/, " + #{$1} + ") # => "123 +  + 456"
4    b.sub!(/(huga)/, " + #{$1} + ") # => "123 + hoge + 456"

ここで分かった人には分かったと思うのだが、実は最初のsubの呼び出し時点ではまだ$1はnilになっているのが原因。
3行目が終わったところでは$1に"hoge"が入っているので、さっきの例では出力できたし、この例では次の4行目の引数に与えられている。
なんとなく第一引数の正規表現がすぐに処理されているように感じるけど、メソッド呼び出す時点では処理されていないってわけで。
なるほどなー

じゃ本来やりたかったことをやるにはどうすればよいかというと、以下のようにsubにブロックを渡してやると良い。

1    a = "123hoge456"
2    a.sub!(/(hoge)/){ " + #{$1} + " } # => "123 + hoge + 456"

更に言うと、そもそもブロック使うならマッチ部分を受け取れるので、

1    a = "123hoge456"
2    a.sub!(/(hoge)/){|match|  " + #{match} + " }# => "123 + hoge + 456"

これで事足りるのであった。。。

よくよくみたらリファレンスにも書かれてた。
http://docs.ruby-lang.org/ja/2.1.0/class/String.html#I_SUB

注意:

第 2 引数 replace に $1 を埋め込んでも意図した結果にはなりません。 この文字列が評価される時> 点ではまだ正規表現マッチが行われておらず、 $1 がセットされていないからです。