Next Previous Contents

Unix Review Column 20 -- Вычисление выражений в тексте

Randal Schwartz

Июнь 1998

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

Среди многих способов работы с текстовыми строками, Perl является ``text wrangling'' языком. Perl делает легким процесс считывания строк произвольной длины, выбора и извлечения интересующих данных, выдачи результатов в файлы, сокеты или другие процессы.

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

Hапример,

        $a = 3 + 4;
        print "I have $a eggs\n";

позволяет мне вычислить выражение 3 + 4 и затем пометить результат в строку. Однако помещение того же самого выражения в строку не будет работать:

        print "I have 3 + 4 eggs\n";

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

Прием заключается в простом предшевствовании выражения символами @{[ и завершении его символами ]}, например вот так:

 
      print "I have @{[ 3 + 4 ]} eggs\n";

Если вы выполните этот код, то вы увидите, что он правильно печатает слово ``7 eggs''! Как это работает? Внешний @{ ... } включает режим интерполяции массива, требуя чтобы внутри скобок находилось либо имя массива, либо ссылка на список. Внутренние квадратные скобки создают анонимный список, и возвращают ссылку на этот список. Этот анонимный список вычисляется из списка выражений внутри скобок -- в нашем случае есть только одно выражение, так что получается список из одного элемента.

Таким образом, выражение вычисляется, превращается в анонимный список, а затем интерполируется триггером @, и все сделано!

Мы даже можем использовать эту конструкцию в более крупных документах (используя here-строки):

        open SM, "|/usr/lib/sendmail -t";
        print SM <<END;
        To: $destination
        From: @{[$source || "root"]}
        Subject: update at @{[scalar localtime]}

        Следующие люди зарегистрированы
        на @{[`hostname` =~ /(.*)/]}:

        @{
          my %foo = map /^(\S+)()/, `who`;
          [sort keys %foo];
        }
        END
        close SM;

Я привел несколько примеров... давайте разберем каждый по отдельности. В первом примере, я открываю канал в sendmail, для того, чтобы отослать электронное сообщение. Далее, я выдаю в этот канал строку заключенную в двойные кавычки. Переменная $destination является обычной скалярной переменной, которую я устанавливаю где-то перед выполнением данного кода.

Строка from сообщения использует конструкцию описанную выше. Если переменная $source установлена, то она используется -- в противном случае возвращается строка root.

Строка subject сообщения также использует конструкцию, описанную выше. В скалярном контексте оператор localtime возвращает строку времени. Поскольку конструктор анонимного массива, заключенный в квадратный скобки требует оценки элементов в списочном контексте, то я привожу его в скалярный контекст с помощью оператора scalar. Результирующие выражение переводится в строку заголовка сообщения с относительной легкостью.

Аналогичным образом, имя текущей машины вычисляется и вставляется в текст. Заметьте, что я беру вывод команды hostname, заключенной в обратные кавычки, и выделяю из него все символы, находящиеся до символа новой строки. Таким образом, символ новой строки не выделяется, и я могу использовать результат в середине строки.

Финальный кусок кода внутри строки, использует дополнительный прием, Конструкция @{...} является любым блоком кола, также как и последнее выражение, вычисленное в этом блоке является ссылкой на список некоторого типа. Таким образом, для получения уникального списка пользователей в системе, я могу использовать временный хэш как набор, Вывод команды who разделяется на строки и сравнивается со строкой регулярного выражения, выделяя два элемента из общего списка для каждой из строк. Это является правильной формой для создания хэша. В заключение ключи хэша сортируются и превращаются в анонимный список.

Другим способом получения текстовой строки являющейся ``в основном константой, но иногда изменяющейся'' является выполнение глобальной подстановки для строки. Хотя мы пока не можем получать произвольные выражения, оно достаточно хорошо работает тогда, когда данные получаются из структуры подобной хэшу:

        %data = (
                TO => 'fred@random.place',
                PRIZE => 'a pearl necklace',
                VALUE => '1000',
        );
        $_ = <<'EOF';
        To: %TO%
        From: merlyn@stonehenge.com
        Subject: Your lucky day

        You are the winner of %PRIZE%,
        worth over $%VALUE%!  Congratulations.
        EOF
        s/%(\w+)%/$data{$1}/g;
        print;

Для каждого из слов, найденных между знаками процента, по ключу ищется соответствующий элемент хэша, и заменяется этим значением. Это является хорошим для проблем набора символов (form-letter type problems). Если данные не могут быть сохранены в хэше, то мы можем сделать дополнительный шаг и сделать замены текста полным выражением, вместо простой, заключенной в двойные кавычки строки, используя модификатор /e для оператора подстановок.

        $_ = <<'EOF';
        To: %TO%
        From: merlyn@stonehenge.com
        Subject: Your lucky day

        You are the winner of %PRIZE%,
        worth over $%VALUE%!  Congratulations.
        EOF
        s/%(\w+)%/&getvaluefor($1)/eg;
        print;
        sub getvaluefor {
                my $key = shift;
                ...
        }

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

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

        $_ = 'I have [ 3 + 4 ] eggs';
        s/\[(.*?)\]/$1/eegs;
        print;

Этот код выдает I have 7 eggs, но каким образом? Хорошо, исключая то, что мы знаем... /s означает, что . может соответствовать символу новой строки. А /g означает, что выполняется более чем одна подстановка. А одиночное /e означает, что правая часть оператора является выражением Perl, а не заключенной в двойные кавычки строкой. И у нас здесь имеется переменная $1, что очень хорошо.

Но наличие второго /e означает, что значение выражения в правой части также должно рассматриваться как код на Perl, и затем должно быть превращено в строковое значение! (В начале это рассматривалось как ошибка, но затем это посчитано очень полезным и осталось как свойство).

Так что мы получаем $1 равным " 3 + 4 " и затем 7, и 7 вставляется вместо заключенного в скобки выражения. Мы можем вставить между скобками все что захотим, и это будет выполнено как код Perl.

Так, теперь у нас есть они... много способов использования текста являющегося в ``в основном константой , но иногда переменной'' в наших программах. Разрешите мне закончить это раздел кусочком истории. Я заканчивал свои письма в comp.lang.perl.misc некоторым хитрым куском кода (часто неясным), который печатал фразу ``Just another Perl hacker,''. Когда я обнаружил возможность двойной оценки кода в процессе подстановки, я просто использовал его для создания одной из этих фраз ``JAPH''. И вот что получилось в результате:

  $Old_MacDonald = q#print #; $had_a_farm = (q-q:Just another Perl hacker,:-);
  s/^/q[Sing it, boys and girls...],$Old_MacDonald.$had_a_farm/eieio;

Посмотрите что получится, если вы сможете определить как это работает!

Особые благодарности ведущим разработчикам Perl, Chip Salzenberg, за идею создания этой ежемесячной колонки. Спасибо Chip!


Next Previous Contents