Подводные камни String#sub

Передо мной стояла банальная задача зачитать содержимое файла и произвести подстановку в определенной строке, взяв значение из переменной окружения (Environment variable). Казалось бы, ничего сложного тут нет.

Предположим что необходимая строка содержится в переменной str:

s = "Use ruby. Be happy!"

и для того, что бы заменить слово ruby на Ruby необходимо воспользоваться методом sub:

s = "Use ruby. Be happy!"
puts s.sub( 'ruby', 'Ruby' ) #=> Use Ruby. Be happy!

Ничего сложного тут нет, все отработало как мы и ожидали. А теперь вспомним об одной полезной особенности String#sub -- в качестве первого аргумента может быть использовано регулярное выражение, например:

puts "0001".sub( /0+/, '0' ) #=> 01

Это типичное использование данного метода. Однако иногда необходим произвести замену воспользовавшись значением являющимся частью совпадающей строки, например у нас есть строка "I love @Ruby@" и мы хотим преобразовать её в "I love Ruby" (выкинуть символ @), при этом слово Ruby может быть написано как с заглавной, так и с прописной буквы: Ruby или ruby, и нам важно сохранить тот регистр, который был указан в исходной строке, для этого воспользуемся таким кодом:

puts "I love @Ruby@".sub( /@([Rr]uby)@/, '\1' ) #=> I love Ruby

Возможно, для кого то понадобится объяснение этого кода. Так вот регулярное выражение /@([Rr]uby)@/ читается так: находим подстроку начинающуюся с символа @, дальше может идти либо символ R, либо r (Ruby и ruby это взаимно заменяемые названия нашего любимого языка программирования, хотя Ruby более корректно), затем должна следовать последовательность из символов uby, и в конце опять символ @. Круглые скобки говорят о том что подстрока между символами @ должна быть сохранена для дальнейшего использования.

Зачем нам этот трюк с круглыми скобками и \1, ведь мы могли бы просто написать:

puts "I love @Ruby@".sub( /@[Rr]uby@/, '?uby' )

Знак вопроса я написал не случайно, в условии сказано что r может быть как заглавной, так и прописной, но в таком варианте мы не в состоянии определить в каком регистре было написано название в изначальном варианте строки, до замены.

Я уже сказал что круглыми скобками мы воспользовались для сохранения группы символов между @, теперь рассмотрим второй аргумент, передуваемый в метод sub -- '\1'. \1 это спец последовательность символов которая распознается методом sub. Во время своей работы метод sub, в случае удачного совпадения, производит замену совпавшей строки на значение, переданное вторым аргументом, но перед тем как сделать замену, этот аргумент особым образом трансформируется. Если будет встречена последовательность символов \[1-9], то эти последовательности будут заменены на подстроки из совпавшей строки -- в нашем случае подстрока одна ([Rr]uby), но их количество может достигать 9, при этом если подстрока пуста или не задана, то подставляется пустое значение. То есть если наш пример модифицировать следующим образом:

puts "I love @Ruby@".sub( /@([Rr]uby)@/, '\1\2\3' ) #=> I love Ruby

То на результате это ни как не отразится.

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

Заменять необходимо было определенную последовательность символов %PLACE_HOLDER%, значение необходимо было брать из переменной окружения CUSTOMER_ID.

Изначальный вариант решения, который пришел мне в голову, был таков:

str.sub!( '%PLACE_HOLDER%', ENV['CUSTOMER_ID'].to_s )

И это решение работало, до того момента как в CUSTOMER_ID не было сохранено значение 'CUST_ID\12', в этом случае %PLACE_HOLDER% раскрылось в совсем неожиданное для меня значение -- CUST_ID2. Объясняется это тем что метод sub проанализировал значение переменной и в качестве \1 попробовал подставить значение сохраненной подстроки из первого аргумента (%PLACE_HOLDER%), и оно естественно оказалось пустым, т.к. первый аргумент не был даже регулярным выражением!

Этот факт меня очень расстроил, ведь '%PLACE_HOLDER%' даже не регулярное выражение, зачем пытаться обрабатывать последовательность \[1-9]. Эх блин багописцы, подумал я и принялся придумывать как обойти проблему, в результате код был переписан в следующий:

str.sub!( '%PLACE_HOLDER%' ) {
 ENV['CUSTOMER_ID'] ).to_s
}

В данном случае магическая последовательность \[1-9] теряет свое воздействие на String#sub и подставляется как есть!

Понятно что все выше сказанное относится не только к sub, но и к gsub и даже к их деструктивным версиям.

На этом спешу откланится, удачи!

Обсуждение

Re: Подводные камни String#sub

К стати, "on the fly":

irb(main):010:0> s = "Use Python. Be Happy"
=> "Use Python. Be Happy"
irb(main):011:0> s[/[Pp]ython/] = 'Ruby'
=> "Ruby"
irb(main):012:0> s
=> "Use Ruby. Be Happy"

--
:r ~/.signature
:wq!

Re: Подводные камни String#sub

К чему там gsub?

Re: Подводные камни String#sub

В данной ситуации поведение вашего проблемного примера вполне естественно, ибо оно вяжется со всеми принципами работы со строками и никаким подводным камнем использования метода String#sub не является. Самым логически правильным способом обойти такое поведение является следующий код:

str.sub!('%PLACE_HOLDER%', Regexp.escape(ENV['CUSTOMER_ID'].to_s))

Вход для пользователей