このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

エスケープ祭り、バックスラッシュの嵐

文字列リテラル内に登場する二重引用符そのものやバックスラッシュそのものは、エスケープしないとまずいですよね。
内容:

  1. 文字列リテラルを置換しよう
  2. 正規表現の復習
  3. シェルコマンドラインエスケープ
  4. シェルコマンドラインをsystem関数に渡す

●文字列リテラルを置換しよう

架空の例を出します。でも、あまりにたわいもない例だとつまらないので、次のストーリーを想定しましょう。

Cプログラムをgettextを使って国際化するときは、国際化/地域化したい文字列リテラルを適当なマクロ(N_とか)で囲みます。例えば、N_("hello") のように。これを機械的にやるとえらいことになります。例えば、


#include N_("myheader.h")
がマクロ展開されたら変なことになるでしょう。

ですが、「まーとりあえず全部置換してしまえ」ってことで(いいんかそれで?)、GNU sedを使って“文字列リテラルに対して文字列置換”をすることにしました。(と、そういう想定です。)

正規表現の復習

C(多くの他の言語でも似たり寄ったり)の文字列リテラルは二重引用符ではじまり二重引用符で終わります。これを正規表現にすると".*"ですね。この正規表現をもとに、目的の置換を行うsedスクリプトを書くと(細かいことはいいとして)次のようになります。


s/\(".*"\)/N_(\1)/g
このスクリプトを使って置換すると、greet("Akiko", "hello");greet(N_("Akiko", "hello")); になっちゃうんですよ。sedは一番長い文字列に一致させようとするので、中間の二重引用符も踏み越えてマッチに走ります。

それでは、"[^"]*" ではどうでしょう。これもダメですね。"Wow, \"Great\"" という文字列リテラルN_("Wow, \")Great\N_("") になってしまいます。文字列のなかにバックスラッシュでエスケープされた二重引用符が登場する可能性があります。

文字列のなかに現れる普通の文字は、二重引用符でもバックスラッシュ(円記号に見えるかもしれません^^;)でもない文字です。エスケープされた表現は、バックスラッシュ+1文字と考えてよいでしょう。

  • 普通の文字 [^"/] (ブラケット内では1つのバックスラッシュ)
  • エスケープされた表現 \\. (ブラケットの外では2つのバックスラッシュ)

このどちらかですが、「どちらか」を表すメタ記号は、GNU sedでは \|です。「ちょっとややこしいぞ、Emacs固有の正規表現」に似ています。グループ化の丸括弧も\( \)です。正規表現の方言にはウンザリしますが、まーしょうがない。それで、sedスクリプトは:


s/\("\([^"\]\|\\.\)*"\)/N_(\1)/g

実は、\041とか\x21(共に27番の文字を表す)では、意味的には4個の文字がひとかたまりですが、41や21を普通の文字と解釈しても結果オーライです。

●シェルコマンドラインエスケープ

sedスクリプトを別ファイルにしなくても、-eオプションでコマンドラインスクリプトを指定できます。


$ sed -e s/\("\([^"\]\|\\.\)*"\)/N_(\1)/g myprog.c
これはダメです。二重引用符やバックスラッシュをシェルも解釈して混乱します。スクリプト部分を一重引用符で囲むのが簡単な解決策です。

$ sed -e 's/\("\([^"\]\|\\.\)*"\)/N_(\1)/g' myprog.c

さて、あえてスクリプト部分を二重引用符で囲んだらどうなるでしょう。正規表現内に含まれる二重引用符はエスケープしなくてはなりません。バックスラッシュはエスケープしなくてもうまくいくところ(例えば、最初のバックスラッシュ)もありますが、正規表現内の\\をシェルが\に変更したりするので、すべてのバックスラッシュをエスケープして二重バックスラッシュにしておいたほうが安全です。その結果:


$ sed -e "s/\\(\"\\([^\"\\]\\|\\\\.\\)*\"\\)/N_(\\1)/g" myprog.c
と、こうなります。

●シェルコマンドラインをsystem関数に渡す

Cのライブラリのなかにsystemという関数があり、引数に渡された文字列をシェルにより実行してくれます。例えば、次は一種のHelloWorldプログラムです。


int main(int argc, char** argv)
{
system("echo 'Hello, world.'");
}

先ほどのコマンドラインをsystem関数に渡してみましょう。systemの引数はCの文字列リテラルになります。当然にまた、バックスラッシュと二重引用符はエスケープしなくてはなりません。


/* dosed.c */

int main(int argc, char** argv)
{
system("sed -e \"s/\\\\(\\\"\\\\([^\\\"\\\\]\\\\|\\\\\\\\.\\\\)*\\\"\\\\)/N_(\\\\1)/g\" myprog.c");

}

ふぃー、できた。バックスラッシュが8個並んでいるところもあるなぁー。