Ruby и special/predefined variables
15 Jan 2014Не так давно я обнаружил интересный пример в одной замечательной книге. С этого примера, я бы и хотел начать наш разговор.
Выглядел примерно так:
"test string in irb".match /irb/
puts $&
#=> "irb"
Так как до этого я не часто встречался с подобными “глобальными” переменными, пример заинтересовал меня и захотелось выяснить, что же это за переменные. Первым делом, я решил узнать, как они называются и где их можно найти. Спустя несколько минут, стало ясно, что это так называемые “special variables”. Не долго думая и открыв google, просмотрев пару тройку результатов, стало ясно, что кроме списка этих переменных ничего особо нет. Это не сильно меня обрадовало и стало понятно, что пришло время открыть репозитарий ruby и начать искать в нем. Так же мне очень помогла одна небезизвестная книга. Как оказалось, ruby создает несколько специальных переменных, в зависимости от откружения, в котором запускаются программы, или в зависимости от действий, которые были выполены ранее. Кстати, это не совсем глобальные перменные, в чем легко можно убедиться, рассмотрев простой пример:
def test
"test string in irb".match /test/
puts "$& in test method #{$&}"
end
"test string in irb".match /irb/
puts "$& in main #{$&}"
#=> "irb"
test
#=> "test"
Как видно из примера, в каждом scope (main и метода), “глобальная” переменная отличается. Любой адекватный человек спросит: как такое, тысяча чертей, возможно? На самом деле все довольно просто, но, для полного понимания, начать придется с основ. Как многие знают, начиная с верисии 1.9 в ruby была добавлена виртуальная машина или YARV или же yet another ruby virtual machine, называйте как хотите, суть одна и та же. Смысл в том, что каждый раз, при запуске, YARV так же создает особый стек, для локальных переменных. В этом стеке указываются абсолютно все локальные переменные, свои для каждого scope. Разделение scope-ов происходит с помощью специальной точки или указателя - environment point (далее EP). Так же, в стеке, перед каждой EP, создается специальная переменная svar, которая как раз и указывает на таблицу специальных символов. Именно из-за этого для каждого scope могут быть свои значения специальных символов, что мы видели в примере выше. Но самое интересное, что у обычного блока и у места, где он будет вызван, scope одинаковый, в чем можно легко убедиться благодаря такому примеру:
"test string in irb".match /irb/
1.times do
"test string in irb".match /test/
puts "$& in block #{$&}"
end
puts "$& in main #{$&}"
> ruby test.rb
>> "$& in block test"
>> "$& in block test"
>> "$& in main test"
На самом деле это логичное поведение, ибо замыкания никто не отменял. Как я уже говорил, таких переменных много, но расскажу я о самых интересных(естественно для себя):
$&
Переменная, с которой начался наш рассказ. Хранит, как вы уже догадались, результат последнего совпадения регулярного выражения.
$1 $2 $3 …
Думаю, многим знакомая похожая переменная из регулярных выражений. Хотя, кого я обманываю? Это та же самая перменная, которая хранит совпадения из скобок:
"test string in irb".match /(irb)/
puts $1
#=> "irb"
$~
Содержит объект класса MatchData, соответствующий последнему совпадению.
"test string in irb".match /(irb)/
puts $~
#=> #<MatchData "irb" 1:"irb">
puts $~.to_s
#=> "irb"
puts $~.to_a
#=> ["irb", 'irb']
$+
Содержит значение последней круглой скобки из последнего совпадения:
"test string in irb".match /irb/
puts $~
#=> #<MatchData "irb" 1:"irb">
puts $+
#=> nil
"test in irb".match /(test) (in) (irb)/
puts $~
#=> #<MatchData "test in irb" 1:"test" 2:"in" 3:"irb">
puts $+
#=> "irb"
$`
Содержит все то, что не совпало в последнем регулярном выражении:
"test string in irb".match /irb/
puts $`
#=> "test string in"
$!
Содержит последнее вызванное исключение:
1 / 0 rescue $!
#=> #<ZeroDivisionError: divided by 0>
$@
Ну а эта переменная содержит массив со всеми trace stack-ами из последнего исключения:
1 / 0 rescue $@
#=> ["<main>:4:in `/'", "<main>:4:in `/'", "(irb):98:in `irb_binding'", ... ]
$*
Эта переменная равносильна переменной ARGV, думаю этим все сказанно.
$$
Переменная возвращает номер процесса, под которым выполняется скрипт.
$$
#=> 33630
puts `ps aux | grep irb`
#=> anton 33630 0.0 0.3 2470520 24084 s008 S+ 2Jan14 0:01.24 irb
.
Так где же определены эти переменные в исходном коде ruby? Как оказалось, все не так сложно, как кажется. Определенны эти переменные в файле parse.y примерно на 7950-той строке (да да, файл не очень большой, всего 11.5к строк кода). Для тех, кто не в курсе, parse.y - грамматический файл интерпритатора, благодаря которому происходит разбиение написанного вами кода на токены (лексемы/указатели), которые в последующем преобразуются в AST структуру, а затем в YARV структуру, ну а дальше в машинный код, который в последующем и будет выполняется. Как не трудно заметить, case функция ищет совпадение символа “$” и специальных символов (блок case), после чего передает их функции set_yylval_name:
7965: case '~': /* $~: match-data */
7966: case '*': /* $*: argv */
7967: case '$': /* $$: pid */
7968: case '?': /* $?: last status */
7969: case '!': /* $!: error string */
7970: case '@': /* $@: error position */
7971: case '/': /* $/: input record separator */
7972: case '\\': /* $\: output record separator */
7973: case ';': /* $;: field separator */
7974: case ',': /* $,: output field separator */
7975: case '.': /* $.: last read line number */
7976: case '=': /* $=: ignorecase */
7977: case ':': /* $:: load path */
7978: case '<': /* $<: reading filename */
7979: case '>': /* $>: default output handle */
7980: case '\"': /* $": already loaded files */
7981: tokadd('$');
7982: tokadd(c);
7983: goto gvar;
-------
7997: gvar:
7998: set_yylval_name(rb_intern3(tok(), tokidx, current_enc));
7999: return tGVAR
И в завершение, следует упомянуть особый файл - English.rb, в котором прописаны алиасы для специальных переменных, благодаря чему можно использовать данные переменные намного понятнее, нежели чем использование $$, $& и так далее:
"waterbuffalo" =~ /buff/
print $", $', $$, "\n"
# With English:
require "English"
"waterbuffalo" =~ /buff/
print $LOADED_FEATURES, $POSTMATCH, $PID, "\n"