Передо мной стояла банальная задача зачитать содержимое файла и произвести подстановку в определенной строке, взяв значение из переменной окружения (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 и даже к их деструктивным версиям.
На этом спешу откланится, удачи!