プログラミングユーティリティ

lex 規則の基礎

必須の規則セクションは、区切り記号 %% で開始します。ルーチンのセクションを後に続ける場合は、規則の最後の行に %% を記述して規則のセクションをそこで終了します。区切り記号 %% は、行の先頭に入力する必要があります。%% の前に空白文字は入れないでください。2 つ目の区切り記号がない場合には、規則セクションがプログラムの終わりまで続いているとみなされます。

規則セクション内の最初の規則の前にある空白で始まる行は、関数 yylex() の開始部分の最初の中括弧のすぐ後ろにコピーされます。この機能を利用すれば、yylex() の局所変数を宣言できます。

各規則によって、検索するパターンと、そのパターンが見つかったときに実行するアクションが指定されます。パターン仕様は、行の先頭から入力する必要があります。スキャナは、パターンと一致しない入力を出力ファイルに直接書き込みます。したがって、最も単純な字句アナライザプログラムは、規則セクションの開始を示す区切り記号 %% だけが入っているプログラムです。このプログラムは、すべての入力をまったく変更を加えずにそのまま出力に書き込みます。

正規表現

検索するパターンは、正規表現と呼ばれる表記法を使用して指定します。正規表現は、文字や演算子をつなぎ合せて形成します。最も単純な正規表現は、以下のような演算子が 1 つも含まれていないテキスト文字の列です。

apple 
orange
pluto 

これら 3 つの正規表現は、入力テキストに現われるこれと同じ文字列すべてと一致します。入力テキストに現われる orange をスキャナにすべて削除させたい場合には、以下のように規則を指定することができます。

orange ;

セミコロンの後にアクションが指定されていないため、スキャナは何も行わずに、元の入力テキストからこの正規表現に一致した文字列をすべて削除したテキストを出力します。つまり、出力されるテキストには、文字列 orange はまったく含まれません。

演算子

ほとんどの式は、orange のように簡単に指定することはできません。式自体は非常に長いものであったり、あるいは (こちらの方がよくある事例ですが) その式のクラスが非常に大きい可能性があります。実際、無限の大きさである可能性も考えられます。

lex 演算子 (表 2-1 を参照) を使用して、特定のクラスのあらゆる式を表すための正規表現を形成できます。たとえば + 演算子は、直前の式が 1 個以上存在することを表します (つまり、直前の式は省略可能です)。? は、直前の式が 0 個または 1 個存在することを表します。* は、直前の式が 0 個以上存在することを表します。したがって m+ は、1 個以上の m で構成される文字列すべてと一致する正規表現になります。

mmm 
m 
mmmmm 

また、7* は、0 個以上の 7 で構成される文字列すべてと一致する正規表現となります。

77 
77777 

777

空白の 3 行目が正規表現に一致したのは、その行に 7 が 1 つも含まれていないためです。

| 演算子は、二者択一を表します。したがって、ab|cdab または cd と一致します。演算子 {} は、繰り返しを指定します。したがって a{1,5} は、a が 1 〜 5 個含まれている文字列と一致します。A(B{1,4}) は、ABCABBCABBBCABBBBC と一致します (グループ化するための記号として丸括弧 ()を使用している点に注目してください)。

角括弧 [] は、角括弧内に指定された文字列のいずれかの文字を表します。したがって [dgka] は、1 個の dgk、または a と一致します。角括弧内の文字は、間に空白文字や句読点を入れないで隣接して並べてください。

^ 演算子は、左角括弧の直後の文字として使用したときには、角括弧内に指定した文字を除く標準セットのすべての文字を表します。|{}、および ^ は、他の目的で使用する場合もあるので注意してください。

標準のアルファベットや数字 (A 〜 Z、a 〜 z、0 〜 9) の範囲は、ハイフンを使用して指定します。たとえば、[a-z] は、任意の小文字を表します。

[A-Za-z0-9*&#]

これは、任意の文字 (大文字または小文字)、任意の数字、アスタリスク、アンパサント、または # と一致する正規表現の式です。

以下に示した入力テキストを与えられると、上記の仕様を規則の 1 つとして持つ字句アナライザは、*&r# を認識して、規則が指定するアクションをその認識した各文字に対して実行し、残りのテキストをそのまま出力します。

$$$$?? ????!!!*$$ $$$$$$&+====r‾‾# (( 

ハイフン文字をクラスに加えるには、[-A-Z] または [A-Z-] のように、角括弧内の最初または最後の文字として入力してください。

演算子は、組み合わせて使用すると非常に効果的です。たとえば、複数のプログラミング言語の識別子を認識するための正規表現は、次のようになります。

[a-zA-Z][0-9a-zA-Z]*

これらの言語における識別子は、文字の後にゼロ、文字、または数字が続く文字列として定義されており、上記の正規表現は、その識別子を表しています。 1 組目の角括弧は、任意の文字に一致します。2 組目の角括弧は、その後の * がなければ、任意の数字または文字に一致します。

2 つの角括弧の組とそれに囲まれた文字は、後ろに 1 個の数字または文字が続く任意の文字と一致します。しかし * が付いているため、この例は後ろに任意の数の文字または数字が続く任意の文字と一致します。たとえば、この例は以下の文字列を識別子として認識します。

e 
not 
idenTIFIER 
pH 
EngineNo99 
R2D2

一方、以下の文字列は識別子として認識されません。not_idenTIFIER には下線が含まれており、5times は文字でなく数字で始まっており、$hello は特殊文字で始まっているためです。

not_idenTIFIER
5times 
$hello 

演算子文字に潜在する問題は、検索する文字としてどのようにその演算子文字を検索パターン内で指定するかということです。たとえば、前述の例は * を含むテキストを認識しません。lex では、2 つの方法のいずれかでこの問題を解決します。バックスラッシュの後の演算子文字、二重引用符で囲まれた文字 (バックスラッシュは除く) は、そのまま文字として扱われます。つまり、検索されるテキストの一部となります。

たとえば、バックスラッシュによる方法を使用して、* の後に任意の数の数字が続く文字列を認識するには、以下のパターンを使用できます。

¥*[1-9]*

¥ 自体を認識するには、バックスラッシュを 2 つ並べる必要があります (¥¥)。同様に、"x¥*x"x*x と一致し、"y¥"z"y"z と一致します。その他の lex 演算子については、表 2-1 で説明しています。

表 2-1 lex 演算子

式 

説明 

¥x

x が lex 演算子の場合は x

"xy"

x または y が lex 演算子 (¥ は除く) の場合でも xy

[xy]

x または y 

[x-z]

x、y、または z 

[^x]

x 以外の文字 

.

復帰改行以外の文字 

^x

行の先頭の x 

<y>x

lex が開始状態 y にあるときは x

x$

行の最後の x 

x?

0 個または 1 個の x 

x*

0 個以上の x 

x+

1 個以上の x 

x{m,n}

m 〜 n 個の x 

xx|yy

xx または yy 

x |

x に対するアクションは、次の規則のアクション 

(x)

x/y

x の後に y が続く場合にのみ x 

{xx}

定義セクションからの xx の変換 

アクション

スキャナは、規則の開始部にある正規表現に一致する文字列を認識すると、その規則の右側を検査してアクションを実行します。ユーザーは、このアクションを指定します。

見つかったトークンの型とその値を記録する、トークンを別のトークンと置換する、トークンまたはトークンの型のインスタンス数 (出現する数) をカウントする、などがアクションとして考えられます。これらのアクションは、C のプログラムとして作成します。

アクションは、任意の数の文を使用して構成できます。何らかの方法でテキストを変更したり、テキストが見つかったことを示すメッセージを出力したい場合があります。たとえば、式 Amelia Earhart を認識して、認識したことを通知するには、以下の規則を適用します。

"Amelia Earhart" printf("found Amelia"); 

テキスト内の長い医学用語を同義の略語に置換する処理は、以下のような規則で実現できます。

Electroencephalogram printf("EEG"); 

テキスト内の行数をカウントするには、行の終わりを認識して、行カウンタを増分します。

lex では、復帰改行を表す ¥n などの、標準の C エスケープシーケンスを使用します。たとえば行数をカウントするには、以下のような構文を使用できます。ここで使用している lineno は、他の C 変数と同じようにして、「定義」で宣言されています。

¥n  lineno++; 

C 言語の NULL 文 (1 個のコロン ;) を指定すると、入力は無視されます。したがって、以下の規則を使用した場合には、空白文字、タブ、復帰改行は無視されます。

[ ¥t¥n] ; 

また、択一演算子 | は、ある規則とその次の規則のアクションが同じであることを表す際に使用することもできます。たとえば、前述の例は以下のように記述しても同じ結果になります。

" " | 
¥t
¥n ;

スキャナは、式と一致したテキストを yytext[] という文字配列内に格納します。必要に応じて、この配列の内容を出力したり操作できます。実際に、lexprintf ("%s", yytext) と同義の ECHO というマクロを備えています。

C 言語の長い文または複数の文でアクションを構成する場合には、その文を複数の行に渡って記述できます。アクションが、1 つの規則だけに対するものであることを lex に伝えるには、その C コードを中括弧で囲みます。

たとえば、数字列が見つかるごとにその数字列とそれまでに見つかった数字列の総数を出力しながら、入力テキスト全体の数字列の総数をカウントするには、次のような lex コードを使用します。

¥+?[1-9]+      { digstrngcount++; 
                 printf("%d",digstrngcount); 
                 printf("%s", yytext); }

? は、その直前の文字が 0 個または 1 個存在することを表しているので、この仕様は 0 個または 1 個のプラス記号が直前に付いた数字列と一致します。また、負の数字列の場合でも、マイナス記号の後の部分はこの仕様に一致するので、同様に数値が捕捉されます。