Next Previous Contents

Unix Review Column 31 -- Развлечения с шаблонами для дождливых

дней

Randal Schwartz

Апрель 2000

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

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

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

Какое отношение это имеет к Perl? Хорошо, я часто вижу вопросы ``как я могу создать шаблон, который можно заполнить''? Для основных применений ответом является ``посмотрите одно из решений работы с шаблонами, которые можно найти на CPAN''. Так что, перейдите на http://search.cpan.org и введите слово template в строке поиска, находящейся слева. Вы увидите несколько способов применения существующего кода.

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


  The [человек] went to the [place].

Как мы можем превратить [человек] в вопрос ``дайте человек'' и поместить ответ обратно в строку? Нечто подобное данному коду будет работать:


  $_ = "The [человек] went to the [place].";
  s/\[(.*?)\]/&process($1)/eg;
  sub process {
    print "дайте мне $_[0]: ";
    chomp(my $response = <STDIN>);
    $response;
  }
  print;

Все что мы делаем здесь -- мы проходим по значению переменной $_ с выполнением глобальной подстановки. Каждый раз, когда вы встречаем объект заключенный в кавычки, мы оцениваем правую часть подстановки как код Perl. В этом случае, действие заключается в запуске подпрограммы process с передачей ей переменной $1 в качестве параметра. Переменная получает входной параметр для того, чтобы создать запрос к пользователю, а затем считывает мой ответ. Возвращаемое процедурой значение становится заменяющим значением для объекта заключенного в кавычки. Обратите внимание на /eg в конце оператора подстановки: в этом случае мы получаем правую часть оператора как выполненный код, с выполнением глобальной подстановки.

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


  { local $/; $_ = <DATA> }
  s/\[(.*?)\]/&process($1)/egs;
  sub process {
    my $prompt = shift;
    $prompt =~ s/\s+/ /g;
    print "give me a $prompt: ";
    chomp(my $response = <STDIN>);
    $response;
  }
  print;
  __END__
  The [sad человек] went to the [fun
  place to go].

Теперь мы получим запрос, похожий на следующее:


  give me a sad человек: ____
  give me a fun place to go: ____

И значение в правой части будет заполнено соответствующим образом. Добавление суффикса s к оператору подстановки разрешает . соответствовать переводам строк. Внутри подпрограммы мы превращаем все переводы строк в одиночные пробелы. Также заметьте, что мы получаем шаблон из дескриптора файла DATA, который начинается в конце программы, сразу после отметки __END__.

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

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


  [person=человек]
  [place1=nearby place]
  [place2=far away place]
  [$person] went to [$place1], and then to [$place2].
  [$person] was [emotion after a long trip].

Здесь я ожидаю, что мы запросим о человеке, двух местах, а затем выполним подстановку, а затем выполним запрос о эмоции и выполним подстановку ответа сразу на место. Заметьте, что person используется дважды.

Мы скажем, что переменная должна быть идентификатором Perl (буквы, цифры и знаки подчеркивания), в регулярном выражении соответствующие \w. Таким образом, теперь скобки могут содержать три вещи и подпрограмма обработки должна различать три разных случая: (1) простой запрос, который необходимо заменить значением, (2) переменная, для которой необходимо выполнить запрос и (3) ссылка на переменную, для которой запрос уже выполнен.

Мы будеи хранить значения переменных в хеше с именем %value. Таким образом, подпрограмма process будеи выглядеть так:


  sub process {
    my $thing = shift;
    if ($thing =~ /^\$(\w+)$/) { # variable reference
      return $value{$1};
    }

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


    $thing =~ s/\s+/ /g;  # handle wrapping

И затем мы будем обрабатывать случай ``определения'':


    my $variable;
    $variable = $1 if $thing =~ s/^(\w+)=//; # может иметь значение undef

В этом месте, переменная $variable равна либо undef либо имени переменной, которую надо определить и запомнить. Так что, левая часть $thing является основой для запроса и затем получают значение:


    print "Give me a", $thing =~ /^[aeiou]/i ? "n " : " ", $thing, ": ";

Заметьте, что здесь есть дополнительная обработка, для того чтобы использовать в запросе ``an apple'' или ``a carrot'' при задании ``apple'' и ``carrot''. В заключении, давайте выполним запрос:


    chomp(my $response = <STDIN>);
    if (defined $variable) {
      $value{$variable} = $response;
      return "";
    }
    return $response;
  }

Заметьте, что если в скобках определяется переменная, то не возвращается никакого значения. Если вы хотите, чтобы определение автоматически запускалось, то вы можете опустить return "". Любой из способов является хорошим.

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


  [person=человек]
  [place1=nearby place]
  [place2=far away place]

заменяются ``ничем'', за которым следует перевод строки, и это происходит три раза. (Если вы до этого работали с m4, то вы можете посчитать это как необходимость использования в вашем вводе частых конструкций dnl()). Это немного неудобно, так что давайте будет отдельно обрабатывать такие случаи. Если строка полностью состоит из объекта заключенного в кавычки, то перевод строки автоматически удаляется.


  s<^\[([^]]+)\]\s*\n|\[([^]]+)\]>
   {&process(defined $1 ? $1 : $2)}meg;

Здесь я вновь использую операцию s/old/new/eg, разделяя две строки используя альтернативные разделители. Заметьте, что образец поиска состоит из двух отдельных регулярных выражений, которые объединены вертикальной линией:


  ^\[([^]]+)\]\s*\n

и


  \[([^]]+)\]

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

Правая часть оператора замены стала более сложной, поскольку нам надо использовать либо $1 либо $2, в зависимости от того, какой из шаблонов поиска был выбран. Для этого используется оператор defined(). И в заключение, оператор подстановки использует дополнительный суффикс m, означая, что символ ^ в регулярном выражении соответствует любому переводу строки, и соответственно суффикс произносится как meg, поскольку прошлой ночью смотрел фильм Meg Ryan на DVD.

И еще одно: у нас нет способа включения в текст символов левых и правых квадратных кавычек, так что давайте для этого будем использовать [LEFT] и [RIGHT]. Это можно сделать включив следующие строки в начало подпрограммы process:


    return "[" if $thing eq "LEFT";
    return "]" if $thing eq "RIGHT";

Давайте теперь совместим все кусочки кода. И для того, чтобы продемонстрировать как легко может быть использована эта программа, я нашел архив с разными историями ``для заполнения'' по адресу http://www.mit.edu/storyfun/, и помещу следующую историю в конец программы:


  { local $/; $_ = <DATA> }
  s/^\[([^]]+)\]\s*\n|\[([^]]+)\]/&process(defined $1 ? $1 : $2)/meg;
  sub process {
    my $thing = shift;
    return "[" if $thing eq "LEFT";
    return "]" if $thing eq "RIGHT";
    if ($thing =~ /^\$(\w+)$/) { # variable reference
      return $value{$1};
    }
    $thing =~ s/\s+/ /g;  # handle wrapping
    my $variable;
    $variable = $1 if $thing =~ s/^(\w+)=//; # may be undef
    print "Give me a", $thing =~ /^[aeiou]/i ? "n " : " ", $thing, ": ";
    chomp(my $response = <STDIN>);
    if (defined $variable) {
      $value{$variable} = $response;
      return "";
    }
    return $response;
  }
  print;

  __END__
  [LEFT]... from <htmlurl url="http://www.mit.edu/storyfun/I_went_for_a_walk">[RIGHT]
  [adj1=adjective]
  [place=place]
  [verbed=verb (ending in -ed)]
  [adj2=adjective]
  [nouns=plural noun]
  [plants=plural plant]
  [adj3=adjective]
  [adj4=adjective]
  [adj5=adjective]
  [noun=noun]
  [verbing=verb (ending in -ing)]
  [verb_past=verb (past tense)]
  [animals=plural animal]

  [your name] went for a walk

  Yesterday, I went out walking, and somehow ended up in [$place]. I saw
  [$plants] and [$animals] -- it was [$adj2]! But I started getting
  [$adj5] hungry, and needed to find my way home. But no matter where I
  [$verb_past], I couldn't see the path. I decided to go around the
  [$adj1] [$noun] up ahead, and discovered that it led back home! I was
  [$verbed]. At dinner, when I told my [$adj3] story, my [$nouns] looked
  at me with [$adj4] expressions. Then they forbade me from ever
  [$verbing] again.

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


Next Previous Contents