Однострочник месяца на Perl: Приключение с неправильно названными файлами
Автор: Ben Okopnik
Перевод: Павел Соколов


ПРИМЕЧАНИЕ ПОВЕСТВОВАТЕЛЯ

Недавно я познакомился с подборкой документов, которые описывают приключения никого иного, как Вумерта Фунли - Прагматичного Компьютерного Детектива известного по всему миру, но очень скрытного человека. Насколько я знаю, информация в этих документах подлинная. Мой анонимный корреспондент (в котором, хотя я и поклялся не выяснять, кто это, я подозреваю Фринка Ублика, близкого друга и приятеля этого великого человека) прислал мне по электронной почте короткое письмо, вызвавшее мой неподдельный интерес, а затем - зашифрованный файл, на который я потратил несколько ночей, чтобы расшифровать. (Кажется он считает, что это показывает наличие у него чувства юмора.) Похоже это стало правилом: через некоторое время я получаю файл от отправителя, чьё имя подходит под сложное регулярное выражение (лист procmail для этого вырос уже на несколько страниц, и теперь, кажется, мутирует сам по себе). Затем я должен бросить всё, чтобы я ни делал, и начинать взламывать шифр на высокой скорости - так как метод шифрования, каким-то образом (как именно - я не разобрался) зависит от времени и становится совсем не взламываемым по прохождении нескольких часов.

В первых письмах я получил разрешение на публикацию этого материала. Мой корреспондент дал понять, что он хотел бы сохранить своё имя в тайне, и согласен, чтобы на него не было никаких ссылок в отношении проделанной работы. Я разместил статью здесь, хотя и не могу заявить об авторстве, в надежде пролить свет на работу этого великого детектива, чьи деяния до сих пор были покрыты завесой тайны.

Ben Okopnik
На борту парохода "Улисс", 10 октября 2002 г.


В файловой системе было темно и тихо; все отложенные записи на диск уже были синхронизированы, жёсткий диск замедлил вращение, а процессор перешёл в режим пониженного потребления энергии. Даже обычно неудержимый Фринк казался подавленным такими обстоятельствами и тихо перепроверял права и пароли удалённого доступа - необходимая предосторожность, прежде чем они смогут покинуть комфорт своей домашней директории "/home" в бронированном транспорте SSH.

Вумерт, однако, был спокоен и готов к действиям. Это было именно то время, когда он предпочитал действовать, - в сумеречной зоне между режимами потребления энергии; в этих условиях даже ужасающие Гейзенжуки [1] (хотя его текущее задание не включало ничего настолько опасного) стали бы сонными и их можно было бы ловить практически голыми руками.

Его клиентка, сильно расстроенная и рыдающая в кружевной платок, призналась, что её правила создания имён для файлов совершенно вышли из под контроля - дикие строки вторглись в её систему имён файлов, ранее полных смысла и интуитивно понятных даже для обычного пользователя. Ответственный за этот проступок работник, был серьёзно УРОНен [2], а полицейские детективы просто пожали плечами, не зная, что предпринять. Все другие варианты свелись к мрачным перспективам ручного переименовывания сотен, а то и тысяч файлов. Хотя файлы и содержали нужные имена в тэгах HTML "<title>" - объём работы, которую надо было сделать вручную, был ошеломляющим. Вумерт был её последней надеждой.


Тихо передвигаясь, Вумерт приблизился к инфо-узлу (inode), помеченному "/var/apache/htdocs". Его поиск потребовал немного побродить по дереву сверху вниз, но знакомство детектива с модулем File::Find [3] сделало эту работу короткой; всхлипывая, клиентка сказала, что имена файлов-негодяев подходят под выражение /^[A-Z][0-9]+\.html?$/ [4] - другими словами, они все начинаются с заглавной буквы, за которой следуют одна или несколько цифр, а заканчиваются они на расширение ".htm" или ".html". Используя эту подсказку, нам понадобилось всего несколько секунд, чтобы обнаружить логово мерзавцев.

Как только он вошёл, нам сразу стало очевидным постыдное состояние дел: подозрительно выглядящие имена файлов слонялись по всем углам, ведя себя вызывающе и запугивая прохожих; другие же, одетые только в прозрачные символьные ссылки (symlinks), высовывались из окон xterm и непристойно предлагали проходящие данные. Что-то должно было быть сделано, и сделано быстро - новые имена файлов, которые попадали в это место, почти сразу развращались под влиянием такого отрицательного примера.

- Тсс, Вумерт, - прошептал Фринк, - кажется дело плохо. Что ты собираешься делать? Тут же их тысячи!

- Не беспокойся, парень. - Вумерт хладнокровно подошёл к интерфейсу командной строки и сдвинул шляпу пониже, чтобы закрыться от слепящих вспышек интенсивного трафика HTTP, - Я только что скачал последнюю версию Perl. У них нет ни шанса. - и надев свои перчатки для печати, он быстро набрал команду. Всё уместилось в одну строку.


perl -wlne'END{print$n}eof&&$n++;/<title>([^<]+)/i&&$n--' *

Результаты были ошеломляющими: как только монитор показал большой "0", все негодяи неожиданно остановились, чем бы они ни занимались, и, развернувшись, уставились на них двоих. Очевидно, они почувствовали неожиданную опасность, исходящую от этих двоих незнакомцах в плащах; самый крупный из негодяев, неприятный тип с надписью "X6664934755666.htm", вытатуированной на его груди, немедленно направился к ним, доставая что-то из кармана. В его намерения определённо не входило вручить Вумерту и Фринку букет цветов или личный код DSA к его владениям.

- Быстро, Вумерт! - закричал Фринк, - Сделай что-нибудь! Кажется он хочет бросить Nimda или даже Code Red!

Вумерт взглянул на своего нервного приятеля.

- Я тебе сказал, парень, расслабься. Первое, у нас есть Perl... - с молниеносной быстротой он отстучал ещё одно виртуозное соло на консоли:


perl -wlne'/title>([^<]+)/i&&rename$ARGV,"$1.html"' *

- ...и второе, - дикая сцена вокруг померкла и преобразилась в ясные, чистые окрестности с аккуратно расположенными файлами, гордо носящими имена подобные
"История 227, Лекция 35: Истоки римской революции.html", - мы в Linux. Вирусы - практически не наша проблема.


Позже тем же вечером, после того, как они забрали свои честно заработанные деньги у благодарной клиентки, и отдыхали с прекрасным высокогорным чаем Ли Шан, который Вумерт привёз из недавнего соединения по telnet с Дальним Востоком, Фринк наконец осмелился задать вопросы, которые у него вертелись на кончике языка с того судьбоносного приключения.

- Вумерт, я видел, как ты выстрелил эти команды, но мне не удалось понять, что ты делаешь. Я увидел регулярное выражение и даже заметил неявный цикл, но что было всё остальное?

- Элементарно, мой дорогой Фринк. Если ты вспомнишь первую строку...


perl -wlne'END{print$n}eof&&$n++;/<title>([^<]+)/i&&$n--' *

...ты заметишь, что я вызвал Perl со следующими ключами:

-w Включить предупреждения
-l Включить обработку конца строки
-n Неявный непечатаемый цикл
-e Выполнить следующие команды

- Включив предупреждения, я сказал Perl проверить мой синтаксис, это следует делать каждый раз, как ты запускаешь скрипт. Затем я указал обработку конца строк, фактически добавляющую возврат каретки к каждой печатаемой строке. Затем я сказал ему, что надо провести цикл по всему содержимому каждого файла и выполнить строку в одинарных кавычках как скрипт.

- Как ты отметил, я действительно создал цикл. Что ты пропустил, однако, это то, что в действительности было два одновременных цикла: я указал список файлов с помощью регулярного выражения shell "*", и Perl прочитал их по одному за раз. Также важно отметить, что синтаксис регулярного выражения внутри кавычек, которые обрамляют скрипт, кажется похожим, но он совсем отличается от синтаксиса регулярного выражения вне кавычек - первое обрабатывается Perl с помощью его внутреннего обработчика регулярных выражений; последнее предоставлено shell, которая использует гораздо более простую систему, называемую "глоббинг" ("globbing").

- Замечательно! - Фринк был возбуждён как щенок на своей первой охоте. - А что ты сделал в самом скрипте?

- Во-первых, я захотел перепроверить, что моё регулярное выражение совпадает именно с тем, с чем, по моему мнению, должно. Простейший способ был посчитать количество файлов - я прибавлял 1 к "$n" каждый раз, когда функция "eof" (конец файла) возвращала "true", и вычесть количество совпадений. Если бы сумма оказалась больше 0, это показало бы, что где-то моё регулярное выражение не нашло совпадений. К счастью...

- Да, я помню - она напечатала ноль.

- Что означало, что всё правильно. Оператор "END{print$n}" был выполнен в конце выполнения программы - заметь, что это не зависит от его положения в программе, хотя большинство людей ставят его в конец. Я выиграл один символ, поставив его в начале - оператор, следующий за блоком, как в случае "eof&&$n++", не требует точки с запятой. В Perl-гольфе [5], каждый символ имеет значение!

- Теперь давай посмотрим на регулярное выражение, которое я использовал. Это сердце этого скрипта:

/         # Начало регулярного выражения
<title>    # Найти подобную строку символов
([^<]+)   # Захватить один или более символов, не совпадающих с "<", в $1
/i        # Конец регулярного выражения, используется модификатор "игнорировать регистр символов" 

- Символы "/" заключают в себе регулярное выражение, совпадения с которым мы пытаемся найти; это стандартный синтаксис Perl, который ты похоже узнал. Видишь "+"? Я воспользовался "жадностью" Perl в интерпретации регулярных выражений: "+" означает "один или более из предшествующих символов или групп", но с подразумеваемым "захватить как можно больше". Это выражение захватит всё до символа "<" (начало тэга HTML) или конца текущей строки. Поэтому, каждый раз, как наш образец совпадал со строкой, я обновлял "$n" используя логическое "и" вместе с оператором декремента.

- В целом, существует эквивалентный скрипт, который показывает всё вышеописанное в более читабельной форме:


#!/usr/bin/perl -w

while ( <> ){ # эквивалент ключа "-n"
    $n++ if eof;
    $n-- if /<title>([^<]+)/i;
}
print "$n\n" # "\n" раньше добавлялся ключом "-l"

- Конечно, этот скрипт может быть вызван командой "perl <scriptname> *", или просто "./scriptname *", если он был сделан выполняемым.

- И последнее замечание. Посмотри на "активный" оператор в скрипте, единственный, который создаёт изменения или производит выходные данные. Это просто "print". Фактически, вся строка была просто тестом - я хотел убедиться, что всё работает нормально, прежде чем делать реальные изменения на диске, что, по моему мнению, правильная политика. Я видел по ужасному виду той толпы, что двух попыток для фактического переименования у меня не будет; по крайней мере у одного из них был "rm -rf /", и он бы, не колеблясь, применил бы его.

- Господи, Вумерт! - шок Фринка был очевиден - Ты должен быть смелым как лев, чтобы противостоять подобному.

Известный детектив взглянул на поблёскивающий вставками из нержавейки кевларовый бок вызова "chroot", который он достал из кармана, и улыбнулся.

- Ну, у меня было несколько трюков в запасе. Может теперь перейдём к переименованию? Если ты помнишь то выражение ...


perl -wlne'/title>([^<]+)/i&&rename$ARGV,"$1.html"' *

- ...ты заметишь, что эта строка во многом похожа на первую; однако, есть несколько новых моментов. Во-первых, я всё ещё использовал ключ "-l" в опциях, но теперь причина была несколько иной: так как строки, захваченные в "$1", скорее всего будут содержать перевод каретки ("\n"), и нам потребовался способ, чтобы избавиться от него. Perl очень сообразителен в отношении удаления первых и последних пробелов в строке и обработки нестандартных символов при использовании "rename", но "filename\n.html" могло бы вызвать проблемы. К счастью, "-n" также "обрубает" строчки на входе, т.е. Perl удалит перевод каретки как только прочитает строчку[7].

- Затем, "$ARGV" - это переменная Perl, в которой содержится имя файла, который читается в данный момент. Так как "$1" содержит результат первой захваченной строки в регулярном выражении (содержимое первых скобок сохраняется в "$1", вторых - в "$2" и так далее), переименование стало простой задачей. Это также позволило нам установить у всех файлов одинаковое расширение "html".

- Если переписать предыдущую строчку в более консервативной форме (и, возможно, в более читабельной), она будет выглядеть так:[8]


#!/usr/bin/perl -w
while ( <> ){
    chomp;                      # Эквивалент "-l"
    if ( /title>([^<]+)/i ){
    rename $ARGV, "$1.html"
    }
}

- Но они набросились на нас...

- В точности. Эти дополнительные символы могли бы означать разницу между жизнью и смертью. Я должен сказать, что я не ожидал такой жёсткой реакции на простое "найти и напечатать", но говорят, что файловые системы становятся всё умнее и умнее - по словам западного гуру [6], с который я однажды беседовал, существует по крайней мере пять журналирующих файловых систем для Linux, и с тех пор я слышал о множестве патчей, касающихся файловых систем. К счастью, мы более чем справились с заданием.

- Теперь, если ты передашь мне того морского окуня по-роттердамски и сан-францисский соус, я расскажу тебе о следующем деле. Обрати внимание, Фринк, - оно обещает быть интересным...


[1] [Прим. пер. Для сохранения художественного замысла и игры слов пришлось придумать это слово. Ниже приведён перевод оригинального примечания]
Из жаргона: Ошибка Гейзенберга (Heisenbug) - Ошибка, которая исчезает или изменяет своё поведение, когда кто-нибудь пытается найти или определить её.

[2] Из жаргона: Устройство Регулировки Отношения Неудользователя. Правильное применение - сверху к голове соответствующей невежественной персоны.
[Прим. пер. И снова выдуманный мной термин, другие варианты приветствуются. Может быть "ЛАРТировать"? В оригинале: LART - Luser Attitude Readjustment Tool.]

[3] См. "perldoc File::Find".

[4] Поиск по образцу в Perl состоит из так называемых "регулярных выражений (regular expressions)". Для более подробной информации по ним см. "perldoc perlre".

[5] Perl-гольф - сильно извращённая форма программирования на Perl, где краткость - сестра таланта, а читабельность радостно выброшена в ближайшее окно. Вумерт - страстный игрок в гольф и часто производит на свет нечитабельную (но эффективную) тарабарщину на Perl; однострочники (строки на Perl длиной менее 80 символов) - обычный пример Perl-гольфа. ПРИМЕЧАНИЕ: В эту игру играют, чтобы произвести впечатление на других Perl-хакеров и чтобы создать короткие, но эффективные инструменты для командной строки. Использование таких строк в коде, с которым, по идее, будут работать и другие, или в коде, который вы пишете за плату, - это определённо плохая привычка и такие программы могут (должны!) вернуться, чтобы преследовать вас.

[6] Per Jim Dennis, 2001. Сегодня, наверно, есть ещё больше...

[7] [Прим. перев. Отрубить-то отрубает, но в этом случае в название файла попадёт строка от тега <title> до конца строки. И ещё я столкнулся с такой вещью: обрубаются только символы \n, поэтому файлы, подготовленные под Windows, не будут правильно обрабатываться из-за конца абзаца, отмечаемого двумя символами, а не одним, как в Linux.]

[8] [Прим. перев. Снова напомню, что скрипт может быть вызван командой "perl <scriptname> *", или просто "./scriptname *", если он был сделан выполняемым. Всё внимание на звёздочку.]


Copyright (c) 2002, Ben Okopnik. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 84 of Linux Gazette, November 2002


Вернуться на главную страницу