Het programma Lex genereert een zogenaamde `Lexer'. Dit is een functie die een stroom karakters als input neemt, en steeds als het een groep karakters ziet die met een sleutelwaarde overeenkomen, een bepaalde actie onderneemt. Een heel eenvoudig voorbeeld:
%{
#include <stdio.h>
%}
%%
stop printf("Stop commando ontvangen\n");
start printf("Start commando ontvangen\n");
%%
De eerste sectie, tussen de %{ en %} wordt direct overgenomen in het gegenereerde programma. Dit is nodig omdat we later printf gebruiken, wat gedefinieerd is in stdio.h.
Secties worden gescheiden door `%%', dus de eerste regel van de tweede sectie start met de `stop' sleutel. Iedere keer dat de `stop' sleutel tegengekomen wordt in de input, wordt de rest van de regel (een print() call) uitgevoerd.
Behalve `stop' hebben we ook een `start' gedefinieerd, die verder grotendeels hetzelfde doet.
We beëindigen de code weer met `%%'.
Doe dit om Voorbeeld 1 te compileren:
lex example1.l
cc lex.yy.c -o example1 -ll
OPMERKING:Als je flex gebruikt ipv lex, moet je misschien `-ll'
vervangen door `-lfl' in de compilatiescripts. Redhat 6.x en SuSE
vereisen dit, zelfs als je `flex' aanroept als `lex'!
Dit genereert het programma `example1'. Als je het draait, wacht het tot je iets intikt. Als je iets typt dat niet overeenkomt met een gedefinieerde sleutel (`stop' en `start') wordt het weer geoutput. Als je `stop' intikt geeft het `stop commando ontvangen';
Sluit af met een EOF (^D).
Je vraagt je misschien af hoe het programma draait, daar we geen main() gedefinieerd hebben. Die functie is voor je gedefinieerd in libl (liblex) die we ingecompileerd hebben met het -ll commando.
Dit voorbeeld was op zichzelf niet erg nuttig, en dat is het volgende ook niet. Het laat echter wel zien hoe je reguliere expressies gebruikt in Lex, wat later superhandig zal zijn.
Voorbeeld 2:
%{
#include <stdio.h>
%}
%%
[0123456789]+ printf("NUMMER\n");
[a-zA-Z][a-zA-Z0-9]* printf("WOORD\n");
%%
Dit Lex bestand beschrijft twee soorten matches (tokens): WOORDen en NUMMERs. Reguliere expressies kunnen behoorlijk intimiderend zijn maar met een beetje werk zijn ze makkelijk te begrijpen. Laten we de NUMMER match bekijken:
[0123456789]+
Dit betekent: een reeks van een of meer karakters uit de groep 0123456789. We hadden het ook kunnen afkorten tot:
[0-9]+
De WOORD match is iets ingewikkelder:
[a-zA-Z][a-zA-Z0-9]*
Het eerste deel matcht 1 en slechts 1 karakter tussen `a' en `z', of tussen `A' en `Z'. Met andere woorden, een letter. Deze beginletter moet gevolgd worden door nul of meer karakters die ofwel een letter of een cijfer zijn. Waarom hier een asterisk gebruiken? De `+' betekent 1 of meer matches, maar een WOORD kan heel goed uit slechts 1 karakter bestaan, dat we al gematcht hebben. Dus het tweede deel heeft misschien nul matches, dus schrijven we een `*'.
Op deze manier hebben we het gedrag van veel programmeertalen geïmiteerd die vereisen dat een variabelenaam met een letter *moet* beginnen maar daarna cijfers mag bevatten. Met andere woorden, `temperature1' is een geldige naam, maar `1temperature' niet.
Probeer voorbeeld 2 te compileren, net zoals voorbeeld 1, en voer wat tekst in. Een voorbeeldsessie:
<tscreen><verb>
$ ./example2
foo
WOORD
bar
WOORD
123
NUMMER
bar123
WOORD
123bar
NUMMER
WOORD
Je vraagt je misschien ook af waar al die witregels in de uitvoer vandaan komen. De reden is eenvoudig: het was in de invoer, en het wordt nergens gematcht, dus wordt het weer uitgevoerd.
De Flex manpage beschrijft de reguliere expressies in detail. Velen vinden de perl reguliere expressies manpage (perlre) ook nuttig, al kan Flex niet alles dat perl kan.
Zorg dat je geen matches met een lengte van nul zoals `[0-9]*' maakt - je lexer kan in de war raken en lege strings herhaaldelijk matchen.
Laten we zeggen dat we een bestand willen parsen dat er zo uitziet:
logging {
category lame-servers { null; };
category cname { null; };
};
zone "." {
type hint;
file "/etc/bind/db.root";
};
We herkennen duidelijk een aantal categorieën (tokens) in dit bestand:
Het overeenkomstige Lex bestand is Voorbeeld 3:
%{
#include <stdio.h>
%}
%%
[a-zA-Z][a-zA-Z0-9]* printf("WOORD ");
[a-zA-Z0-9\/.-]+ printf("BESTANDSNAAM ");
\" printf("QUOTE ");
\{ printf("OBRACE ");
\} printf("EBRACE ");
; printf("PUNTKOMMA ");
\n printf("\n");
[ \t]+ /* negeer whitespace */;
%%
Als we ons bestand aan het programma toevoeren dat door dit Lex bestand gegenereerd is (met gebruik van example3.compile) krijgen we:
WOORD OBRACE
WOORD BESTANDSNAAM OBRACE WOORD PUNTKOMMA EBRACE PUNTKOMMA
WOORD WOORD OBRACE WOORD PUNTKOMMA EBRACE PUNTKOMMA
EBRACE PUNTKOMMA
WOORD QUOTE BESTANDSNAAM QUOTE OBRACE
WOORD WOORD PUNTKOMMA
WOORD QUOTE BESTANDSNAAM QUOTE PUNTKOMMA
EBRACE PUNTKOMMA
Als we dit vergelijken met het bovengenoemde configuratiebestand, wordt duidelijk dat we het netjes `getokeniseerd' hebben. Ieder deel van het configuratiebestand is gematcht, en omgezet naar een token.
We hebben geleerd dat Lex in staat is om willekeurige invoer te lezen, en te bepalen wat ieder onderdeel van de invoer is. Dit wordt `tokenizering' genoemd.