Странная замена обратной косой черты в Ruby - PullRequest
25 голосов
/ 09 октября 2009

Я не понимаю этот код Ruby:

>> puts '\\ <- single backslash'
# \ <- single backslash

>> puts '\\ <- 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa')
# aa <- 2x a, because two backslashes get replaced

пока все как положено. но если мы ищем 1 с /\\/ и заменяем на 2, закодированный '\\\\', почему мы получаем это:

>> puts '\\ <- only 1 ... replace 1 with 2'.sub(/\\/, '\\\\')
# \ <- only 1 backslash, even though we replace 1 with 2

и затем, когда мы кодируем 3 с помощью '\\\\\\', мы получаем только 2:

>> puts '\\ <- only 2 ... 1 with 3'.sub(/\\/, '\\\\\\')
# \\ <- 2 backslashes, even though we replace 1 with 3

Кто-нибудь может понять, почему обратный слеш проглатывается в замещающей строке? это происходит на 1.8 и 1.9.

Ответы [ 5 ]

62 голосов
/ 11 ноября 2010

Быстрый ответ

Если вы хотите обойти эту путаницу, используйте намного менее запутанный синтаксис блока . Вот пример, который заменяет каждый обратный слеш двумя обратными слешами:

"some\\path".gsub('\\') { '\\\\' }

Ужасные детали

Проблема в том, что при использовании subgsub) без блока ruby ​​интерпретирует специальные последовательности символов в параметре замены. К сожалению, sub использует обратную косую черту в качестве escape-символа для них:

\& (the entire regex)
\+ (the last group)
\` (pre-match string)
\' (post-match string)
\0 (same as \&)
\1 (first captured group)
\2 (second captured group)
\\ (a backslash)

Как и любой побег, это создает очевидную проблему. Если вы хотите включить литеральное значение одной из вышеуказанных последовательностей (например, \1) в выходную строку, вы должны экранировать ее. Итак, чтобы получить Hello \1, вам нужна строка замены, равная Hello \\1. И чтобы представить это как строковый литерал в Ruby, вы должны снова избежать обратной косой черты следующим образом: "Hello \\\\1"

Итак, есть двух разных проходов . Первый берет строковый литерал и создает внутреннее строковое значение. Вторая принимает это внутреннее строковое значение и заменяет приведенные выше последовательности на соответствующие данные.

Если за обратной косой чертой не следует символ, соответствующий одной из указанных выше последовательностей, то обратный слеш (и следующий за ним символ) будет проходить без изменений. Это также влияет на обратную косую черту в конце строки - она ​​будет проходить без изменений. Легче всего увидеть эту логику в коде Рубиниуса; просто найдите метод to_sub_replacement в классе String .

Вот некоторые примеры того, как String#sub выполняет синтаксический анализ строки замены:

  • 1 обратная косая черта \ (со строковым литералом "\\")

    Проходит без изменений, потому что обратная косая черта находится в конце строки и после нее нет символов.

    Результат: \

  • 2 обратной косой черты \\ (со строковым литералом "\\\\")

    Пара обратных слешей соответствует последовательности экранированного обратного слэша (см. \\ выше) и преобразуется в один обратный слеш.

    Результат: \

  • 3 обратной косой черты \\\ (которые имеют строковый литерал "\\\\\\")

    Первые две обратной косой черты соответствуют последовательности \\ и преобразуются в одну обратную косую черту. Затем последний обратный слеш находится в конце строки, поэтому он проходит через неизмененный.

    Результат: \\

  • 4 обратной косой черты \\\\ (которые имеют строковый литерал "\\\\\\\\")

    Каждая пара обратной косой черты соответствует последовательности \\ и преобразуется в одну обратную косую черту.

    Результат: \\

  • 2 обратной косой черты с символом посередине \a\ (со строковым литералом "\\a\\")

    \a не соответствует ни одной из escape-последовательностей, поэтому ему разрешено проходить без изменений. Задний слеш также допускается.

    Результат: \a\

    Примечание: Тот же результат можно получить из: \\a\\ (с литеральной строкой: "\\\\a\\\\")

Оглядываясь назад, это могло бы быть менее запутанным, если бы String#sub использовал другой escape-символ. Тогда не было бы необходимости дважды убегать от обратной косой черты.

18 голосов
/ 09 октября 2009

Это проблема, потому что обратная косая черта (\) служит escape-символом для регулярных выражений и строк. Вы можете использовать специальную переменную \ &, чтобы уменьшить количество обратных слешей в строке замены gsub.

foo.gsub(/\\/,'\&\&\&') #for some string foo replace each \ with \\\

РЕДАКТИРОВАТЬ: я должен упомянуть, что значение \ & из соответствия регулярному выражению, в данном случае один обратный слеш.

Кроме того, я подумал, что существует особый способ создания строки, которая отключает escape-символ, но, очевидно, нет. Ни один из них не произведет две косые черты:

puts "\\"
puts '\\'
puts %q{\\}
puts %Q{\\}
puts """\\"""
puts '''\\'''
puts <<EOF
\\
EOF  
4 голосов
/ 09 октября 2009

argh, сразу после того, как я все это напечатал, я понял, что \ используется для ссылки на группы в строке замены. Я предполагаю, что это означает, что вам нужен литерал \\ в строке замены, чтобы получить один замененный \. Чтобы получить литерал \\, вам нужно четыре \ s, поэтому для замены одного на два вам действительно нужно восемь (!).

# Double every occurrence of \. There's eight backslashes on the right there!
>> puts '\\'.sub(/\\/, '\\\\\\\\')

что-то я пропустил? Есть ли более эффективные способы?

3 голосов
/ 09 октября 2009

Устранение небольшой путаницы во второй строке кода автора.

Вы сказали:

>> puts '\\ <- 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa')
# aa <- 2x a, because two backslashes get replaced

2 обратных слеша здесь не заменяются. Вы заменяете 1 экранированную обратную косую черту двумя символами (aa). То есть, если бы вы использовали .sub(/\\/, 'a'), вы бы увидели только один 'a'

'\\'.sub(/\\/, 'anything') #=> anything
2 голосов
/ 09 октября 2009

В книге кирки упоминается именно эта проблема. вот еще один вариант (со страницы 130 последнего издания)

str = 'a\b\c'               # => "a\b\c"
str.gsub(/\\/) { '\\\\' }   # => "a\\b\\c"
...