👋

[Bug #21651] String#gsub で を置き換えるときに意図しない挙動になるバグ報告

に公開

[Bug #21651] replacing a string with one backslash with two backslashes

  • 次のように String#gsub で置き換える文字を "\\ \\" であるときは意図する挙動になるが "\\\\" では置き換えられない(意図する挙動にならない)というバグ報告
# "\\\\" は置き換えられない
pp "\\".gsub("\\", "\\\\")  # => "\\"

# "\\ \\" だと置き換えられる
pp "\\".gsub("\\", "\\ \\") # => "\\ \\"
  • これ、コメントでは意図する挙動であることが説明されているんですがいまいち前者がなぜ "\\" になるのかよくわからないので整理してみる
  • まず前提として Ruby の文字列リテラルでは \ + 英字 で特殊文字として文字列が定義される
"\t" # => タブ文字
"\n" # => 改行文字
"\s" # => 空白文字
  • また \ の後ろに無効な文字がある場合は \ が省かれる
# \ が除かれた文字列として定義される
puts "\z\あ"  # => zあ
  • 逆に \ の後ろに文字がない場合はシンタックスエラーになる
"\"
# => error: unterminated string meets end of file
  • また "\\\\" も実際には \\ という文字になる
puts "\\\\" # => \\
  • これを踏まえた上で "\\""\\" を置換する場合は以下のような結果になる
# 単純に \ の値を \ で置き換える
puts "\\".gsub("\\", "abc")  # => abc
  • 同様に "\\\\" を置き換える文字に指定した場合には \\ になることを期待するがそうはならずに \ に置き換わる
puts "abc".gsub("abc", "\\\\")
# 実際の挙動   => \
# 期待する挙動 => \\
  • ここがなんで \ になるのかがあんまりわかってない
  • チケット内だと以下のドキュメントが引用されているんですがそういうものなんですかね…?

Note that \ is interpreted as an escape, i.e., a single backslash.

Note also that a string literal consumes backslashes.
See String Literals for details about string literals.

  • うーん #gsub の第二引数に文字列を渡したときに単に置換されるのではなくて特別な動きする形になるのかな
  • 例えば "\1" で正規表現でキャプチャした値で置き換える、みたいなことができる
# (b+) にマッチした値が置換後の文字列になる
p 'xbbb-xbbb'.gsub(/x(b+)/, "\\1")
# => "bbb-bbb"
  • ここの \\ が特殊な動きをするエスケープになっていて \\\\ が実質 \ として置換される、その結果意図しないような挙動に見える、って感じなのかな
  • 逆に "\\ \\" の場合は \\ の後に記号がないので \ として扱われる、みたいな挙動になるみたい
puts "abc".gsub("abc", "\\ \\") # => \ \
puts "\\".gsub("\\", "\\\\\\\\")  # \\ になるように \\\\ + \\\\ の文字列に置き換える
puts "\\".gsub("\\", "\\&\\&")    # マッチした文字を2倍にする
puts "\\".gsub("\\") { "\\\\" }   # 置き換える文字をブロックの戻り値にする
  • このあたりあんまり意識してはいなったんですが今までハマったことがあるような気もする
GitHubで編集を提案

Discussion