sub, gsub でバックスラッシュ(\)に置換するとき

動機

LaTeX なんかを書いていると, 改行を表すために \\ などと書くことがある. LaTeX ならこのままでいいんだけど, たとえば Markdown で MathJax を使っているときには \\\ と書かないと反応しないことがある. そこで次の処理を考える.

String#gsub メソッドを使って, \\\\\ に置換しよう. \\ でバックスラッシュ 1 文字のはずだから ‘\\\\\\’ と 6 本書けば良いはず. ところが……

s = "\\\\"
puts s # => \\
puts s.sub(/\\\\/, '\\\\\\') # => \\ <= Why ?????

という現象が発生. よくわからなかったのでいたずらに \\ を増やしてみると

puts s.sub(/\\\\/, '\\\\\\\\') # => \\ (8 本書いたがまだ2本)
puts s.sub(/\\\\/, '\\\\\\\\\\') # => \\\ (10 本書いてやっと3本)

ということに. どういう挙動なのかよくわからない.

結論#

結論だけ先に書く. 困ったので調べてみると, 次のようにやるとこちらの意図通りに置換されることがわかった:

puts s.sub(/\\\\/){'\\\\\\'} #=> \\\

もう結論は書いたので, 以下の文章は理由が知りたい人向けです.

上のような現象の理由

ググってみると, こちら に情報が. 1999 年のメールのようですが, 一番最後の部分を引用します.

Q5-7)バックスラッシュをエスケープするにはどうしますか

Regexp.quote('\\')で、エスケープされます。gsubを使う場合には、 gsub(/\\/, ‘\\\')では、置換文字列が構文解析で一度\\に変換され、 実際に置き換えるときにもう一度\と解釈されるので、gsub(/\\/,'\\\\\') とする必要があります。\&がマッチ文字列をあらわすことを使えば、 gsub(/\\/,'\&\&')と書けます。gsub(/\\/){'\\\\'}なら、エスケープが 1回しか解釈されませんので、求める結果が得られます。

どうも sub(/regexp/, 'string') とするときに, string は 2 度エスケープされる模様. つまり,

s = "\\\\"
s.sub(/\\\\/, '\\\\\\')

としたときには, \\\\\\

\\ \\ \\
\ \ \ (1 回目のエスケープ)
\\\ (3 本のバックスラッシュ)
\\ \ 
\ \(2 回目のエスケープ. 1 本のバックスラッシュは何もエスケープしていないのでただ残る.)
\\ (2 本のバックスラッシュ)

という風に解析される. だから, 2本のバックスラッシュが残る.

だから, 目的の \\\\\ に置換するためには

s = "\\"
s. sub(/\\\\/, '\\\\\\\\\\')

とした上で,

\\\\\\\\\
\\ \\ \\ \\ \\
\ \ \ \ \ (1 回目のエスケープ)
\\ \\ \
\ \ \ (2 回目のエスケープ)
\\\

と処理される. こんなの考えるの鬱陶しいので, おとなしく (g)sub(/regexp/){'\\\\\\'} などと書いたほうがよさそうではある.

実行環境#

一応.

% ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]

参考にしたリンク