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]