Лексинг / Разбор "здесь" документов
Для тех, кто является экспертом по лексингу и парсингу... Я пытаюсь написать серию программ на Perl, которые будут анализировать мэйнфрейм IBM z/OS JCL для различных целей, но я сталкиваюсь с препятствиями в методологии. Я в основном следую идеологии лексического анализа / парсинга, выдвинутой в "Perl высшего порядка" Марком Джейсоном Доминусом, но есть некоторые вещи, которые я не могу понять, как это сделать.
У JCL есть так называемые встроенные данные, которые очень похожи на "здесь" документы. Я не совсем уверен, как лексировать их в токены.
Структура для встроенных данных выглядит следующим образом:
//DDNAME DD *
this is the inline data
this is some more inline data
/*
...
Условно, "*" после "DD" означает, что следующие строки являются самими встроенными данными, оканчивающимися либо "/*", либо следующей действительной записью JCL (начинающейся с "//" в первых 2 столбцах).
Более продвинутые, встроенные данные могут выглядеть так:
//DDNAME DD *,DLM=ZZ
//THIS LOOKS LIKE JCL BUT IT'S ACTUALLY DATA
//MORE DATA MASQUERADING AS JCL
ZZ
...
Иногда встроенные данные сами по себе являются JCL (возможно, для перекачки в программу или во внутренний ридер, что угодно).
Но вот беда. В JCL записи имеют размер 80 байтов фиксированной длины. Все, что за столбцом 72 (столбцы 73-80), является "комментарием". Кроме того, все, что следует за пробелом, следующим за действительным JCL, также является комментарием. Поскольку я пытаюсь манипулировать JCL в своих программах и выкладывать его обратно, я бы хотел записать комментарии, чтобы сохранить их.
Итак, вот пример встроенных комментариев в случае встроенных данных:
//DDNAME DD *,DLM=ZZ THIS IS A COMMENT COL73DAT
data
...
ZZ
...more JCL
Первоначально я думал, что мог бы получить свой самый верхний лексер в строке JCL и сразу создать не-токен для столбцов 1-72, а затем токен (['COL73COMMENT',$1]) для комментария столбца 73, если любой. Это затем передало бы следующему итератору / токенизатору строку текста cols 1-72, за которым следовал токен col73.
Но как бы мне, оттуда, взять встроенные данные? Первоначально я полагал, что самый верхний токенизатор может искать "DD \*(,DLM=(\S*))" (или тому подобное), а затем просто продолжать извлекать записи из итератора подачи, пока он не достигнет разделителя. или действительный стартер JCL ("//").
Но вы можете увидеть проблему здесь... У меня не может быть 2 самых верхних токенизаторов... либо токенайзер, который ищет комментарии COL73, должен быть верхним, либо токенайзер, который получает встроенные данные, должен быть сверху.
Я думаю, что парсеры Perl имеют ту же проблему, так как
<<DELIM
не обязательно конец строки, за которым следуют данные документа. В конце концов, вы могли видеть Perl как:
my $this=$obj->ingest(<<DELIM)->reformat();
inline here document data
more data
DELIM
Как бы токенизатор / парсер узнал, что нужно токенизировать ")->reformat();" а затем все еще захватывать следующие записи как есть? В случае встроенных данных JCL эти строки передаются как есть, столбцы 73-80 НЕ являются комментариями в этом случае...
Итак, кто-нибудь по этому поводу? Я знаю, что будет множество вопросов, разъясняющих мои потребности, и я с удовольствием уточню столько, сколько необходимо.
Заранее благодарю за любую помощь...
2 ответа
В этом ответе я сосредоточусь на heredocs, потому что уроки могут быть легко перенесены в JCL.
Любой язык, который поддерживает heredocs, не является контекстно-свободным и, следовательно, не может быть проанализирован с помощью общих методов, таких как рекурсивное спускание. Нам нужен способ, чтобы направлять лексер по более извилистым путям, но при этом мы можем поддерживать внешний вид языка без контекста. Все, что нам нужно, это еще один стек.
Для парсера мы относимся к введению в heredocs <<END
как строковые литералы. Но лексер должен быть расширен, чтобы сделать следующее:
- Когда встречается введение heredoc, он добавляет терминатор в стек.
- Когда встречается новая строка, тело heredoc лексируется до тех пор, пока стек не станет пустым. После этого нормальный разбор возобновляется.
Позаботьтесь об обновлении номера строки соответствующим образом.
В рукописном комбинированном парсере / лексере это может быть реализовано так:
use strict; use warnings; use 5.010;
my $s = <<'INPUT-END'; pos($s) = 0;
<<A <<B
body 1
A
body 2
B
<<C
body 3
C
INPUT-END
my @strs;
push @strs, parse_line() while pos($s) < length($s);
for my $i (0 .. $#strs) {
say "STRING $i:";
say $strs[$i];
}
sub parse_line {
my @strings;
my @heredocs;
$s =~ /\G\s+/gc;
# get the markers
while ($s =~ /\G<<(\w+)/gc) {
push @strings, '';
push @heredocs, [ \$strings[-1], $1 ];
$s =~ /\G[^\S\n]+/gc; # spaces that are no newlines
}
# lex the EOL
$s =~ /\G\n/gc or die "Newline expected";
# process the deferred heredocs:
while (my $heredoc = shift @heredocs) {
my ($placeholder, $marker) = @$heredoc;
$s =~ /\G(.*\n)$marker\n/sgc or die "Heredoc <<$marker expected";
$$placeholder = $1;
}
return @strings;
}
Выход:
STRING 0:
body 1
STRING 1:
body 2
STRING 2:
body 3
Парсер Marpa немного упрощает это, позволяя запускать события после разбора определенного токена. Они называются паузами, потому что встроенный лексинг делает паузу для вас. Вот краткий обзор высокого уровня и короткий пост в блоге, описывающий эту технику с помощью демонстрационного кода на Github.
На случай, если кому-то будет интересно, как я решил эту проблему, вот что я сделал.
Моя основная лексическая подпрограмма принимает итератор, который перекачивает полные строки текста (который может взять его из файла, строки, чего угодно). Подпрограмма использует это для создания другого итератора, который проверяет строку "комментариев" после столбца 72, которую затем возвращает как токен "mainline", за которым следует токен "col72". Затем этот итератор используется для создания еще одного итератора, который пропускает токены col72 без изменений, но берет основные токены и переводит их в атомарные токены (такие как STRING, NUMBER, COMMA, NEWLINE и т. Д.).
Но вот в чем суть... у подпрограммы lexing еще есть ОРИГИНАЛЬНЫЙ ИТЕРАТОР... поэтому, когда он получает токен, указывающий на наличие документа "здесь", он продолжает обрабатывать токены, пока не достигнет токена NEWLINE (означающего конец фактического текстовая строка), а затем использует исходный итератор для извлечения данных документа здесь. Поскольку этот итератор передает итератор атомарных токенов, извлечение из него предотвращает атомизацию этих строк.
Для иллюстрации подумайте об итераторах как о шлангах. Первый шланг является основным итератором. К этому я присоединяю шланг итератора col72, и к этому я присоединяю шланг атомного токенизатора. Когда потоки символов попадают в первый шланг, распыленные жетоны выходят из конца третьего шланга. Но я могу прикрепить двухстороннее сопло к первому шлангу, которое позволит его выходу выходить из альтернативного сопла, предотвращая попадание этих данных во второй шланг (и, следовательно, в третий шланг). Когда я закончу перенаправление данных через альтернативное сопло, я могу отключить это, и тогда данные снова начнут течь через второй и третий шланги.
Easy-peasey.