Под катом маленький пример того, за что мне очень нравится язык Ruby.
Потребовалось создать пару десятков файлов, каждый из которых должен был бы выглядеть приблизительно так:
require 'mxx_ru/binary_unittest' MxxRu::setup_target( MxxRu::BinaryUnittestTarget.new( "test/so_5/samples_as_unit_tests/coop_listener.ut.rb", "sample/so_5/coop_listener/prj.rb" ) ) |
Причем был очень важный фактор: имя если внутри файла использовалось, скажем, имя coop_listener, то и сам файл должен был называться coop_listener.ut.rb. Если файл называется exception_reaction.ut.rb, то это имя должно быть дважды указано у него внутри:
MxxRu::setup_target( MxxRu::BinaryUnittestTarget.new( "test/so_5/samples_as_unit_tests/exception_reaction.ut.rb", "sample/so_5/exception_reaction/prj.rb" ) ) |
Понятное дело, что повторять имя файла дважды внутри самого файла не интересно, поэтому появился приблизительно такой вариант:
require 'mxx_ru/binary_unittest' sample_name = 'exception_reaction' MxxRu::setup_target( MxxRu::BinaryUnittestTarget.new( "test/so_5/samples_as_unit_tests/#{sample_name}.ut.rb", "sample/so_5/#{sample_name}/prj.rb" ) ) |
Уже лучше, но все равно имя дублировалось -- сначала нужно было должным образом назвать .ut.rb-файл, а затем повторить это же имя внутри .ut.rb-файла. Поэтому был сделан следующий шаг, использующий возможность получить имя исходного файла:
require 'mxx_ru/binary_unittest' sample_name = File.basename(__FILE__, '.ut.rb') MxxRu::setup_target( MxxRu::BinaryUnittestTarget.new( "test/so_5/samples_as_unit_tests/#{sample_name}.ut.rb", "sample/so_5/#{sample_name}/prj.rb" ) ) |
Т.е. из полного имени текущего исходного файла (__FILE__) удаляется путь и остается только имя файла с удаленным расширением '.ut.rb' (это осуществляется методом File.basename). Полученый результат сохраняется в переменной sample_name, и затем через string-interpolation подставляется в соответствующие места.
Казалось бы, цель достигнута: можно копировать один и тот же файл под разными именами и не нужно ничего исправлять внутри него.
Но смущал объем копипасты, который был внутри каждого файла. А то, если со временем нужно было бы заменить строку 'test/so_5/samples_as_unit_tests' на какую-нибудь другую? Править все 20 файлов? Неприятная перспектива.
Поэтому была сделана следующая итерация:
require_relative 'details.rb' setup_sample_as_unit_test File.basename(__FILE__,'.ut.rb') |
Всего две строки. Уже совсем хорошо. Почти.
Все-таки мое чувство прекрасного смущало повторение операций с File.basename. Ведь, если подумать, можно и от них избавиться, если использовать тот факт, что в Ruby можно получить стек вызова, в котором будут указаны имена исходных файлов. Что позволило записать .ut.rb-файлы вот так:
require_relative 'details.rb' setup_sample_as_unit_test |
Вся магия кроется внутри файлика details.rb. Который чуть помудренее, чем показанный выше код:
require 'mxx_ru/binary_unittest' def setup_sample_as_unit_test(sample_name = nil) ut_name = File.basename( /^(.+):\d/.match( caller(1,1)[0] )[1] ) sample_name = File.basename( ut_name, '.ut.rb' ) unless sample_name MxxRu::setup_target( MxxRu::BinaryUnittestTarget.new( "test/so_5/samples_as_unit_tests/#{ut_name}", "sample/so_5/#{sample_name}/prj.rb" ) ) end |
Вся магия сосредоточена в первой строке функции setup_sample_as_unit_test :)
Сначала берется информация о том, кто именно и откуда вызвал setup_sample_as_unit_test (конструкция caller(1,1)[0] -- caller возвращает вектор и нам нужен его первый элемент).
Далее из полученной строки вида "test/so_5/samples_as_unit_tests/exception_reaction.ut.rb:3:in `main'" нужно вытащить только имя файла, без номера строки и дополнительной информации. Для этого используется регулярное выражение /^(.+):\d/. Оно вычленяет все, что находится до двоеточия, после которого есть хотя бы одна цифра. Вычленение происходит посредством метода Regex#match, который возвращает массив совпадений с образцом. Нам нужен второй элемент этого массива, т.к. именно в нем окажется имя файла. Тут, кстати, есть фокус с платформой Windows: имя файла может быть абсолютным с именем диска, т.е. что-то вроде "D:/home/eao197/sandboxes/bla-bla-bla". Поэтому искать просто до двоеточия нельзя, нужно именно до двоеточия, за которым идут цифры (т.е. номер строки).
Ну а дальше все просто: из полного имени файла отбрасывается путь. Получается имя файла, из которого вызвана setup_sample_as_unit_test. Потом из этого имени удаляется расширение и получается имя примера, которое сохраняется в sample_name.
Но с sample_name оказалась небольшая закавыка. Выяснилось, что есть пара примеров, которые лежат не в sample/so_5, а внутри дополнительного подкаталога -- sample/so_5/svc. Например, код exception_reaction лежит в sample/so_5/exception_reaction, а код svc_hello в sample/so_5/svc/hello. Как учесть этот факт?
Никак :) Такие случаи нужно указывать явно. Например, файл svc_hello.ut.rb имеет вид:
require_relative 'details.rb' setup_sample_as_unit_test 'svc/hello' |
Т.е. могут быть случаи, когда setup_as_unit_test вызывается с полностью определенным значением sample_name. И это значение не нужно вычислять внутри setup_as_unit_test. Что и записывается посредством унаследованного из Perl-а оператора unless. Оператор неоднозначный, но для записи однострочников очень удобный.
В показанном решении есть один стремный момент: вычленение имени файла, из которого вызывали setup_as_unit_test, базируется на знании технических деталей реализации (т.е. того, как имя файла представлено в результатах caller-а). Но код был проверен под Ruby 2.0, 2.1 и 2.2. Так что, надеюсь, в течении какого-то времени эти детали не поменяются.
Удобство Ruby проявилось еще и в том, что все распространяется в исходниках. Поэтому когда под Linux-ом и Ruby-2.2.1 выяснилось, что скрипт работает не так, как задумывалось (сказалась разница в поведении String#[] в разных версиях Ruby), то исправить ошибку не составило труда. Просто вносишь правки и смотришь, что получается. Тогда как в языках с предварительной компиляцией нужно делать дополнительные манипуляции. А уж если утилита распространяется в кросс-платформенном бинарном виде (как Java в jar-файлах), то может вообще оказаться, что исходники лежат где-то в другом месте и под рукой их нет вовсе.
Полагаю, что такой фокус возможен и в Perl-е, и в Python-е. Вряд ли Ruby здесь имеет какие-то уникальные преимущества. Другое дело, что из этой "большой тройки" мейнстримовых скриптовых языков лично мне Ruby наиболее симпатичен. Впрочем, это уже совсем другая история... :)
Комментариев нет:
Отправить комментарий