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

第 6 章 m4 マクロプロセッサ

概要

m4 は、C 言語とアセンブリ言語のプログラムを前処理する際に使用できる汎用マクロプロセッサです。テキストの文字列を別の文字列に置換する簡単な処理のほかに、m4 は以下の処理を行うことができます。

ユーザーは、組み込みマクロを使用してこれらの処理を実行したり、独自のマクロを定義できます。一部の組み込みマクロは処理の状態に副作用を及ぼしますが、それ以外は組み込みマクロとユーザー定義マクロはまったく同じように機能します。

m4 の基本動作は、英数字のトークン (文字と数字の文字列) をすべて読み取り、そのトークンがマクロの名前かどうかを判定することです。マクロの名前はそのマクロの定義テキストに置換され、その文字列は入力に戻されて再度読み取られます。マクロは、引数を使用して呼び出すことができます。引数は、収集されて、定義テキストが再度読み取られる前に適切な位置でその定義テキストに置換されます。

マクロ呼び出しの一般形式を以下に示します。

名前 (引数1, 引数2, ..., 引数n)

マクロ名の直後に左括弧がない場合は、引数がないものとみなされます。引数の収集時に引数の先頭にある引用符で囲まれていない空白文字、タブ、復帰改行は無視されます。左と右の単一引用符は、文字列を囲む際に使用します。引用符で囲まれた文字列の値は、引用符を取り除いた文字列になります。

マクロ名が認識されると、その引数は左括弧に対応する右括弧を検索することによって収集されます。与えられた引数がマクロ定義の引数の数よりも少ない場合には、足りない引数はすべて NULL であるとみなされます。引数を収集している間に、マクロ評価が行われます。また、入れ子にされた呼び出しの値に含まれているコンマや右括弧は、元の入力テキストのコンマや括弧と同様にすべて有効です。引数の収集が終わると、マクロの値は入力ストリームに返され、再度読み取られます。以下に、詳細を説明します。

以下の形式のコマンドを使用して m4 を呼び出します。

$ m4 file file file

各引数ファイルは、順番に処理されます。引数が 1 つも指定されていない場合、またはハイフンが引数として指定されている場合には、標準入力が読み取られます。最終的に m4 の出力をコンパイルする場合は、次のようなコマンドを使用してください。

$ m4 file1.m4 > file1.c

m4 コマンド行で -D オプションを使用すると、マクロを定義できます。たとえば、類似した 2 つのバージョンのプログラムがあり、その 2 つの出力ファイルを生成できる 1 つの m4 入力ファイルがあるとします。つまり、file4.m4 には以下のような行が含まれています。

if(VER, 1, do_something) 
if(VER, 2, do_something)

このプログラムのメークファイルは以下のようになります。

file1.1.c : file1.m4 
m4 -DVER=1 file1.m4 > file1.1.c 
...  
file1.2.c : file1.m4 
m4 -DVER=2 file1.m4 > file1.2.c 
...  

-U オプションを使用すると、 VER の定義を解除することができます。file1.m4 に以下の行が含まれている場合には、

if(VER, 1, do_something) 
if(VER, 2, do_something) 
ifndef(VER, do_something)

そのメークファイルには以下のような行が含まれることになります。

file0.0.c : file1.m4 
m4 -UVER file1.m4 > file1.0.c 
...  
file1.1.c : file1.m4 
m4 -DVER=1 file1.m4 > file1.1.c 
...  
file1.2.c : file1.m4 
m4 -DVER=2 file1.m4 > file1.2.c 
...  

m4 マクロ

マクロの定義

基本となる組み込み m4 マクロは、define() です。このマクロを使用して、新しいマクロを定義します。以下を入力すると、文字列 namestuff として定義されます。

define(
name, stuff)

この定義以降に現われる name は、すべて stuff に置換されます。定義される文字列は、英数字にしてください。また、定義文字列の先頭は英数字 (下線は英数字とみなされます) にする必要があります。定義文字列は、1 組の括弧を含んだ任意のテキストです。この定義文字列は複数の行に渡っていても構いません。

典型的な例を以下に示します。

define(N, 100) 
...  
if (i > N)

この例では、N は 100 として定義されており、その定数 Nif 文で使用されています。

前述のように、define() が引数を持っていることを示すには、ワード define の直後に左括弧を置く必要があります。マクロ名の直後に左括弧が続いていない場合は、そのマクロは引数を持っていないとみなされます。したがって、上記の例の N は、引数を持たないマクロです。

マクロ名は、英数字以外の文字で囲まれている場合にだけ、そのマクロ名として認識されます。以下の例では、変数 NNNN が含まれていますが、定義されたマクロ N とこの変数の間には何の関係もありません。

define(N, 100) 
...  
if (NNN > 100)

m4 は、できるかぎり早くにマクロ名を定義されているテキストに展開します。したがって、以下の例では、define(M, N) の引数を収集するときには N が 100 に置換されているので、M は 100 として定義されます。別の見方をすれば、N を再定義しても M の値は 100 のまま変わらないということになります。

define(N, 100) 
define(M, N)

このような結果を回避する方法は 2 つあります。1 つは、定義の順序を変更して回避する方法です。この方法は、上記の例のような場合に特に有効です。

define(M, N) 
define(N, 100)

これにより、M は文字列 N として定義されるので、M の値が後で要求されたときには、結果は常にその時点の N の値になります。つまり M は、N の値 100 に置換されます。

引用符で囲む

より一般的な解決方法は、define() の引数を引用符で囲んでその引数の展開を遅らせる方法です。左と右の単一引用符に囲まれたテキストは、すぐには展開されず、引数が収集されたときに引用符が取り除かれます。引用符で囲まれた文字列の値は、その引用符を取り除いた文字列です。

したがって、以下の例では、M は 100 ではなく文字列 N として定義されます。

define(N, 100) 
define(M, `N')

一般的なルールとして、m4 は評価時に 1 組の単一引用符を取り除きます。これは、マクロの外でも同じです。ワード define を出力に表示する場合には、入力ではそのワードを引用符で囲む必要があります。

`define' = 1;

通常は、マクロの引数を引用符で囲んで、マクロに定義した値が実際にそのマクロに割り当てられるようにする方法をお勧めします。たとえば、N を再定義するには、以下のように引用符を使用してその評価を遅らせます。

define(N, 100) 
...  
define(`N', 200)

引用符で囲まない場合は、2 番目の定義の N はすぐに 100 に置換されます。

define(N, 100) 
...  
define(N, 200)

このため結果は以下の定義と同じになります。

define(100, 200)

m4 は、名前と判断できるものしか定義できないので、この文を無視します。

左と右の単一引用符以外を引用符記号として使用したい場合には、以下のように組み込みマクロ changequote() を使用して、引用符記号を変更することができます。

changequote([, ])

この例では、マクロによって引用符記号を左と右の単一引用符から左と右の角括弧に変更します。引用符記号の最大文字長は 5 文字です。元の引用符記号に戻すには、引数のない changequote() を使用します。

changequote

undefine() は、マクロまたは組み込みマクロの定義を削除します。

undefine(`N')

この例では、マクロによって N の定義が削除されます。undefine() の引数は必ず引用符で囲んでください。同様に、組み込みマクロも undefine() を使用して削除できます。

undefine(`define')

組み込みマクロを削除したり再定義すると、そのマクロの元の定義は使用できなくなります。マクロの名前は、defn() を使用して変更することができます。たとえば、組み込みマクロの define()XYZ() という名前に変更する場合には、以下のように指定します。

define(XYZ, defn(`define')) 
undefine(`define')

この指定を行うと、XYZ()define() の元の意味を引き継ぎます。したがって、以下のマクロは A を 100 として定義します。

XYZ(A, 100)

組み込みマクロの ifdef() は、マクロが現在定義されているかどうかを判定することができます。以下のようにして、システムごとに特定のマシンに適した定義を行うことができます。

ifdef(`pdp11', `define(wordsize,16)') 
ifdef(`u3b', `define(wordsize,32)')

ifdef() マクロには、3 つの引数を指定できます。最初の引数が定義されている場合は、ifdef() の値は 2 番目の引数になります。最初の引数が定義されていない場合は、ifdef() の値は 3 番目の引数になります。

ifdef(`unix', on UNIX, not on UNIX)

3 番目の引数がない場合は、ifdef() の値は NULL になります。

引数

ここまでは、文字列を別の (固定された) 文字列に置換するという、最も単純なマクロ処理について説明してきました。マクロは、呼び出しごとに結果が異なるように定義することもできます。マクロの置換テキスト (マクロ define() の 2 番目の引数) は、そのマクロが実際に使用されるときに、$n の部分が n 番目の引数に置換されます。したがって、以下のように定義された bump() は、bump(x) の場合、x = x + 1 と同義になります。

define(bump, $1 = $1 + 1)

マクロには、任意の数の引数を指定できますが、個別にアクセスできるのは最初の 9 個 ($1 〜 $9) だけです。$0 はそのマクロ自身の名前です。すでに説明したように、指定されていない引数は、NULL 文字列に置換されます。そのため、以下のように、引数を連結するマクロを定義できます。

define(cat, $1$2$3$4$5$6$7$8$9)

つまり、cat (x、y、z) の結果は xyz になります。引数 $4 〜 $9 は、対応する引数が指定されていないため、NULL になります。

引数の収集時に引数の先頭にある引用符で囲まれていない空白文字、タブ、復帰改行は破棄されます。それ以外の空白はそのまま残ります。したがって、以下のマクロの場合には、ab cとして定義されます。

define(a, b c)

引数はコンマによって区切られます。括弧で囲まれたコンマは引数の区切りとはみなされません。以下の例では、2 つの引数 a(b,c) を使用しています。このように括弧で囲むことによって、コンマや括弧を引数として指定することができます。

define(a, (b,c))

以下の例では、 $(** は、その後で呼び出すマクロに指定された引数のリストに置換されます。その引数リストの中の各項目はコンマで区切られます。したがって、以下の例の結果は、1, 2 になります。

define(a, 1) 
define(b, 2) 
define(star, `$(**') 
star(a, b)

また、以下の例では m4star() の引数を収集するときに ab から引用符を取り除き、star() を評価するときに ab を展開するので、上記の例と結果は同じになります。

star(`a', `b')

$@ は、その後で呼び出すマクロの各引数が引用符で囲まれている以外は、$(** と同義です。したがって、以下の例では at() が評価されるときに引用符が再び引数に付けられるので、結果は a,b となります。

define(a, 1) 
define(b, 2) 
define(at, `$@') 
at(`a', `b')

$# は、その後で呼び出すマクロの引数の総数に置換されます。したがって、以下の例の結果は、3 になります。

define(sharp, `$#') 
sharp(1, 2, 3)

以下の場合は、結果は 1 になります。

sharp()

また以下の場合は、結果は 0 になります。

sharp

組み込みマクロの shift() は、1 つ目の引数以外のすべての引数を返します。1 つ目の引数以外の引数は、引用符で囲まれコンマで区切られて入力に返されます。最も単純な例を以下に示します。

shift(1, 2, 3)

この例の場合は、結果は 2、3 になります。$@ の場合と同様に、引用符で引数を囲むことによって、その引数の展開を遅らせることができます。したがって以下の例の場合は、shift() が評価されるときに引用符がまた引数に付けられるので、結果は b となります。

define(a, 100) 
define(b, 200) 
shift(`a', `b')

演算用の組み込みマクロ

m4 には、整数演算を行う 3 つの組み込みマクロがあります。incr() は、その数値の引数を 1 ずつ増やします。decr() は、1 ずつ減らします。したがって、プログラミングでよく使われる「N より 1 大きい」変数を定義するには、以下のように incr() を使用します。

define(N, 100) 
define(N1, `incr(N)')

つまり、N1N の現在値よりも 1 大きい値として定義されます。

演算でよく用いられるメカニズムは、eval() という組み込みマクロです。このマクロでは、整数に対して任意の演算を行うことができます。以下に、このマクロの演算子を優先度が高いものから順に示します。

+ - (単項)
(**(** 
(** / % 
+ - 
== != < <= > >= 
! ‾ 
& 
| ^ 
&& 
|| 

必要に応じて、括弧を使用して演算をグループ化できます。eval() に最終的に渡される式のオペランドは、すべて数値でなければなりません。真の関係 (たとえば、1 > 0) は 1、偽の関係は 0 になります。eval() の精度は 32 ビットです。

簡単な例を以下に示します。この例では、M2(**(**N+1 として定義しています。

define(M, `eval(2(**(**N+1)')

この場合、以下の結果は 9 になります。

define(N, 3) 
M(2)

ファイルの取り込み

組み込みマクロ include() を使用すると、新しいファイルを任意の時点に入力に取り込むことができます。

include(ファイル名)

このマクロは、指定したファイルの内容をマクロとその引数の位置に挿入します。include() の値 (include() の置換テキスト) は、ファイルの内容です。必要に応じて、ファイルの内容を定義などに取り込むことができます。

include() で指定されたファイルにアクセスできない場合には、重大なエラーが発生します。重大なエラーとしたくない場合には、 sinclude() (silent include) を使用することができます。指定されたファイルにアクセスできない場合でも、この組み込みマクロはメッセージを出力せずにそのまま処理を続けます。

出力の分割

処理中に、m4 の出力を一時ファイルにすることができます。収集されたデータはコマンドで出力できます。m4 は、出力を 1 番から 9 番の 9 つに分割することができます。組み込みマクロ divert(n) を使用した場合には、後続の出力はすべて n で参照される 1 つの一時ファイルに追加されます。このファイルへの出力を停止するには、divert() マクロまたは divert(0) マクロを使用します。これによって、通常の出力プロセスが再開されます。

分割された出力テキストは、通常は処理の終わりに番号順に配置されます。新しい分割出力を現在の出力の後に追加することによって、いつでも分割出力を元に戻すことができます。0 〜 9 以外の番号で分割された出力は、破棄されます。組み込みマクロ undivert() は、すべての分割出力を番号順に元に戻します。undivert()に引数を指定すると、指定された分割出力を指定された順に元に戻します。引数なしで undivert() を使用すると、(divert() の場合と同様に) 分割された出力テキストは 0 〜 9 以外の番号が付けられ破棄されます。

undivert() の値は、分割された出力テキストではありません。また、分割出力されたマクロのデータは、再度読み取られません。組み込みマクロ divnum() は、現在アクティブな分割出力の番号を返します。通常の出力方法で処理を行なっている場合、divnum() の値は 0 です。

システムコマンド

組み込みマクロ syscmd() を使用して、任意のプログラムを実行できます。以下に示した例では、オペレーティングシステムの date コマンドが呼び出されています。通常、後に続く include() 用のファイルを作成する際にこの syscmd() を使用します。

syscmd(date)

個々のファイルに容易に一意の名前を付けるために、組み込みマクロ maketemp() は、引数に含まれている文字列 XXXXX を現在のプロセスのプロセス ID に置換します。

条件付きテスト

任意の条件付きテストを、組み込みマクロ ifelse() を使用して実行することができます。このマクロの簡単な例を以下に示します。

ifelse(a,b,c,d)

この例では、文字列 ab が比較されます。ab が等しければ ifelse() は文字列 c を返します。等しくなければ d を返します。たとえば、2 つの文字列を比較して、等しい場合は yes、等しくない場合は no を返す compare() というマクロを定義するには、以下のようにします。

define(compare, `ifelse($1, $2, yes, no)')

ifelse() の評価を遅らせるために、引用符を使用している点に注目してください。最後の引数が省略されている場合は、結果は NULL になります。たとえば以下に示した例では、ab が等しい場合は c、等しくない場合は NULL という結果になります。

ifelse(a,b,c)

ifelse() は、実際には任意の数の引数を持つことができ、単純な条件を判定して処理を分岐する機能も備えています。たとえば以下の例では、文字列 a と 文字列 b が等しければ結果は c になります。等しくない場合は de を比較して、等しければ結果は f になります。de が等しくない場合は、結果は g になります。

ifelse(a,b,c,d,e,f,g)

文字列の操作

len() マクロは、引数に含まれている文字列の長さ (文字数) を返します。たとえば、

len(abcdef)

6 を返し、

len((a,b))

5 を返します。

substr() マクロは、文字列の部分文字列を生成します。たとえば以下のマクロは、文字列 si 番目の位置 (起点は 0) から始まる n 文字の部分文字列を返します。

substr(s, i, n)

n が省略されている場合には、i 文字目以降の文字列が返されます。たとえば、以下のように入力すると、

substr(`now is the time',1)

以下の文字列が返されます。

ow is the time

i または n が範囲を超えている場合は、結果は不定です。

index(s1,s2) マクロは、文字列 s1 の中で文字列 s2 が現われる場所のインデックス (位置) を返します。文字列 s2 が含まれていない場合は -1 が返されます。substr() の場合と同様に、文字列の起点は 0 です。

translit() は、文字置換を行います。このマクロの一般形式を以下に示します。

translit(s,f,t)

このマクロは、f に指定された文字を t に指定された対応する文字に置換して s を変更します。

たとえば、以下の入力を使用すると、

translit(s, aeiou, 12345)

母音字はそれに対応する数字に置換されます。tf より短い場合は、t に指定されていない文字は削除されます。t に何も指定されていない場合は、f の中の文字が s から削除されます。

したがって、以下の場合には、s から母音字 a,e,i,o,u がすべて削除されます。

translit(s, aeiou)

マクロ dnl() は、dnl() の後から復帰改行までの文字 (復帰改行も含む) をすべて削除します。このマクロは主に、m4 の出力に必要ない空の行を削除するために使用されます。たとえば、以下を入力すると、定義に含まれない行の終わりは復帰改行になります。

define(N, 100) 
define(M, 200) 
define(L, 300)

したがって、復帰改行を必要としない出力に復帰改行がコピーされます。これらの各行に dnl() を追加すると、復帰改行はなくなります。また、以下のように記述した場合も、同じ結果を得ることができます。

divert(-1) 
define(...) 
...  
divert

出力

組み込みマクロ errprint() は、その引数を標準エラーファイルに書き込みます。以下にその例を示します。

errprint(`fatal error')

また、dumpdef() は、引数として指定した項目の現在の名前と定義を出力する、デバッグ支援用のマクロです。引数が指定されていない場合は、現在の名前と定義がすべて出力されます。

組み込み m4 マクロの一覧

表 6-1 組み込み m4 マクロの一覧

組み込み m4 マクロ 

説明 

changequote(L, R)

左の引用符を L、右の引用符を R に変更します。

changecom

左と右のコメントマーカーをデフォルトの # と復帰改行から変更します。 

decr

1 ずつ減分された引数の値を返します。 

define(name, stuff)

namestuff として定義します。

defn('name')

引用符で囲まれた引数の定義を返します。 

divert(number)

出力を number 個に分割します。

divnum

現在アクティブな分割出力の番号を返します。 

dnl

復帰改行までを削除します (復帰改行も含む)。 

dumpdef(`name', `name', . . .)

指定された定義を出力します。 

errprint(s, s, . . .)

引数を標準エラーに書き込みます。 

eval(numeric expression)

数式 numeric expression を評価します。

ifdef(`name', true string, false string)

name が定義されている場合は true string を返し、定義されていない場合は false string を返します。

ifelse(a, b, c, d)

ab が等しい場合は c を返し、等しくない場合は d を返します。

include(file)

ファイルの内容を取り込みます。 

incr(number)

number を 1 ずつ増分します。 

index(s1, s2)

s2 が現われる s1 中の位置を返します。s2 が含まれていない場合は -1 を返します。

len(string)

文字列 string の長さを返します。

maketemp(. . .XXXXX. . .)

一時ファイルを作成します。 

m4 exit

m4 を直ちに終了します。

m4 wrap

最後の EOF で、引数 1 が入力ストリームに返されます。

popdef

引数の現在の定義を削除します。 

pushdef

以前の定義をすべて保存します (define() の機能と同等)。

shift

1 つ目の引数以外のすべての引数を返します。 

sinclude(file)

ファイルの内容を取り込みます。ファイルが見つからない場合は、無視して継続します。 

substr(string, position, number)

文字列 stringposition 番目の位置から始まる文字数が number の部分文字列を返します。

syscmd(command)

システムのコマンドを実行します。 

sysval

syscmd() への最後の呼び出しのコードを返します。

traceoff

広域的にも、指定されたマクロに対しても、トレースをオフにします。 

traceon

すべてのマクロに対してトレースをオンにします。また、引数を使用して、指定したマクロに対するトレースだけをオンにします。 

translit(string, from, to)

from で指定された文字を to で指定された文字に、文字列 string 中の文字を置換します。

undefine(`name')

定義のリストから名前を削除します。 

undivert(number, number, ...)

number 番目の分割出力を現在の分割出力の後に追加します。