Ruby и special/predefined variables

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

Выглядел примерно так:

"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"