字句解析器生成系Flexの正規表現で、Onigurumaと同じUnicodeプロパティを使えるようにしてみる
ところで、最近のさまざまな言語仕様において、「Unicodeプロパティ」や「Unicodeカテゴリ」と呼ばれるものが使われるようになっている。細かい定義は知らないが、例えばECMAScript Language Specificationを例にとると、
UnicodeLetter
any character in the Unicode categories “Uppercase letter (Lu)”, “Lowercase letter (Ll)”, “Titlecase letter (Lt)”,“Modifier letter (Lm)”, “Other letter (Lo)”, or “Letter number (Nl)”.
と書かれている箇所がある。この、"Lu"などがUnicodeカテゴリだ。
さらに、Unicodeプロパティと呼ばれるものは、例えば「Hiragana」というものがある。文字通り「ひらがな」の文字の範囲を表している。Onigurumaという正規表現エンジンにおいては、このUnicodeプロパティ/Unicodeカテゴリをパターンとして指定することが可能だ。\p{property-name}
という構文で利用できる。\p{Hiragana}
と書けば、ひらがな1文字にマッチさせることが可能だ。
さて、flexはレキサージェネレーターと呼ばれて構文解析器を作るときの字句解析のエンジンを生成してくれるソフトウェアだ。だいたいパーサジェネレーターのbisonとペアで使われる。正規表現を用いて文字列を切り出し、状態遷移も使って字句解析を行うことが出来る。
しかし、この正規表現エンジンは取り替えることができないため、Flexに付属の古いものを使わざるを得ない。ECMAScriptやR6RSなどの近代的な(?)言語仕様の実装にはUnicodeカテゴリ/Unicodeプロパティの仕様が必須なため、これは不便だ。
というわけで、Flexの正規表現内でOniguruma的なUnicodeプロパティ/Unicodeカテゴリの指定ができるように(やや強引に)変換するスクリプトを作成した。
原理は簡単で、プロパティ/カテゴリの正規表現を、文字の羅列に変換している。ちなみに入力はUTF-8を前提としている。
#!/usr/bin/ruby # codepoint.rb UnicodeCategories = %w{ C Cc Cf Cn Co Cs L Ll Lm Lo Lt Lu M Mc Me Mn N Nd Nl No P Pc Pd Pe Pf Pi Po Ps S Sc Sk Sm So Z Zl Zp Zs } UnicodeProperties = %w{ Arabic Armenian Bengali Bopomofo Braille Buginese Buhid Canadian_Aboriginal Cherokee Common Coptic Cypriot Cyrillic Deseret Devanagari Ethiopic Georgian Glagolitic Gothic Greek Gujarati Gurmukhi Han Hangul Hanunoo Hebrew Hiragana Inherited Kannada Katakana Kharoshthi Khmer Lao Latin Limbu Linear_B Malayalam Mongolian Myanmar New_Tai_Lue Ogham Old_Italic Old_Persian Oriya Osmanya Runic Shavian Sinhala Syloti_Nagri Syriac Tagalog Tagbanwa Tai_Le Tamil Telugu Thaana Thai Tibetan Tifinagh Ugaritic Yi } DataFile = File.dirname(__FILE__) + '/UnicodeData.txt' UnicodeData = open(DataFile) { |f| f.readlines } def main input = $stdin.read mapping = { } UnicodeCategories.each do |cate| input.gsub! /\\p\{#{cate}\}/ do if mapping[cate] mapping[cate] else STDERR.puts "expanding '#{cate}' ..." target_lines = UnicodeData.select { |d| d.split(';')[2] =~ /#{cate}/ } mapping[cate] = process_lines(target_lines) mapping[cate] end end end UnicodeProperties.each do |prop| input.gsub! /\\p\{#{prop}\}/ do if mapping[prop] mapping[prop] else STDERR.puts "expanding '#{prop}' ..." target_lines = UnicodeData.select { |d| d.split(';')[1] =~ /#{prop}/i } mapping[prop] = process_lines(target_lines) mapping[prop] end end end print input end def process_lines(lines) codepoints = lines.map{ |ln| ln.split(';')[0].hex } codepoints.map! do |code| if 0000_0000 <= code && code <= 0x0000_007F '\x%02x' % code elsif 0x0000_0080 <= code && code <= 0x0000_07FF lower = (code & 0x3F) | 0x80 higher = (code >> 6) & 0x1F | 0xc0 '\x%02x\x%02x' % [higher,lower] else lower = (code & 0x3F) | 0x80 middle = (code >> 6) & 0x3F | 0x80 higher = (code >> 12) & 0xF | 0xe0 '\x%02x\x%02x\x%02x' % [higher,middle,lower] end end "(" + codepoints.join('|') + ")" end main
実行には、Unicodeの文字コードデータベースであるUnicodeData.txtが必要なので、ダウンロードして欲しい。
さて、サンプルのFlexファイルを作成してみる。今回は超最低限のファイルを使って実験してみる。
これは、ひらがなだったらその文字を表示・それ以外は無視というスクリプトだ。
/* hiragana.l */ %% \p{Hiragana}+ { printf("> %s\n", yytext); } . { } %%
最低限過ぎる。
使い方は、この内容を標準出力でフィルタして、flexコマンドにパイプで渡してやればよい。
$ cat hiragana.l | ruby codepoint.rb | flex # lex.yy.cというCソースファイルが生成されているので、これをコンパイル・実行する $ gcc lex.yy.c -lfl -o hiragana # 実行結果 $ ./hiragana 字句解析器生成系Flexの正規表現で、Onigurumaと同じUnicodeプロパティを使えるようにしてみる > の > で > と > じ > を > えるようにしてみる
ひらがなが入力されたときだけエコーバックされている。とりあえず成功っぽい。
改良の余地は多ので、精進します。