Super Technique 講座

m4 チュートリアル

m4 はUNIXの標準コマンドの1つであり、古い歴史を持つマクロプロセッサである。しかし、やや使い方が難しく、しかも古典度が高く専門的なために、どうしても紹介のプライオリティが下がる傾向があって、日本語でマトモに書かれた解説にお目にかかったことがない。そこで、m4 に多少の経験値がある筆者があえて m4 のチュートリアルを書いて見せる。基本的な情報は m4 の info から仕入れており、それにいろいろな実例を加えて書いている。


マクロプロセッサ m4 とは?

m4 はマクロプロセッサである。つまり、Cプリプロセッサ cpp (今時だと gcc -E で起動するのが良かろう)のようなものだが、次のような cpp にはない特徴がある。

  1. cpp は扱うテキストがC言語で書かれていることを期待する。つまり、空白と改行はターミネータに過ぎず、任意に空白や改行が付け加わっても問題がないという前提で動作する。現実には cpp はコメントを削除し、行番号を付与し、マクロディレクティブ行と、置換結果が空になる行は、空行として維持される。つまり、いろいろとやりすぎているのである。
  2. cpp が「単語」として認識するのはC言語の「単語」である。だから、「Test#4」のようなマクロを定義できるわけがない。
  3. cpp には、組み込みのマクロがいくつか存在している。一番有名なのは「unix」というマクロである。これは処理系が UNIX のものの場合には「1」である。もし、通常のテキストに対して何かを置換するためにcpp を実行し、その中に不幸にして「unix」という文字が含まれていたらどうなるだろう?
    これは gcc の実装ではドライバからオプションのかたちで unix などの非ANSIマクロが供給されているのであり、通常の Intel 386 Linux では、次のような起動オプションである。
    -D__ELF__ -Dunix -Di386 -D__i386__ -Dlinux -A system(posix)
    

    つまり、Intel 386 Linux の gcc では、「__ELF__」「unix」「i386」「__i386__」「linux」の5つの非標準マクロが定義されてプリプロセスが行われているのである。当然、これらの単語が含まれたC言語ソース以外のテキストを処理した場合、すべて「1」に化ける(まあ、これを回避するためには、cpp -u か、gcc -E -undef のオプションで起動する必要がある)。

つまり、これらの特徴から、行指向のテキストは本質的に cpp では処理できないし、C言語の通常の名前キャラクター(a-zA-Z0-9_)以外の文字を含む部分を置換することもできないことになる。これは「汎用的なマクロプロセッサ」として cpp を使うのは大変難しいことを示している。

そこで「汎用的なマクロプロセッサ」として用意されているのが m4 なのである。m4 の特徴は cpp とは対照的である。

  1. cpp ではマクロは1行のものしか定義できないのは周知のことである。強引に長いマクロを書くことはできるが、その場合でも改行を含むことはできない。しかし、C言語では改行は本質的ではないために、これは問題にはならない。だが、改行が本質的である言語のソースの場合には、どうしても改行をマクロに含みたいケースがある。cpp はこれには対応できないが、m4 はこれに対応できる。つまり、マクロ定義の中に改行コードを含みうる。
  2. cpp ではトークンはC言語の定義をそのまま流用している。だから、トークンとして使えるのは [a-zA-Z_][a-zA-Z0-9_]* の正規表現で示されるシーケンスだけである。しかし、m4 は任意の文字セットをトークンとして再定義できる仕様がある。だから、トークンの定義を TeX 風に「\」で始めるとか、sh 風に「$」で始めるとか、そういうこともできる(ただし、これをするためには m4 の再コンパイルが必要だが...)。
  3. m4 はさまざまな置換に使える有用なの「組み込み関数」を持っている。
  4. しかし、それらの組み込み関数でさえ、プレフィックスによる置換の禁止を設定することができる。だから、偶然の名前バッティングを有効に回避することができる。
  5. さまざまな仕様が良く考えられており、再帰定義を活用することでループを実現することさえできる。

これらの特徴が、m4 を「汎用的マクロプロセッサ」として有益なものにしている。それゆえ、プログラマが m4 マクロを自分で記述することはそれほどにはなくても、実は UNIX のさまざまな便利ツールの中で m4 は活用されているのである。たとえば...

autoconf
今時これがなくては、マルチプラットフォームのコンパイル&インストールができなくなっている、と言っても過言ではないのが autoconf である。一般ユーザは autoconf の結果である「./configure」を起動するに過ぎないが、実はこの「./configure」を作るためのコマンドである autoconf は、m4 を使い倒している。実際には configure.in というファイルを作成して、それを処理して ./configure を作成するツールが autoconf である。configure.in は次のようなマクロが記述され、
AC_INIT(common.h)
AC_CHECK_PROG(LDCONFIG,ldconfig,"ldconfig")
AC_SUBST(LDCONFIG)
AC_CONFIG_SUBDIRS(src tool plug etc texinfo)
AC_OUTPUT(Makefile)
これがシェルスクリプト ./configure に変換される(中身は膨大なので、手元の適当な ./configure を参照されたい)。インストールのしたいユーザは、この ./configure を実行すると、実行環境を調べて適切な Makefile を Makefile.in から作り出す... という寸法だ。

だから、凝った configure.in を書くことになると、ちょっとばかり m4 の挙動を知っておいた方が良いことにもなる。

CF
これはサーバ管理者への福音ツールである。/usr/sbin/sendmail は古い歴史を持ち、古いソフトにありがちなのだが、設定ファイル /etc/sendmail.cf は1文字オプションかつ奇怪な文法でサーバ管理者を悩ませ続けている。だってねえ、今時でさえ uucp 関連の詳細なオプションがテンコ盛りなんだよ! 独自の sendmail.cf をフルスクラッチで書こうとすると、「sendmail 詳説」という分厚い本を首っ引きにせざるをえないわけだ。しかし、メールサーバの設定なんて9割以上は定型的なものに過ぎない。こんな難解な sendmail.cf で頭を悩ますのはバカバカしい... ということで登場したのが、sendamil の簡易設定ツールである CF である。CF は人間に判りやすい設定ファイルから sendmail.cf を自動生成する。難解な sendmail.cf 文法とはオサラバだ! という舞台裏で、人間に判りやすい設定ファイルをマクロ置換して sendmail.cf を生成しているのが m4 のわけである。

さまざまなツールのコンパイルの一環として
フツーの UNIX(特にフリー系) に入っているのは多分 GNU の GNU m4 だろう。だから、GNUの各種プロダクトでは「どうせ自分のとこで開発してる奴だから...」という理由で、それらのプロダクトのコンパイルのために、陰でけっこう m4 を活用している。まあ、これは自前でそういう GNUツールをコンパイルしてみないと判らないが、gcc などでもそのコンパイルには m4 を使っている。

というわけで、m4 は表にはなかなか出ないシャイなツールだが、縁の下でずいぶん力を発揮している。こういう便利ツールはちょっと知っておくと便利であり、意外な活用法もあるわけだ。また、m4 を知っているとなるといかにも UNIX ウィザード臭いわけで、周囲の UNIX 系プログラマから尊敬の目で見られる(のではないかな? 苦笑)。

しかし、「尊敬の目で見られる(?)」のは、やはり m4 がちょっとばかり難しいという理由もある。しかもこういう地味なツールのマニュアル翻訳はどうしても後回しになって、特に日本語ではマトモなチュートリアルさえない、という状況が長く続いている(まあ、勉強する奴は英語のドキュメントを読むわなあ)。ここでちょっとばかりチュートリアルを書いてみるのも、m4 のような便利ツールの普及に少しは力になるのではなかろうか。

ちなみに「GNU Autoconf/Automake/Libtool」(オーム社、Vaughan,Elliston,Tromey,Taylor共著)にちょっとだけ(15ページだけ) m4 の解説があるが、筆者が日本語の解説を見たのはこれ位なものだ...だから、このページってなかなか希少価値があるようで、アクセス数に比較してリンクを張られる頻度が高いようだ。よしよし。


m4 の使い方

要するに m4 はテキストツールである。つまり、

% m4 [options] files....

のかたちで、引数として取られたファイルをマクロ置換した結果を標準出力に吐き出す。オプションの主要なものには次のものがある。

--help, --version
言うまでもなく、ヘルプとバージョンを表示する。
-Q, --quiet, --silent
ワーニング出力を抑制する。シェルスクリプト用だな、要するに。
-P, --prefix-builtins
これは重要。組み込みマクロなどは、デフォルトでは統一された名前ではないが、このオプションをつけるとすべての組み込みマクロは m4_ で始まる名前であると解釈される。これによって、不用意な置換を防止できる。
-I, --include=DIRECTORY
以下3つは cpp のオプションとしてもあるもの。これはインクルードするディレクトリを追加する。
-DNAME[=VALUE], --define=NAME[=VALUE]
コマンドラインからマクロを定義するアレである。スクリプトから利用する場合にスイッチとして使えて、効果絶大である。
-UNAME, --undefine=NAME
-D の逆操作で、定義されているマクロをコマンドラインから未定義にする。
-s, --synclines
#line 1 "filename"」の要領でファイル名を挿入してくれる。複数ファイルの時に重宝する。

まあ、その他にもあるが、--help ででも見てくれたまえ。また、ファイル名に「-」を使うと、標準入力から入力を受け付ける(GNUツールにはよくあるパターンだな)。


m4 のディレクティブ

さて、本題。m4 には組み込みのディレクティブ(要するに cpp だと #define とかの機能)がいくつもある。これらは cpp のものよりも汎用的に使えるのでしっかりと解説しよう。しかし、残念なことがある。それは日本語コードはちゃんと認識せず、基本的に最上位ビットが立っているとコケる。だから日本語のファイルに対して m4 を使う場合には、JIS に変換して使うのがよかろう(まともに対応する奴はいないのか?)。


マクロ定義に関するディレクティブ

まず解説の一番手はマクロを定義するディレクティブである。要するに cpp の「#define」に相当するのは、やはり「define(Name[, Value])」である。マクロを定義し、以降にそのシンボルが登場した時にマクロとして展開する。たとえば、次の通り。

define(`X',`*Macro defined at Xwin*')
XFree86 is a free version of the X Window.
X is a standard Window System of UNIX. 

これを「m4 test.m4」の要領でマクロ展開すると、次のようになる。

空行!
XFree86 is a free version of the *Macro defined at Xwin* Window.
*Macro defined at Xwin* is a standard Window System of UNIX. 

もし、cpp だとどうなるだろう? 比較してみよう。

#define X *Macro defined at Xwin*
XFree86 is a free version of the X Window. 
X is a standard Window System of UNIX. 
# 1 ""

XFree86 is a free version of the *Macro defined at Xwin*  Window.
*Macro defined at Xwin*  is a standard Window System of UNIX. 

「何だ、ほとんど同じじゃん!」なんて言わないで欲しい。まず余計な行表示は入らない。また並べて見るとよくわかる。

XFree86 is a free version of the *Macro defined at Xwin* Window. m4
XFree86 is a free version of the *Macro defined at Xwin*  Window. cpp
*Macro defined at Xwin* is a standard Window System of UNIX. m4
*Macro defined at Xwin*  is a standard Window System of UNIX. cpp

つまり、cpp は展開されたマクロの直後に余計な空白が一文字入っているのである。m4 は正しく空白を処理し、余計な空白や改行を追加しない。m4 のやり方の方が汎用的なのだ。

さて、ここでトークンとクォート規則について注釈しよう。m4 はマクロ展開言語なので、トークンを展開して出来たトークンについて、更にマクロが定義されていた場合には二次的に展開を行う。つまり、

define(`test',`W')
define(`X',`test ')
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 

のケースでは、「X」というトークンは、一旦「test」に展開された後、更に「W」に展開され直す。つまり次のようになる(もうマクロ定義が空行に化けるのはいいよな...省略する)。

XFree86 is a W version of the W Window.
W is a standard Window System of UNIX. 

まあ、ここらへんの感覚は cpp と同じである。m4 のトークンの定義は「letter, digit, _」で最初の文字が「digit」ではないものであるとされている。これはC言語の変数名と同じであり、2byte文字は使えない(JISコードにしてもね)。当然、大文字小文字は区別される。

ということは、次のようなこともできる。

define(`X',`test of')
define(`test',`W')
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 
→  ちょっと行を節約しよう!
XFree86 is a W version of the W of Window.
W of is a standard Window System of UNIX.

まあ、これは驚くようなことでもない。cpp だって似たようなマクロは頻繁に書かれるのである。しかし、cpp ではいわゆる「トークン連結演算子」と呼ばれる「##」を使って、作成したトークンを連結する。m4 ではこれはない。その代わり、次のようなことができる。

define(`X',`W')
define(`WFree86',`MyWindow')
indir(`X')Free86 is a test version of the X Window.
→
MyWindow is a test version of the W Window.

indir() は、強制的にマクロの置換を実行する。だから、indir(`X')W→(WFree86と見なされて)→YWindow という連鎖で置換が進行しているのである。これを使えば、トークンを結果として連結できることになるのである。

また、改行を含むマクロは単に次のように素直に改行を入れるだけのことである。要するにリテラル文字列の定義が、改行コードを含んでも良く、とにかく「'」まで執念深くリテラルとして追っていくのだ。これは改行コードが本質的である言語(古典的Basic とか /bin/sh とか awk とかね)を処理する場合に大変都合が良い。

define(`X',`Big NEWS...
NeWS')
X is a Window System for UNIX.
→
Big NEWS...
NeWS is a Window System for UNIX.

マクロ置換の原則

しかし、一旦置換された内容などを更に置換することを避けたい場合がある。つまり、展開された内容は「タダの文字列であって、トークンではない」ことを m4 に伝えれば良いのである。文字列シーケンスを「トークンでなくする」機能が「クォート」である。

m4 のクォートはバッククォートとシングルクォートをペアにして使う(C言語風ではなくて、一般英文風である)。今まで使ってきたように「`X'」などのクォートは、これを単なる文字列シーケンスとして取り扱い、マクロ展開をその時点では拒んでいるのである。これをテストしてみよう。

define(`X',`is')
define(X,`W')  → 結果として define(`X',`W'), define(`is',`W') を定義
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 
→
XFree86 W a test version of the W Window.
W W a standard Window System of UNIX. 
define(`X',`is')
define(`X',`W') → define(`X',`W') だけが定義される 
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 
→
XFree86 is a test version of the W Window.
W is a standard Window System of UNIX. 

それゆえ、define で定義されるマクロ置換対象であるトークンは、クォートしておくのが無難というものである。クォートしないと、意図しない副作用がありうるのである。逆に置換されるテキストの方のクォートは、置換されたときにクォートが消滅することになる。だから、一旦置換された対象に対して更に置換がなされるのである。これを防ぐためには「二重にクォートすれば良い」という結論になる。

define(`X',`test')
define(`test',`new')
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 
→
XFree86 is a new version of the new Window.
new is a standard Window System of UNIX. 

上のケースでは X が置換された結果である test も元のテキストにあった X のどちらも new に置換されている。しかし、二重にクォートした下のケースでは、X が置換された結果である test では一重のクォートが生き残り、そのために置換を拒んでそのまま残るのである。

define(`X',``test'')
define(`test',`new')
XFree86 is a test version of the X Window.
X is a standard Window System of UNIX. 
→
XFree86 is a new version of the test Window.
test is a standard Window System of UNIX. 

ということは逆に、地のテキストでもクォートを使うことで臨時に置換を拒むことができる。

define(`X',`test')
XFree86 is a test version of the X Window.
`X' is a standard Window System of UNIX. 
→
XFree86 is a test version of the test Window.
X is a standard Window System of UNIX. 

クォート自体を生き残らせるためには、当然次のようにする。

define(`X',`test')
XFree86 is a test version of the X Window.
``X'' is a standard Window System of UNIX. 
→
XFree86 is a test version of the test Window.
`X' is a standard Window System of UNIX. 

マクロ引数

cpp でマクロに引数が取れるように、m4 でも引数付きマクロが定義できる。ただし、やり方はずいぶん違い、若干 /bin/sh 風であり、不定長引数なども実現できる。

cpp では関数風に仮引数を与えてマクロを定義するが、m4 は /bin/sh 風(てっか TeX に似てるな... TeXマクロも置換マクロ言語だよな)であり、引数に「$1,$2,...」といった番号を与えてアクセスする。つまり、次のようである。

define(`pair',`$1 => $2')
pair(`1st',`Apple')
pair(`2nd',`Orange')
pair(`3rd',`Banana')
→
1st => Apple
2nd => Orange
3rd => Banana

余計な引数がある場合は無視されるし、引数が足りない場合は空文字列になる。引数個数の不一致では、特に警告は生成されない。

define(`pair',`$1 => $2')
pair(`1st',`Apple',`Computer')
pair(`2nd')**
pair()**
→
1st => Apple
2nd => **
 => **

その他、「$0」は、そのマクロの名前を示す(/bin/sh風)。また、「$#」が引数の個数を、「$*」と「$@」がすべての引数を「,」で連結したものを示す(これも /bin/sh 風)。

$*」と「$@」の違いは、クォートの扱いの違いである。「$*」は積極的にクォートを外してトークンとして評価する。だから、次のようになる。

define(`echo1',`$*')
define(This,That)
echo1(`This',`is',`a',`pen')
echo1(``This'',`is',`a',`pen')
→
That,is,a,pen
This,is,a,pen

それに対して、「$@」はクォートを評価の時に保存しておく。だから上と同じ内容を評価すると次のようになる。

define(`echo2',`$@')
define(This,That)
echo2(`This',`is',`a',`pen')
echo2(``This'',`is',`a',`pen')
→
This,is,a,pen
`This',is,a,pen

では、このような引数処理があるのならば、皆さんのご期待のとおり、/bin/sh みたいに shift がある。つまり、一種のループ処理が出来て、「不定個の引数すべてについて〜をする」というのが出来るのであるが、これはまだ予告するだけとしておく。→ループ


undefine と include

define に対応して当然「#undef」が存在する。「undefine」ディレクティブによって、定義されたマクロを空にできる(当然、-u オプションと対応している)。

define(`X',`NeWS')
X is a Window System for UNIX.
→
NeWS is a Window System for UNIX.
undefine(`X')
But X is too large!
→
But X is too large!

まあ、この動作は言うまでもない。しかし、cpp と違って、「pushdef,popdef」などという一時的にマクロ定義をスタックに積んでしまい、別な定義を与えておいて、使ってからまた元に戻す、なんていう洒落たこともできるのである。

define(`X',`NeWS')
X is a Window System for UNIX.
→
NeWS is a Window System for UNIX.
pushdef(`X',``X'')
But X is too large!
→
But X is too large!
popdef(`X')
So X is presented by Sun.
→
So NeWS is presented by Sun.

まあ、賢いって言えば賢いが、面白いだけかな? それよりも実用上重要なのは当然他のファイルをインクルードするディレクティブであり、当然「include」という名前だ。ただし、「sinclude」というディレクティブもあり、これがなかなか賢い。この2つの違いは「include(`ファイル名')」の時に、ファイル名がなければエラーになるにも関わらず、「sinclude(`ファイル名')」の時にはエラーにならないという違いがある。これは cpp にはない「ちょっとした心使い」だと筆者は思う。当然使い方は次の通り。

sinclude(`outer.m4')

これはどってことない。しかし次のような使い方も出来るのが驚きである。

define(`inc',sinclude(`outer.m4'))
inc
original text.
inc

これは正しくトークン inc にファイル内容が結びつけられて、2回ファイル内容が展開される。つまり、次のようになる。

ここは define ディレクティブが処理されて空行
outer.m4 contains follows...
outer.m4 file end.
original text.
outer.m4 contains follows...
outer.m4 file end.

もし、outer.m4 がなければ、sinclude なので単なる空文字列に展開されるに過ぎない。


条件分岐

さて、次は cpp にもある「条件コンパイル文」(#ifdef とか)の役割を果たす条件判定ディレクティブの紹介である。その名の通り「ifdef」というディレクティブがある。

ifdef(NAME,IF-CASE,ELSE-CASE)

の構文である。だから、次のように使える。複数行の場合でも m4 の文字列が厳格に扱われることに注意されたい。

ifdef(`COND',``COND' is defined...
`COND' is COND.',`COND is *NOT* defined!')

これを「% m4 test.m4」とか「% m4 -DCOND test.m4」とか「% m4 -DCOND=conditionA test.m4」とか、適当な値を引数で与えて実行してみると良い。そうすると、正しく条件判定をして、「COND」の定義如何によって、結果が変わる。

% m4 test.m4
COND is *NOT* defined!
% m4 -DCOND test.m4
COND is defined...
COND is .
% m4 -DCOND=conditionA test.m4
COND is defined...
COND is conditionA.

複数行の定義でも問題ないし、二重クォートがうまく通用していることについても注意を払われたい。「COND」が未定義の条件では、わざわざ二重クォートしなくてもそもそも未定義なので、正しく「COND」のまま展開されるのは理解できるかな?

cpp の「#if」に相当する、マクロの「値」に従って分岐するディレクティブは「ifelse」である。これは一種の switch 文のように、複数の値を比較できるというなかなか凝ったディレクティブである。

ifelse(比較対象A,比較対象B,一致時に展開[,不一致時に展開])

が基本的な構文であるが、オプションである [,不一致時に展開] で、さらに条件判定が出来るのである。つまり、次のように使えるのである。まず基本の使い方。

ifelse(COND,1,``COND' is one',``COND' is COND')
→ m4 -DCOND=1 test.m4 の場合
COND is one
→ m4 -DCOND=3 test.m4 の場合
COND is 3

しかし、これは次のように switch 文のように連鎖して使える。Lisp 知ってる人だと、「あ、cond と同じ要領ね!」とすぐピンとくる使い方だ。

ifelse(COND,1,``COND' is one',COND,2,``COND' is two',``COND' is COND')
→ m4 -DCOND=1 test.m4 の場合
COND is one
→ m4 -DCOND=2 test.m4 の場合
COND is two
→ m4 -DCOND=3 test.m4 の場合
COND is 3

とはいえ、マクロ展開の原理がちゃんと判っているのならば、次のマクロはこういう風には動かない理由を理解できるだろう。

ifelse(`COND',1,``COND' is one',``COND' is COND')
→ m4 -DCOND=1 test.m4 の場合
COND is 1   あれれ?

要するに、引数を評価する際に、ifelse に「COND」を評価させたいのではない。「COND」の定義されている値を比較したいのである。だから、この場合にはクォートしてしまうと、実際には ifelse は「COND」という文字列自体を評価することになり、常にこのマクロは else 条件になる。お分かりかな? マクロのトークン名と、そのマクロが表す値とをうまく区別して理解してね。


ループ

さて、m4 のマクロは不定引数で定義され、引数の個数に関する厳格なチェックはない。そして、マクロの内部で当然今説明した条件判定が可能なのである。また、マクロは展開された内容に、マクロを示すトークンが含まれていたら更に展開されるものである。このことは、マクロによって「再帰」が書けることを意味している。要するに cpp では能力が低すぎるために「再帰マクロによるループ構造」のようなシャレた真似ができなかっただけで、実際にはマクロであっても「再帰」ができるのである。まあ、このマクロによる再帰って、Lisp では良く使われていたりするのだが、そう一般には馴染みがないだろうな。

「けど俺ぁ再帰って苦手だ...」と思われる読者も多くいよう。そういう読者は「この問題を克服して、一回り大きな人間になってやるんだ!」という決意の元に一度きっちり勉強されることを筆者は薦める。コンピュータ科学の基礎にあるのは「再帰」であり、「再帰」をしっかり理解すると、「ホントに一回り大きく」なれるぞ! まあ、そのためには「再帰関数の技」でも読まれると良かろう。

m4 の再帰で、「次の要素を得る」のを担当するのが「shift」である。要するに不定引数をリストであるかのように扱って、その cdr に相当する動作を「shift」で行うのである。ここらへんの感覚は /bin/sh に近い。「shift」の動作はホントに自明なものである。

shift()
→
(空)
shift(foo)
→
(空)
shift(foo,bar,baz)
→
bar,baz

要するに shift は引数の最初のものだけを無視して、2番目以降の引数だけを返す。だからループの内部では、次のようにして使うのが定石になる。

define(`loop',`loop(shift($@))')

つまり、マクロの定義の内部で、さらにその自分自身の名前を使っちゃっているわけである。しかし、その引数は shift によって1つ減っている、という状態を作り出しているのがミソなのである。とはいえ、上のマクロは停止しないので、無限ループに陥る。shift は引数がなくても、単に空文字列を返すからである。だから、先程説明した条件判定を使って、正しく停止するようにしてやらなくてはならない。

define(`last',`ifelse($#,1,$1,`last(shift($@))')')
last(foo,bar,baz,quux)
→
quux

これは次のようなシェルスクリプトと同じように、最後の引数だけを表示する(まあ、こんな風に再帰的に自分を呼び出すシェルスクリプトを書く神経は疑われるが...これは自己ツッコミだな)。

if [ "$#" = "1" ] 
then
    echo $1
else
    shift
    ./last.sh $@
fi

とにかく、shift の使い様は /bin/sh と同様なものであることはご理解頂けたと思う。じゃあ、今度は不定引数を逆順に表示するものだ。

define(`reverse',`ifelse($#,1,$1,`reverse(shift($@)) $1')')
reverse(foo,bar,baz,quux)
→
quux baz bar foo

さらに、for ループだって実現できちゃうのだ。これは info に載っている奴をそのまま掲載するが、こんなのはちょっとヤリ過ぎだな。

define(`forloop',
     `pushdef(`$1', `$2')_forloop(`$1', `$2', `$3', `$4')popdef(`$1')')
define(`_forloop',
     `$4`'ifelse($1, `$3', ,
     `define(`$1', incr($1))_forloop(`$1', `$2', `$3', `$4')')')
forloop(`i', 1, 8, `i ')
→
1 2 3 4 5 6 7 8
forloop(`i', 1, 4, `forloop(`j', 1, 8, `(i, j) ')
')
→
(1, 1) (1, 2) (1, 3) (1, 4) (1, 5) (1, 6) (1, 7) (1, 8)
(2, 1) (2, 2) (2, 3) (2, 4) (2, 5) (2, 6) (2, 7) (2, 8)
(3, 1) (3, 2) (3, 3) (3, 4) (3, 5) (3, 6) (3, 7) (3, 8)
(4, 1) (4, 2) (4, 3) (4, 4) (4, 5) (4, 6) (4, 7) (4, 8)

ちなみに「incr」は数値の引数を1つ加算して値とする組み込みマクロであることは、フツー推測できるだろう。当然「decr」も存在する。

まあ、「こんなマクロ一体何時使うんじゃ!」と叱られそうなものであるが、「なぜ山に登るのか... それはそこに山があるからだ!」的なノリで理解して頂きたい。これがハッカーというものだ。実際、いわゆる「高級言語」が発達する前に、このようなマクロ言語の技はいわゆる「マクロアセンブラ」として独自に発達していたのである。そういうマクロ言語の生き残りが TeX であったりするわけで、今ではあまり馴染みがなくなっている(GNUの as ってアセンブラのクセにマクロが定義できない...)。実はマクロもこれだけのことが出来るのである。なかなか凄いでしょ。


メタ文字の入れ換え

さて、少し実用的な機能の話に戻そう。m4 が「汎用的なマクロプロセッサ」と名乗るのはダテではない。なぜならば、どのような言語にも対応できるように、いろいろなメタ文字などを入れ換えることが出来るのである。これによって、入出力のフォーマットのメタ文字とぶつからない(あるいは合わせる)ようにうまく処理をすることができるのである。

まず、クォート文字が変更できる。これをするのが「changequote」ディレクティブだ。

changequote(開始クォート文字列,終了クォート文字列)

デフォルトのクォート文字(`')に出力の上で特殊な意味があって、バッティングを避けたいならば、変えることさえできるのである。ここで「クォート文字」なのがミソだ。つまり、changequote([[,]]) のようにして、「[[」「]]」のようなシーケンスをクォート文字にできるのである! また引数が空の、changequote(,)は、クォートメカニズムを停止する。こうするとクォートが消えることはなくなる。まあ、若干ジョークでCコメント風にやってみよう。

changequote(/*,*/)
define(/*reverse*/,/*ifelse($#,1,$1,/*reverse(shift($@)) $1*/)*/)
reverse(/*foo*/,/*bar*/,/*baz*/,/*quux*/)

また、コメントも変えることができる。要するに「#」で始まるシェルなどのコメントの中身を展開したいときに、これが必要になる。m4 もデフォルトで「#〜改行」をコメントとして扱うので、「#」以降の内容はデフォルトではマクロ展開されない。これは困るケースがあるので、こういう時にはコメントの定義を「changecom」ディレクティブで変更する。

changecom(開始コメント文字列,終了コメント文字列)

C言語風コメントにしてみよう。

changecom(/*,*/)
define(`X',`XFree86')
/* comment for X */
→  こっちが新しくコメントになったから展開されない
/* comment for X */
# for X Window System
→  デフォルトのコメントの方が展開される
# for XFree86 Window System

さらにコメントと似ているが、m4 によって完全に無視され、一切出力されない処理を受けるものとして、「dnl」がある。これはそういえば、autoconf でコメントを書くのによく使われているな。つまり、dnl 以降改行までは、出力を抑止され、出力にはでてこない。m4 用のコメントに使うのが良かろう(逆に言うとデフォルトの「#」コメントは中身がマクロ展開されないだけで、そのテキスト自体は出力されるのである!)。

この dnl が有用なのは、有効に改行を「食べて」しまうためである。つまり次のようになる。

X
define(`X',`test')
X
define(`X',`test')dnl
X
→改行を正確に書くと
X

test
test

フツーは define のようなディレクティブのみの行は空行に展開されてしまうが、末尾に「dnl」とつけるだけで、末尾の改行を「食べて」ディレクティブの痕跡を抹消できるのである。実際この dnl は超強力なので次のように置換テキストの中に「dnl」が入っていたとしても、

define(`X',`test dnl
test')dnl
X
→
test test

のように有効になる。これは要するにマクロ展開が展開されたテキスト自身に対しても更に展開を続ける、という性格からこうなるわけだ。勿論こうすると「dnl」の文字を出力できるのは言うまでもないが...

define(`X',`test `dnl'
test')dnl
X
→
test dnl
test

また、テキストとして include とか define とかを含むテキストを処理する場合には、これが m4 のディレクティブとして扱われると大変困る。これを回避するためには起動オプションを使う。起動する際に「% m4 -P some.c」という風に「-P」オプション(あるいは --prefix-builtins オプション)を使えば、すべての組み込みディレクティブに対して、それが「m4_」の付いた名前であるとされる。これを使うと m4 で処理したものに何か加工したあとで、更に m4 で処理させるなんて技も使える。

少し具体的なケースを検討しよう。m4 はC言語ヘッダファイルを処理できるか、という問題である。cpp の処理とバッティングしないか、ということを検討するのである。これは結論を言ってしまえば、バッティングしない。なぜなら良くできたもので、cpp とバッティングする可能性のあるディレクティブ「include,define,ifdef(だけではなくて多くのディレクティブ)」は、すべて引数を取らない場合にはディレクティブとして扱われないのである。しかし、ちょっと注意すべきことがある。それはコメントである。次のようなC言語ヘッダを処理するとしよう。

#include MYHEADER
ifdef(`X',`#define `X' X',`#define `X' 10')

なんとなく、「% m4 -DMYHEADER='<stdio.h>' -DX=3」というようなコマンドラインで処理できそうな気がする。しかし、これは失敗する。

% m4 -DMYHEADER='<stdio.h>' -DX=3 header.h
#include MYHEADER
#define `X' X

あれれ、展開しないぞ! 賢明な読者は察するだろうが、「#」が m4 のコメントとして扱われるために、「#」で始まる cpp のディレクティブ行はマクロ展開しないのだ! これを回避するためには、頭に一行コメント変更をいれておく必要がある。

changecom(,)
#include MYHEADER
ifdef(`X',`#define `X' X',`#define `X' 10')

こうすれば、見事 cpp ディレクティブ内部もちゃんと展開し、思った通りの出力になるのである。この時、「#include」の行は最初からコメントなので間違いにくいが、出力テキストの中で「#define」とやっている部分は見落としやすい。ちょっと注意が必要である。

% m4 -DMYHEADER='<stdio.h>' -DX=3 header.h

#include <stdio.h>
#define X 3

あと、マクロ名を構成する文字さえも、実は入れ替えることもできる。しかし、これは「実験的な仕様」なので、GNU m4 でもそれなりのオプションをつけてコンパイルし直さないと、使えない。だから、この仕様の解説は飛ばそう。凄い仕様だな....


組み込み文字列処理関数

GNUのツールっていうと、文字列処理関数がエラく充実している傾向がある。たとえば GNU Make の文字列処理関数なんて、皆さんもきっと使ったことがあろうな。要するにそういう内部関数用ライブラリを、GNUは作って持っているんだろう。というわけで、m4 にも数多くの文字列処理関数がある。これらは基本的に GNU m4 の拡張であり、クラシックな m4 には存在しない。が、使えば便利なものであることは言うまでもない。

まず、正規表現置換をする組み込み関数 patsubst だ。これは次の構文で使う。

patsubst( 対象文字列, 正規表現, 置換文字列 )

第3引数を与えなければ、一致部分が削除される。やや厄介なことに、m4 の正規表現は emacs の正規表現である。emacs の正規表現は結構クセがあるので、通常の sed や awk の正規表現と違う特徴を列挙しよう。これらは要するに、「語」という単位があることに因るものである。

  1. \< が語の先頭を表す。同様に \> は語の末尾を表す。だから、単語を「*」で囲むようにするためには次のようにする。
    define(`X',`this is a pen')
    patsubst(patsubst(X,`\<',*),`\>',*)
    →
    *this* *is* *a* *pen*
    
  2. これはもっと簡単に出来る。\b を使えば良いのである。これは語の先頭あるいは末尾を表す。
    define(`X',`this is a pen')
    patsubst(X,`\b',*)
    
  3. また、「語を構成する文字([0-9a-zA-Z_])」を表すのが \w だ。だから、これはこうも書ける。この時「\&」が一致部分を表すシンボルとして置換文字列の中で使われていることに注意。
    define(`X',`this is a pen')
    patsubst(X,`\w+',*\&*)
    

次に translit 組み込み関数は、tr(1) みたいなノリで、字単位の置換をする。

translit( 対象文字列, 対象字種, 置換字種)

つまり、大文字を小文字に変換するのだと次の通り。

define(`X',`this is a pen')
translit(X,`a-z',`A-Z')
→
THIS IS A PEN

また、regexp 組み込み関数は、正規表現一致の部分だけを抜きだしている。だから、これを使って文字列をバラすことができるのである。これを使うと合わせ技で字の頭だけを大文字化することもできる。

define(`X',`this is a pen')
define(`_capitalize',`regexp(`$1',`\(\w\)\(\w*\)',`translit(`\1',`a-z',`A-Z')'`\2')')
define(`capitalize',`patsubst(`$1',`\w+',`_capitalize(\&)')')
capitalize(X)
→
This Is A Pen

ちょっくら複雑だが、要するに capitalize マクロは patsubst を使って単語毎に切り分けをして下請けの _capitalize マクロを呼び出す。_capitalize マクロの引数 $1 は、バラされた単語であり、これを regexp が最初の一文字(\1)とそれ以降(\2)にバラす。だから、最初の1文字(\1)だけ、translit によって大文字化する。そういうノリで書かれたマクロである。この時、regexp が \( ... \) によって部分一致文字列を指定でき、その参照が登場順に \1, \2,... などで出来ることに注意(まあ、これは awk とか sed とかでもオナジミの機能だ...)。

まあ、その他にもこのカテゴリには len とか、index とか、substr とかがある。これらは perl とか使い慣れてりゃ名前から判るよな。まあ、相対的に重要度が劣るので、これらは適当に試して見たまえ。


その他

大体こんなところで、フツーに使う機能は終りだ。勿論、シェルを実行して結果を取り込む esyscmd とか計算をさせる eval とかあるが、これらは具体的なところで使ってみて憶えるものだ。まあ、そんな具合で結構仕様がテンコ盛りだということは御理解頂けたと思う。まあ、繰り返しにはなるが、m4 は cpp みたいな阿呆なマクロプロセッサとは違って、どんなケースでも使えるようにうまく考えてある汎用プロセッサなのである。ちょっとしたスクリプトから高度な置換をするものまで、応用範囲はメッチャ広いものであることをプログラマ諸氏に御理解頂くために、これを書いたようなものだ。まあ、使ってみてね。どうせあなたのUNIXには必ずあるんだから...



copyright by K.Sugiura, 1996-2006