Rubyで正規表現の一致部分に対する置換

Ruby正規表現の一致部分($1、$2、...)に対する置換をしたいのだが、できる方法が見つからない。

やりやいことは、例えば以下のような置換。

<div id="XXX" class="YYY" ~>~
↓divでclassがYYYの行に対してidとclassの後ろにAAAを追加する。
↓/<div\s+id=\"(\w+)\"\s+class=\"(YYY)\"/の$1と$2のマッチした部分を置換したい。
<div id="XXX-AAA" class="YYY-AAA" ~>~

正規表現で置換部分が1つだけ、かつ後方参照を使わなければ、以下でできる。

class String (Ruby 2.4.0)

self[regexp, nth] = val

× nthが複数必要な場合には対応できない。

× 後方参照が必要な場合も対応できない。

例えば、文字列で一致した一部をupcaseで置き換える場合はうまくいかない。

# ruby -e "str='abc'; str[/a(b)c/,1]=$1.upcase"
-e:1:in `<main>': undefined method `upcase' for nil:NilClass (NoMethodError)

当たり前だけど、先に評価すれば$1が設定されて、置き換えはできる。

# ruby -e "str='abc'; if /a(b)c/===str then; str[/a(b)c/,1]=$1.upcase; end; p str"
"aBc"

すでにやり方は確立されていそうだが、探しても見つからないので、簡単なメソッドを定義して実現してみる。

def replace_matches(str, re, vals)
	m = re.match(str)
	if m then
		(m.size-1).downto(1)do |i|
			next unless vals[i-1]
			ofs = m.offset(i)
			str[ofs[0], ofs[1]-ofs[0]] = eval(vals[i-1])
		end
	end
	return str
end

第1引数`str'は置換対象の文字列。

第2引数`re'は置換するための正規表現(後方参照する部分は重ねてはならない)。

第3引数`vals'は置換文字列の式で以下になる。

  • 文字列の配列で指定する。
  • 文字列は後でevalで評価するので式を書く。変換しない場合はnil
  • $1、$2、...はevalのときの後方参照で評価されるので、第2引数`re'の後方参照の指定に一致する。

返却値は変換後の文字列。一致しない場合は元の文字列。

require用にrm.rbに保存して実行して意図どおりの結果が出力されることを確認。

# ruby -r "./rm" -e "str='abc'; p replace_matches(str, /a(b)c/, [%Q{$1.upcase}])"
"aBc"

複数の場合も問題なし。$3を$1を使って置換するのもOK。

# ruby -r "./rm" -e "str='abc'; p replace_matches(str, /(a)(b)(c)/, [nil,%Q{$2.ord.to_s},%Q{'%02X' % $1.hex}])"
"a980A"

なお、後方参照を後方から置換しているので、MatchDataのoffsetがずれない。

前方から置換した場合
offset => [[0,1],[1,2],[2,3]]
abc => abc(変換なし)
abc => a98c
a98c => a90Ac
後方から置換した場合
offset => [[0,1],[1,2],[2,3]]
abc => ab0A
ab0A => a980A
a980A => a980A(変換なし)