2. Flex

Jak jsme si již řekli v úvodní části našeho povídání, flex generuje z našeho popisu tokenů scanner: kód v jazyce C, který ve vstupním textu rozeznává lexikální vzory. V této části se budeme zabývat pravidly pro psaní takových souborů.

2.1 Soubor file.l

definice
%%
pravidla
%%
libovolný C kód

V souboru s pravidly pro flex můžeme rozlišit celkem tři části, které jsou od sebe oddělené řádky obsahujícími pouze dva znaky pro procento. V prvé části souborů pro flex můžeme nalézt deklarace jmen, které nám později usnadní psaní pravidel. Je rovněž možné (a vhodné) zařadit sem definice proměnných, které později použijete ve svém kódu.

Do druhé části zapisujeme pravidla lexikálního scanneru. Jedná se o dvojice regulární výraz a kód v jazyce C, který je vykonán, je-li daný regulární výraz ve vstupním souboru rozpoznán.

Třetí část je ze všech nejjednodušší - je totiž beze změn okopírována do výsledného zdrojového souboru, můžete sem tedy umístit těla libovolných pomocných funkcí či funkci main (). Pokud třetí část vynecháte, je vygenerována automaticky funkce main, která okamžitě spustí scanner - funkci yylex ().

2.2 Jednoduché příklady

Dříve, než začnu podrobněji popisovat jednotlivé části, předvedeme si dva kratší příklady. Nejedná se sice o typické příklady použití programu flex, jistě z nich však získáte alespoň rámcovou představu jeho možností.

subst-login.l
%%
username    printf("%s", getlogin());

V tomto souboru je jediné pravidlo - kdykoliv se ve vstupním textu vyskytne slovo username, je do stdout vytištěno jméno uživatele, který program spustil. Má podobný program význam? Jistěže. Pokud vstupní znaky nevyhovují žádnému pravidlu, je použito pravidlo implicitní, které vytiskne řetězec do stdout.

countchar.l
	int num_of_chars = 0,
	    num_of_lines = 0;
%%

\n		num_of_lines++; num_of_chars++;
.		num_of_chars++;

%%

int main(int argc, char **argv)
{
  yylex ();
  printf ("Počet řádků: %d, počet znaků: %d\n", 
	   num_of_lines, num_of_chars);
  return 0;
}

Druhý příklad demonstruje, jakým způsobem můžeme ve flexu napsat program počítající znaky a řádky vstupního souboru. Funkce yylex () čte vstupní znaky, implicitně z stdin. Pokud se ve vstupním souboru vyskytne znak pro konec řádky, zvýší se čítač řádků i čítač znaků, v opačném případě se zvýší pouze počet znaků.

Samo sebou zbývá otázka, jak si můžete oba programy vyzkoušet. Programem flex získáme soubor v jazyce C a ten již dál překládáme běžným způsobem:

0rfelyus@hobitin:~$ flex countchar.l
0rfelyus@hobitin:~$ gcc lex.yy.c -o countchar -lfl

nebo:

0rfelyus@hobitin:~$ flex -ocountchar.yy.c countchar.l 
0rfelyus@hobitin:~$ gcc countchar.yy.c -o countchar -lfl

Jestliže flex generuje zdrojové soubory, proč je nutné linkovat výsledný kód s knihovnou libfl? Flexem vygenerovaný kód scanneru vyžaduje definice rozličných funkcí - například funkce yywrap (), která je zavolána při dosažení konce vstupního souboru. Je samozřejmě na programátorovi, aby si sám určil, co se v takovém případě má stát (například který soubor je nutné dále zpracovat). V knihovně libfl jsou jednoduché implementace těchto funkcí, tudíž v případě jednoduchých scannerů, kdy není nutné modifikovat standardní chování, není třeba psát mnoho kódu. Pokud však chceme od scanneru chování odlišné, stále je zde možnost napsat si funkce vlastní.

2.3 První část souboru file.l - definice

Jak jsem se již zmínil, do první části souboru lze mimo jiné zařadit definice proměnných, které později použijete (například jako proměnnou num_of_chars z příkladu countchar.l). Ve skutečnosti jsou vaše možnosti bohatší - jakýkoliv odsazený kód nebo kód uvozený mezi řádky obsahující %{ a %}, bude beze změn okopírován na začátek výsledného zdrojového kódu.

Příklad 1
	/* Tento odsazený komentář bude v example.yy.c */

%{

/* A tento komentář tam bude také */

/* Můžeme sem také zařadit hlavičkové soubory .... */

#include <string.h>
#include "myheader.h"

/* .... a nebo definice proměnných */

int	my_fair_variable = 0;

%}

Abychom zpřehlednili pravidla scanneru, můžeme též definovat jména pro regulární výrazy. Definice jména se sestává ze jména na začátku řádky následovaného mezerami a regulárním výrazem začínajícím prvním "viditelným znakem" (tedy nikoliv mezerou nebo tabulátorem). Jméno musí začínat písmenem či podtržítkem, dále se může skládat i z číslic či znaku pomlčky.

Příklad 2 - jména
DIGIT		[0-9]
HEXDIGIT	[0-9A-F]

2.4 Druhá část souboru file.l - pravidla

Každé pravidlo se skládá ze dvou částí - pattern (vzor) a akce, vzor i akce musí být uvedeny na témže řádku a vzor nesmí být odsazený (neboť i zde platí, že odsazený text je beze změny okopírován do výstupního zdrojového souboru). Vzor (pattern) je regulární výraz - tak, jak jej známe z ostatních standardních UNIXových nástrojů (awk, grep -e,...) rozšířený o několik málo nových pravidel, akce je libovolný kód v jazyce C.

2.4.1 Regulární výrazy

V následující tabulce je přehled regulárních výrazů, které můžete ve svých vzorech použít:

. Libovolný znak (kromě nového řádku)
^ Začátek řádku
$ Konec řádku
[abcdik] [a-dik] Třída znaků (character class). Oběma příkladům vyhovují znaky 'a', 'b', 'c', 'd', 'i', 'k'
[^abcdik] [^a-dik] Ani jeden ze znaků - negace třídy znaků.
[:alpha:] [:alnum:] Třídy znaků odpovídající funkcím z libc: isalpha, isalnum,...
{NAME} Provede expanzi v první části definovaného jména. Například {HEXDIGIT} expanduje do regulárního výrazu [0-9A-F].
"text" Na text v uvozovkách se nevztahují pravidla pro speciální znaky regulárních výrazů (+,*,|,...)
\n \123 \xef Backslash funguje tak, jak jsme si navykli v ostatních programech (newline, octal, hexadecimal,..)
R* R+ R{n,m} Regulární výraz opakovaný libovolněkrát, jednou či vícekrát, n-krát až m-krát (m nebo n je možné vynechat
R|S Textu vyhovuje buď regulární výraz R nebo S
(R) Uzávorkuje celý regulární výraz. Vhodné v kombinaci s R*, R|S,...
R/S Regulární výraz R, právě tehdy když je následován výrazem S, S (tzv trailing) se však do regulárního výrazu nepočítá.
R$ je ekvivalentní R/\n
<<EOF>> End-of-file. Tento vzor nelze spojovat s ostatními.

2.4.2 Matching - porovnávání

Jistě si kladete otázku, která akce bude zvolena, pokud posloupnost vstupních znaků vyhovuje více regulárním výrazům. Vždy bude zvolena akce, jejíž regulární výraz odpovídá nejdelšímu vstupnímu řetězci (včetně trailing). Pokud se maximální možná délka odpovídajícího vstupního řetězce rovná pro více regulárních výrazů, je zvolena akce dříve uvedeného vzoru.

Jak jsme si již řekli, pokud vstupnímu textu neodpovídá žádný regulární výraz, je text vytištěn do stdout.

Příklad 3.
prvni		printf ("A");
prvni/druhy	printf ("B");
p.*i		printf ("C");

Příklady vstupů a výstupů:

Vstup Výstup Zdůvodnění
prvni A Vstupu sice odpovídá ještě třetí vzor, ten je však uveden až po vzoru prvním
prvniprvni C Vstupu by odpovídaly dva první vzory, avšak nejdelší text ze vstupního řetězce odpovídá třetímu vzoru.
prvnidruhy Bdruhy Druhý vzor "vyhrál" nad ostatními, neboť mu odpovídá nejdelší text ("prvnidruhy"). Trailing ("druhy") se však do zpracovaného textu nepočítá, proto se jej scanner pokusil opět zpracovat. Neodpovídá mu žádné pravidlo, bude proto vytištěn do stdout.

2.4.3 Akce

Akce je libovolný kód v jazyce C. Pokud se tento kód nevejde spolu se vzorem na jednu řádku, je možné jej rozdělit na více řádek, ovšem potom jej musíme ohraničit složenými závorkami (otevírající složená závorka musí být na témže řádku jako vzor). Samozřejmě je možné akci zcela vynechat a tím daný vzor ignorovat (je však dobrým zvykem napsat explicitně komentář /* ignored */ - zlepší se jak čitelnost kódu, tak vaše karma).

Jistě vás okamžitě napadne využití - komentáře ve vstupním textu.

Příklad 4 - komentáře
#.*\n		/* shellovské komentáře - ignoruj do konce řádky */
\/\/.*\n	/* C++ komentáře - ignoruj */

Pokud se shodují akce několika vzorů, můžete si život (a soubor s definicemi) zjednodušit znakem '|' (pipe) místo akce. Znak '|' znamená - tato akce je totožná s akcí následující.

Kromě běžného kódu, můžete ve svých akcích používat ještě následující předdefinované makra, funkce a proměnné, z nich nejdůležitější yytext a yylength.

char *yytext Ukazatel na začátek textu odpovídajícího regulárnímu výrazu
int yylength Délka textu odpovídajícího regulárnímu výrazu
ECHO Vytiskne yytext na výstup
REJECT Spustí akci druhého nejlepšího vzoru
yymore Následující akce obdrží v proměnné yytext text odpovídající jejímu vzoru připojený k textu, který obsahuje proměnná yytext nyní.
yyless(n) Vrátí do vstupního bufferu scanneru obsah proměnné yytext až na prvních n znaků
unput(c) Vrátí znak c do vstupního bufferu
input () Přečte jeden znak ze vstupního bufferu

2.4.4 Start-condition - počáteční stav

Velmi často vstupní text není zcela "homogenní" a některé části je nutné zpracovat jinak než ostatní. Dobrým příkladem mohou být například řetězce v jazyce C. Na rozdíl od zbytku textu si musíme v řetězcích všímat různých podřetězců - "\n", "\123",... Potřebujeme tedy v rámci scanneru jakýsi mini-scanner.

S podobnými problémy se vypořádáme právě pomocí počátečních stavů (start-condition), kterými můžeme pravidla dočasně povolit či zakázat. Nejdříve musíme v první části souboru s pravidly stav deklarovat. %x deklaruje stav exkluzivní (je-li stav aktivní, platí pouze pravidla podmíněná tímto stavem), %s stav inkluzivní (pokud je stav aktivní, platí všechna pravidla a pravidla podmíněná tímto stavem).

Spojení pravidla a stavu docílíme uvedením výčtu stavů uvozených mezi znaky '<' a '>' před vzor pravidla (více stavů oddělíme čárkami). Abychom byli ušetřeni neustálého opakování stavů, můžeme soubor pravidel obalit složenými závorkami a společné stavy umístit před tyto závorky (viz příklad).

Na počátku je scanner ve stavu INITIAL. Změnu stavu scanneru dosáhneme direktivou BEGIN(stav). Současný stav scanneru je možné obdržet pomocí makra YYSTATE a jelikož jsou stavy implementovány pomocí typu int (integer), lze jej jednoduše uschovat a později obnovit.

Nad to jsou k disposici funkce implementující zásobník stavů. Myslím, že by bylo zbytečné podrobně vysvětlovat význam funkcí yy_push_state (), yy_pop_state (), yy_top_state ().

Příklad 5.
%x		STRING 

%%

\"		yy_push_state (); BEGIN (STRING);

<STRING> {

\"		yy_pop_state ();
\\n		do_something_usefull ('\n');
\\[0-7]{3}      d_s_u (strtol (yytext, NULL, 8));
		.
		.
		.
}

2.5 Vstup

Implicitně scanner zpracuje soubor yyin, který je na počátku roven stdin. Jelikož scanner interně bufferuje vstup, je bezpečné do proměnné yyin přiřadit pouze na konci souboru - v pravidle <<EOF>> nebo funkci int yywrap (), kterou scanner volá na konci souboru. Pokud funkce yywrap nastavila yyin na nový soubor, vrátí nulu. V opačném případě vrátí nenulové číslo a tím sdělí scanneru, že již žádné další soubory zpracovány nebudou.

Následující tabulka obsahuje funkce, kterými lze pracovat se vstupnímu buffery scanneru. Myslím, že je jednoduché domyslit si, jak s jejich pomocí ve scanneru implementovat například direktivu "include".

YY_BUFFER_STATE yy_create_buffer (FILE *file, int size)
void yy_switch_to_buffer (YY_BUFFER_STATE new_buffer)
void yy_delete_buffer (YY_BUFFER_STATE buffer)
void yy_flush_buffer (YY_BUFFER_STATE buffer)

Samozřejmě kromě souborů lze specifikovat mnohem "exotičtější" zdroje scanneru:

yy_scan_string (const char *str)
yy_scan_bytes (const char *bytes, int len)

[Obsah] [Předchozí kapitola: Úvod] [Následující kapitola: Bison]


© 1999 0rfelyus Emacs