Статьи

Code Safari: Подчеркните Безумие

На прошлой неделе я наткнулся на отличную презентацию конференции Scotland Ruby: «Литературная критика для бездействующего программиста» Роланда Свинглера . Он познакомил меня с сумасшедшим маленьким скриптом в рубине, который позволяет писать программы полностью в подчеркиваниях!

# hello.rb require "_" ____ _ _____ ____ __ ____ ____ __ ___ ____ __ __ _ ______ _____ ___ _ _ ___ _____ ______ ____ _ _ ____ _ _ ____ _ ____ __ __ ___ _ ______ ___ ____ __ ______ ____ _ ____ ____ __ _ ____ _ _ ___ _____ _____ _ ______ ____ _ ______ _____ 

Это приложение Hello World, прямо здесь. Я тоже почти не верил. Роланд кратко объясняет, как этот код работает в своем выступлении, но, учитывая наши предыдущие приключения с запутыванием , я хотел бы погрузиться немного глубже.

С высоты птичьего полета

Мы собираемся поработать над копией библиотеки из git, чтобы мы могли легко поиграть с ее изменением. Клонируйте его и попробуйте запустить приведенный выше скрипт:

 > cd /tmp > git clone https://github.com/mame/_ > ruby -I _/lib hello.rb Hello, world! 

Привет, мир действительно! Параметр -I для Ruby добавляет новый каталог к ​​пути загрузки, что позволяет строке require "_" успешно найти _.rb внутри репозитория git. Теперь, когда наш тестовый жгут работает, давайте откроем код, чтобы увидеть, в чем заключается безумие.

 # _/lib/_.rb def __script__(src) code = [] src = src.unpack("C*").map {|c| c.ord.to_s(6).rjust(3, "0").chars.to_a } src.flatten(1).map {|n| n.to_i(6) + 1 }.each do |n| code.empty? || code.last.size + n + 1 >= 60 ? code << "" : code.last << " " code.last << "_" * n end ([%q(require "_")] + code).join(" 

«)
конец

 $code, $fragment = [], [] def method_missing(mhd, *x) if x.empty? $code.concat($fragment.reverse) $fragment.clear end $fragment << (mhd.to_s.size - 1).to_s end at_exit do $code.concat($fragment.reverse) eval($code.join.scan(/.../).map {|c| c.to_i(6) }.pack("C*")) end 

Игнорируя функцию __script__ , которая является помощником для создания исходных файлов подчеркивания, есть только 12 строк, которые выполняют настоящую работу. Чтобы понять их, нам необходимо ознакомиться с несколькими интересными концепциями Ruby.

Первым является method_missing , встроенный метод, который вызывается для объекта, когда метод в противном случае не соответствует ничему в классе.
В случае подчеркивания это определение method_missing на верхнем уровне, который в Ruby по-прежнему выполняется в контексте объекта:

 > ruby -e 'puts self.class' Object 

Чтобы проиллюстрировать это, мы можем написать скрипт, который возвращает любой метод, который мы вызываем:

 # echo.rb def method_missing(method_name) puts method_name.to_s end hello world 

Вторая концепция, которую нужно понять, это именование методов в Ruby. Традиционно вы определяете методы с символами и подчеркиванием, но это не является встроенным ограничением Ruby. Как показывает подчеркивание, имя метода, состоящее только из подчеркивания, является допустимым, и вы можете пойти еще дальше, определив методы с пробелами в них! Конечно, парсер не сможет их вызвать, но мы можем обойти это:

 class Greeter define_method("say hello") do puts "hello" end end Greeter.new.send("say hello") 

Последняя важная концепция — at_exit . Из рубиновой документации

Преобразует блок в объект Proc (и, следовательно, связывает его в точке вызова) и регистрирует его для выполнения при выходе из программы. Если зарегистрировано несколько обработчиков, они выполняются в обратном порядке регистрации.

Это то, что тестовые среды, такие как Test::Unit и RSpec обычно используют для автоматического запуска тестов после выполнения файла.

Соединяя это вместе

Теперь мы можем понять общую структуру подчеркивания: использовать метод, отсутствующий, для обработки вызовов методов с любым количеством знаков подчеркивания, накапливать коды в переменных $code и $fragment , а затем выполнять эти коды при выходе из программы. Давайте разберемся с кодами, которые он накапливает, добавив оператор put перед оператором eval . Мы можем сделать это легко, так как мы клонировали код из git и добавили его в наш путь загрузки; такой вид взлома библиотек сложнее (хотя все еще возможно!) при работе с гемами.

 at_exit do $code.concat($fragment.reverse) puts $code.inspect eval($code.join.scan(/.../).map {|c| c.to_i(6) }.pack("C*")) end 

Это показывает список чисел, сгенерированных mhd.to_s.size - 1 в method_missing

 > ruby -I _/lib hello.rb ["3", "0", "4", "3", "1", "3" # and so on... 

Как мы получаем отсюда код? В процессе конвертации есть несколько шагов, которые мы рассмотрим по очереди. irb позволяет нам быстро и легко пройти процесс и визуализировать преобразования:

 irb> input = ["3", "0", "4", "3", "1", "3"] => ["3", "0", "4", "3", "1", "3"] irb> input.join => "304313" irb> input = ["3", "0", "4", "3", "1", "3"] => ["3", "0", "4", "3", "1", "3"] irb> input = input.join => "304313" irb> input = input.scan(/.../) => ["304", "313"] irb> input = input.map {|c| c.to_i(6) } => [112, 117] irb> input.pack("C*") => "pu" # first two characters of our source code 

Там есть несколько хитрых звонков. scan может быть немного запутанным, если вы не видели его раньше, тем более что это регулярное выражение не выглядит знакомым. Еще раз, ссылаясь на документацию :

итерация по str , соответствующая шаблону (который может быть регулярным выражением или строкой). Для каждого совпадения генерируется результат, который либо добавляется в массив результатов, либо передается в блок.

У нас нет блока, поэтому он просто добавляет к результату. /.../ выражение /.../ соответствует трем символам, поэтому оно разделяет входные данные на группы по три.

to_i интересен: что это за 6 параметр?

Возвращает результат интерпретации начальных символов в str как целочисленную базовую базу (от 2 до 36).

Так что это интерпретирует число как основание 6, но зачем это нужно? Посмотрим, что произойдет, если мы изменим его обратно на базу 10 (обратите внимание на добавленную base параметров в сигнатуре метода).

 # _/lib/_.rb def __script__(src, base = 6) code = [] src = src.unpack("C*").map {|c| c.ord.to_s(base).rjust(3, "0").chars.to_a } puts src.inspect src.flatten(1).map {|n| n.to_i(base) + 1 }.each do |n| code.empty? || code.last.size + n + 1 >= 60 ? code << "" : code.last << " " code.last << "_" * n end ([%q(require "_")] + code).join(" 

«)
конец

 > ruby -r _/lib/_ -e 'puts __script__("puts "Hello, world!"", 10)' require "_" __ __ ___ __ __ ________ __ __ _______ __ __ ______ _ ____ ___ _ ____ _____ _ ________ ___ __ _ __ __ _ _________ __ _ _________ __ __ __ _ _____ _____ _ ____ ___ __ __ __________ __ __ __ __ __ _____ __ _ _________ __ _ _ _ ____ ____ _ ____ _____ 

Декодирование работает нормально, так что, похоже, только из эстетических соображений была выбрана база 6.

Последним этапом процесса является таинственный метод pack с его непостижимым аргументом 'C*' . Это метод, который переводит массив в двоичную последовательность на основе однобуквенных кодов, которые сопоставляются с типом данных, таким как различные типы целых чисел и строк. C отображается на «8-битное целое число без знака (без знака)), а * означает преобразование всех оставшихся элементов, используя это отображение. Поскольку все наши числа являются кодами ASCII для кода, это приводит к преобразованию нашего массива в допустимый исходный код, который можно передать в eval . Это сложно понять словами — лучше всего поиграть с ним в irb .

Мы сделали это до конца, но мы перепрыгнули через еще одну важную ветку в method_missing . Вот это снова для справки:

 $code, $fragment = [], [] def method_missing(mhd, *x) if x.empty? $code.concat($fragment.reverse) $fragment.clear end $fragment << (mhd.to_s.size - 1).to_s end 

Что такое x.empty? проверять делать? Это связано с порядком, в котором методы вызываются в ruby. Объединение нескольких методов без явных круглых скобок, как правило, является плохой формой, но поддерживается Ruby. Он оценивает вызовы справа налево, так что следующие две строки эквивалентны:

 method1 method2 method3 method1(method2(method3)) 

Однако это означает, что method3 выполняется первым, что дает нам обратный порядок. Именно здесь пригодится вызов $fragment.reverse frag.reverse, так что подчеркнутая версия может быть в том же порядке, что и код, который она представляет. The x.empty? это просто способ определения конца строки — подчеркивание не использует аргументы ни для чего другого, а method3 — единственный из перечисленных выше методов, у которого его нет.

Еще одна вещь, которую вы, возможно, заметили, это то, что здесь нет фактической проверки подчеркивания. Похоже, что мы могли бы использовать любые символы, которые мы выбрали, чтобы написать нашу запутанную программу! Это возбуждает во мне ребенка, который писал скрытые буквы лимонными чернилами.

 require '_' look a poem! a monkey ark a baboon yacht too _ a big aquian din! a animal party 

Но что такое скрытое сообщение? Это для вас, чтобы обнаружить! Вот еще несколько вещей, которые вы можете попробовать:

  • Кодировка Base 6 была короче, чем Base 10, но самая короткая? Напишите программу для расчета базы, которая дает кратчайший выход для данной программы.
  • Подчеркивание было написано на 1.8.7, на 1.9.2 есть ошибка с вводом «put 1». Вы можете найти и исправить это?
  • Больше стихов!

Поделитесь, как вы идете в комментариях. Присоединяйтесь ко мне на следующей неделе для новых приключений в джунглях кода.