3.5 m4が入力を出力へコピーする仕組み
m4は入力をトークンごとに読み、そのトークンを直ちに出力へ直接コピーします。
例外は、マクロ定義を持つ単語が見つかった場合です。その場合、m4はマクロの展開結果を計算し、必要なら引数を得るためにさらに入力を読みます。そして展開結果を、残りの入力の前に挿入します。つまり、マクロ呼び出しの結果として得られたテキストは、再び読み込まれてトークンに分割されます。
m4はできるだけ早くマクロを展開します。あるマクロの引数を収集している間に別のマクロ呼び出しが見つかると、その別の呼び出しを先に展開します。この処理は、展開するマクロ呼び出しがなくなり、すべての入力を消費するまで続きます。
具体例として、m4が次の入力をどう処理するかを見てみましょう。
format(`Result is %d', eval(`2**15'))
最初に、m4はトークン「format」がマクロ名だと判断し、別のマクロの候補に出会うまで、トークン「(」「`Result is %d'」「,」「 」を収集します。実際に「eval」もマクロ名なので、入れ子の引数収集によって「(」「`2**15'」「)」を取り込み、唯一の引数「2**15」を指定してevalマクロを呼び出します。「eval(2**15)」の展開結果は「32768」で、これは5つのトークン「3」「2」「7」「6」「8」として再走査されます。次の「)」と合わせてformatマクロの引数がすべて揃い、ユーザーが次のように入力したのと同じ状態になります。
format(`Result is %d', 32768)
formatマクロは「Result is 32768」に展開され、トークン「Result」「 」「is」「 」「3」「2」「7」「6」「8」を探す走査がもう一度行われます。どれもマクロではないため、最終的な出力は次のとおりです。
⇒Result is 32768
より複雑な例として、Gnulibプロジェクトの実際のコード例1について、不具合のある方法と望ましい結果を対比します。ユーザーは、引数を大文字に変換して接頭辞を付け、シェル変数にした上で、シェルの代入文を出力したいと考えています。最初の試みは次のようになります。
changequote([,])dnl
define([gl_STRING_MODULE_INDICATOR],
[
dnl comment
GNULIB_]translit([$1],[a-z],[A-Z])[=1
])dnl
gl_STRING_MODULE_INDICATOR([strcase])
⇒
⇒ GNULIB_strcase=1
⇒
おっと、引数が大文字になっていません。また、マニュアルでは分かりやすく示せませんが、空に見える2行には、実際には末尾のスペースが2個ずつ含まれています。解析を順に追えば、何が起きたかは簡単に分かります。最初に、m4はトークン「changequote」をマクロとして認識し、続く「(」「[」「,」「]」「)」で引数リストを構成します。マクロは空文字列に展開されますが、シェルコードの生成により便利なものへ引用文字を変更します(シェルスクリプトには、対応の取れていない「`」や「'」が頻繁に現れますが、対応の取れていない「[]」はまれです)。同じ1行目で、m4はトークン「dnl」を、その行の残りを消費する組み込みマクロとして認識するため、その行の出力はありません。
2行目ではマクロ定義が始まります。m4はトークン「define」をマクロとして認識し、その後に「(」「[gl_STRING_MODULE_INDICATOR]」「,」が続きます。引用されていないカンマが現れたので、第1引数は、一重に引用された文字列トークンの展開結果、すなわち「gl_STRING_MODULE_INDICATOR」だと分かります。続いてm4は「NL」「 」「 」を読みますが、これらの空白は引数収集の一部として捨てられます。その後、かなり長い一重引用の文字列トークン「[NL dnl
commentNL GNULIB_]」が続きます。次にトークン「translit」が現れ、m4がこれをマクロ名として認識するため、入れ子のマクロ展開が始まります。
translitへの引数は、トークン「(」「[$1]」「,」「[a-z]」「,」「[A-Z]」、最後の「)」から得られます。3つの文字列引数はすべて展開されます(つまり、引用符が取り除かれます)。「$」も「1」も大文字にする必要がないので、マクロの結果は「$1」です。この展開結果は再走査され、リテラルの2文字「$」と「1」になります。
外側のマクロの走査が再開し、「[=1NL ]」、最後に「)」を取り込みます。収集した展開済みテキストの各部分は連結され、最終的にマクロ「gl_STRING_MODULE_INDICATOR」は、文字列「NL dnl commentNL GNULIB_$1=1NL 」として定義されます。もう一度「dnl」が認識され、出力への改行を防ぎます。
最後の行を解析します。最初の「 」「 」はそのまま出力されます。次に「gl_STRING_MODULE_INDICATOR」がマクロ名として認識され、引数リストは「(」「[strcase]」「)」です。マクロ定義には「$1」という並びが含まれるので、再走査を始める前に、その並びを引数「strcase」に置き換えます。再走査では「NL」と4個のスペースが現れ、そのまま出力されます。続く「dnl」は、テキスト「 commentNL」を捨てます。次に4個のスペースがそのまま出力され、先ほどのパラメータ置換で生じたトークン「GNULIB_strcase」が続きます。これはマクロ名ではないので、そのまま出力され、続いてリテラルのトークン「=」「1」「NL」と、さらに2個のスペースが出力されます。最後に、元のマクロ呼び出しの後にあった「NL」が走査され、そのまま出力されます。
次に修正した方法を示します。改行と空白の使い方を組み替え、出力される空白を減らします(空白はシェルスクリプトには無害でも、見た目がよくないことがあります)。また、引用の問題を直し、マクロ「gl_STRING_MODULE_INDICATOR」を定義したときではなく、呼び出したときに大文字化するようにします。さらに、translitの第1引数に引用をもう1段階追加し、結果が、さらなる展開を要する大文字のマクロ名候補としてではなく、文字列として再走査されるようにします。
changequote([,])dnl define([gl_STRING_MODULE_INDICATOR], [dnl comment GNULIB_[]translit([[$1]], [a-z], [A-Z])=1dnl ])dnl gl_STRING_MODULE_INDICATOR([strcase]) ⇒ GNULIB_STRCASE=1
1行目の解析は変わりません。2行目では、定義するマクロ名を読み、前と同様に「NL」と2個のスペースを捨てます。ただし今回は、次のトークンが、入れ子の引用を含む「[dnl commentNL GNULIB_[]translit([[$1]], [a-z],
[A-Z])=1dnlNL]」です。続く「)」がマクロ定義を終了し、「dnl」が改行を読み飛ばします。translitの早すぎる展開は起きず、文字列全体がマクロの定義になります。
最後の行を解析します。最初の2個のスペースはそのまま出力され、次に、引数「strcase」を指定したgl_STRING_MODULE_INDICATORの呼び出しが続きます。ここでも、マクロ定義内の「$1」は再走査前に置換されます。再走査では最初に「dnl」が現れ、「 commentNL」を捨てます。その後、2個のスペースをそのまま出力します。次のトークンは「GNULIB_」ですが、マクロではないのでそのまま出力されます。トークン「[]」は空文字列なので出力に影響しません。続いてトークン「translit」が現れます。
今回は、translitへの引数が「(」「[[strcase]]」「,」「 」「[a-z]」「,」「 」「[A-Z]」「)」として解析されます。2個のスペースは捨てられ、translitによって望みどおりの結果「[STRCASE]」が得られます。これは再走査されますが、文字列なので引用符が取り除かれ、リテラルの「STRCASE」だけが出力されます。続いて走査器は「=」「1」を読み、そのまま出力します。その後の「dnl」は、gl_STRING_MODULE_INDICATORの定義の残りを捨てます。出力の末尾の改行は、マクロ呼び出しの後に現れたリテラルの「NL」です。
GNU m4のトレース機能を使えば、m4がマクロを展開する順序をさらに調べられます(マクロ呼び出しのトレースを参照)。
(1)
https://lists.gnu.org/archive/html/bug-gnulib/2007-01/msg00389.htmlのパッチと、それに続くhttps://lists.gnu.org/archive/html/bug-gnulib/2007-02/msg00000.htmlのパッチに基づきます。