boost::Xpressive を使ってみた。

Web 製作にあたって必要になった「C++ソースコード<, >, &文字参照しつつ、コメントを <span class=comment>〜</span> で修飾」するプログラムを、あえて perl では書かずに C++ で書いてみた。
というのも boost::Xpressive という便利ライブラリが存在することは前々から知っていたので、そのテストの切っ掛けとして丁度いいと思ったからである。


べ、別に perl を使おうと思ったら PC にインストールされてなかった、なんてアホな理由じゃないんだからね!?




コホン。というわけで、ソースをどうぞ:

/* cpp2html.cpp */
#include <iostream>
#include <string>
#include <boost/xpressive/xpressive.hpp>
#include <boost/algorithm/string.hpp>

int main()
{
	using namespace std;
	using namespace boost::xpressive;
	using namespace boost::algorithm;

	string buf, str;
	while( getline( cin, str ) )
	{
		// 文字参照
		replace_all( str, "&", "&amp;" );
		replace_all( str, "<", "&lt;" );
		replace_all( str, ">", "&gt;" );

		buf += str + '\n';
	}

	// コメントをマークアップ

	sregex c_comment = "/*" >> -*_ >> "*/",	// C コメント
		cpp_comment = "//" >> *(~_n);	// C++ コメント

	// 複合コメントは一括で
	sregex comment = ( c_comment | cpp_comment ) >> *( *space >> ( c_comment | cpp_comment ) );

	// コメントの置き換え
	cout << regex_replace( buf, comment, string("<span class=comment>$&</span>") );

	return 0;
}

コンパイル&実行すると・・・

>g++ cpp2html.cpp

>a < cpp2html.cpp
<span class=comment>/* cpp2html.cpp */</span>
#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;boost/xpressive/xpressive.hpp&gt;
#include &lt;boost/algorithm/string.hpp&gt;

int main()
{
        using namespace std;
        using namespace boost::xpressive;
        using namespace boost::algorithm;

        string buf, str;
        while( getline( cin, str ) )
        {
                <span class=comment>// 文字参照</span>
                replace_all( str, "&amp;", "&amp;amp;" );
                replace_all( str, "&lt;", "&amp;lt;" );
                replace_all( str, "&gt;", "&amp;gt;" );

                buf += str + '\n';
        }

        <span class=comment>// コメントをマークアップ</span>

        sregex c_comment = "<span class=comment>/*" &gt;&gt; -*_ &gt;&gt; "*/</span>",  <span class=comment>// C コメント</span>
                cpp_comment = "<span class=comment>//" &gt;&gt; *(~_n); // C++コメント

        // 複合コメントは一括で</span>
        sregex comment = ( c_comment | cpp_comment ) &gt;&gt; *( *space &gt;&gt; ( c_comment | cpp_comment ) );

        <span class=comment>// コメントの置き換え</span>
        cout &lt;&lt; regex_replace( buf, comment, string("&lt;span class=comment&gt;$&amp;&lt;/span&gt;") );

        return 0;
}

とまぁ、途中に少々ダメな点はあるが、おおむね満足いく結果になった。
ダメな点を改良することも無論できるが、少々面倒な作業になるので今回は省略する。


さて、解説であるが、まず前半部分は単純な置き換えを行っているだけだ。
ここで、replace_allboost::algorithm の関数であり、Xpressive ではないので注意。
勿論、ここでは replace_all など使わず、一文字ずつ標準出力から受け取って switch 文で分岐していってもいい。むしろ今回のような単純な例では、そちらの方が効果的であろう。
ただ、今回は boost での文字列処理に慣れるという意味で、こちらを使ってみた。


さて肝心の後半部分で、Xpressive が使用されている。
ここですることは、まず正規表現のルールを指定する sregex オブジェクトを作ることだ。


ここでは、CコメントのルールとC++コメントのルールが、まず指定されている。
Cコメントに関しては「まず "/*" があって、出来るだけ短い任意の文字の可変個の並びが続いて、"*/" で終わる」というルールが Xpressive 流に書かれている。
C++コメントに関しては「まず "//" があって、改行を含まない任意の文字の可変個の並びが続く」というルールである(「改行で終わる」としていないのは、タグの関係上、改行を含めると閉じるタグが次の行の頭に入ってしまうから。最後の改行を指定しなくても、Xpressive は最長マッチがデフォなので、ちゃんと改行まで認識してくれる)。
なおこれらは perl っぽく書き表すことも出来て、それぞれ

sregex c_comment = sregex::compile("/\\*.*?\\*/"),	// C コメント
	cpp_comment = sregex::compile("//[^\\n]*");	// C++ コメント

こんな感じに書いてもよい。C++文字列なので \ 記号が重複している以外は、極めて真っ当な正規表現であるのが分かるだろう。


次に、複数行連続でコメントアウトした場合などに余分なタグが挟まれるのを消すため、コメントが空白(改行を含む)を挟んで連続している場合に一つのコメントとみなすような正規表現ルールを指定し、これに comment という名前をつけている。
このように「正規表現ルールの中に、別の正規表現ルールを組み込む」という使い方は、Xpressive 独自のものである(らしい。よく知らないので^^;)。


そして、最後に regex_replace でコメントをマークアップする。
regex_replace 関数は、第一引数に置換対象の文字列全体、第二引数に実際に置換を行う部分の正規表現、そして第三引数に正規表現にマッチした部分が置き換えられる文字列を指定すればよい。
ここで第三引数であるが、一見不要な string への変換は必須であるので注意すること。
変換をしない場合は、意味不明なエラーメッセージをはいてコンパイルに失敗する。
また、書式については、「 $& と書いた部分にマッチした文字列が置き換えられる」約束になっている。
もっと複雑な処理もできるらしいので、機会があったら調べてみたい。
あ、ちなみに、ここでは regex_replace の結果をストリームに流しているが、実は結果は string オブジェクトであるので、そのまま適当なオブジェクトに格納することも可能である。


さて、こんな感じで書いたわけであるが・・・個人的に、どうしても気に食わない部分がある。
それは、前半部だ。こんな単純な置き換え、いちいち replace_all なんて関数を使う必要、果たしてあるのか?
かといって、C言語的に

ostringstream buf;
char c;
while( cin.get(c) )
{
	switch(c)
	{
	case '<':
		buf << "&lt;";
		break;

	case '>':
		buf << "&lt;";
		break;

	case '&':
		buf << "&amp;";
		break;

	default:
		buf << c;
		break;
	}
}

ってやるのも、どうよ、って感じである。もちろん高速ではあるが。
そこで、これをきれいに書ける方法を、模索してみたい。