前回までにMakefileで使うルール(ターゲットと依存先)とレシピ(ルールに続くコマンドが書いてある部分)、変数などに関する説明を行ってきた。これだけでもMakefileの多くの内容が読めるようになったはずだ。

加えて、Makefileでは文字列やファイル名の加工機能、制御構文に似た機能が提供されていて、プログラミング言語に近い使い方もできるようになっている。「タスクの整理」という目的ではほとんど関数は使わないのだが、文字列やファイル名の加工では使うこともあるので、ある程度関数について知っておいた方が良い。

Makefileの関数

Makefileにおける関数は、現在主要になっているプログラミング言語の関数とはかなり趣が異なっている。同じ要領で使おうとすると、“ハマる”かもしれない。

まず基本的に、Makefile (GNU make)では、変数と同じように関数を表記する。基本シンタックスは次のような感じだ。

$(関数 引数)

もしくは、{}を使って次のように書くこともできる。

${関数 引数}

そして、変数が使える場所では関数も使うことができる。変数が展開されるような部分で、代わりに関数の処理結果が展開される、といった仕組みになっている。

引数が2つ以上ある場合、関数の引数は「,(カンマ)」区切りで指定する。次のようになる。

$(関数 引数,引数,引数…)

こちらも同様に、{}を使って次のように書くこともできる。

${関数 引数,引数,引数…}

Makefileでは、変数はただの文字列としてだけではなく、空白区切りにして複数の単語として処理されるケースがある。そのため、引数に空白が含まれることが多い。そのため、引数の区切りとして空白文字ではなく「,」が使われていると考えると飲み込みやすいと思う。

コマンドラインでは引数は空白区切りで指定する。Makefileにおいて、レシピはコマンドを指定できるので、ついついコマンドラインと同じ概念で使いたくなるが、Makefileの関数における引数の区切りは空白ではなく「,」だ。

ということは、一見関数の引数が空白区切りのように書いてある場合、それは空白も含んだ文字列が一つの引数になっていることになる。最初はこの辺りを間違えがちなので、留意していただきたい。

なお、GNU makeのドキュメントでは、関数の引数に変数が入るようなケースでは、次のように括弧の表記に同じ記号を使うことを推奨している。

$(関数 引数,引数,$(変数))

次のような書き方をしても間違ってはいないのだが、推奨はされていない。

$(関数 引数,引数,${変数})

好みの問題のような気もするが、公式のドキュメントが同じ括弧を使うことを推奨しているので、その書き方に習っておくのが良いだろう。

文字列を操作する関数

GNU makeで使用できる関数はいくつもあるので、一気に取り上げることはできない。今回は特に汎用的に使えると思われる文字列の加工用関数を取り上げておく。次の関数が文字列を加工する関数だ。

関数 シンタックス
subst $(subst 置換前文字列,置換後文字列,文字列)
patsubst $(patsubst パターン,置換後文字列,文字列)
$(変数:パターン=置換後文字列)
$(変数:サフィックス=置換後文字列)
strip $(strip 文字列)
findstring $(findstring 検索文字列,文字列)
filter $(filter パターン パターン パターン…,文字列)
filter-out $(filter-out パターン パターン パターン…,文字列)
sort $(sort 文字列)
word $(word 番号,文字列)
wordlist $(wordlist 開始番号,終了番号,文字列)
words $(words 文字列)
firstword $(firstword 文字列)
$(word 1,文字列)
lastword $(lastword 文字列)
$(word $(words 文字列),文字列)

patsubst関数については、以前変数を取り上げたときに少しだけ紹介した。変数で拡張子の書き換えなどができるのだが、それは実際にはpatsubst関数への短縮表記になっており、関数を指定しているのと同じだ。

関数のそれぞれの意味は次のようになる。

関数 内容
subst テキストを指定された文字で置換。関数は変換された後の文字列に置き換えられる
patsubst テキストを空白区切りの単語で構成された文字列であるとし、それぞれの単語がパターンに一致する場合、置換後の文字列へ変換する。パターンと置換後文字列にはワイルドカードとして%を指定することができる。パターンにおいて%に一致した内容は、置換後文字列における後方参照として機能する。%は1つのみがこの機能を果たし、2つ目以降の%はただの文字列の%として使われる。%をクォートするには\%と記述する。patsubst関数は変数においてショートカット表記が用意されており、変数の記述の状態でもパターンやサフィックスを使用した置換を実施できる
strip 文字列の先頭および末尾の連続する空白を削除する。変数が空であるかどうかを確認する場合などに便利
findstring 文字列から検索文字列を検索する。検索文字列が含まれていれば関数は検索文字列に置き換わり、見つからなければ空の文字列に置き換わる
filter 文字列を空白区切りの単語で構成された文字列であるとし、同じく空白で区切られた複数のパターンのどれかに一致する場合に一致するものをすべて返す関数。変数に格納されたファイル名一覧から、特定のサフィックスを持つファイルだけを取り出したいといった用途で使われる
filter-out 文字列を空白区切りの単語で構成された文字列であるとし、同じく空白で区切られた複数のパターンのどれにも一致しない場合に一致しないものをすべて返す関数。filter関数と逆の動作をする
sort 文字列を空白区切りの単語で構成された文字列であるとし、単語を辞書順で整列し、最終的にそれぞれの単語を1つの空白で区切った文字列へ置き換える。重複している単語は1つにまとめられる。処理する単語の順序を気にしない場合には、重複する単語を1つにまとめる方法としてこの関数が使われることもある
word 文字列を空白区切りの単語で構成された文字列であるとし、指定した番号番目の単語に置き換える。番号は1からはじまり、単語数よりも多い数が指定された場合には空文字列へ置き換えられる
wordlist 文字列を空白区切りの単語で構成された文字列であるとし、指定した開始番号番目から指定した終了番号番目までの単語を1つの空白で区切った文字列へ置き換える。番号は1からはじまり、単語数よりも多い数が指定された場合には空文字列へ置き換えられる
words 文字列を空白区切りの単語で構成された文字列であるとし、単語数に置き換える
firstword 文字列を空白区切りの単語で構成された文字列であるとし、最初の単語に置き換える
lastword 文字列を空白区切りの単語で構成された文字列であるとし、最後の単語に置き換える

気をつけたいのは、Makefileの変数には型がないので、すべてのデータは基本的に文字列なのだが、実際には単一の文字列として処理の対象となるケースと、リストや配列的なニュアンスで処理される2つのケースがあるという点だ。文字列とリスト、または配列といった型が用意されていれば明確なのだが、Makefileではこの辺りがコンテキストに依存していて、曖昧なまま表記する仕組みになっている。

現在主流のプログラミング言語では型が用意されているため、そうした概念を適用しようとすると混乱するかもしれないが、とりあえず「単一の文字列としての挙動とリストとしての挙動が関数によって変わる」ということを把握しておいていただきたい。

文字列を操作する関数の使用例

関数に関しては、実際に書いてみてどのように動作するのか演習することをお勧めしたい。本稿では、今回取り上げた関数をほぼ一通り試すことができるように次のMakefileを用意した。

FILES:=src2.c src1.c src2.o src1.o a.out

all:    print-vals                          \
    test-subst                          \
    test-patsubst                           \
    test-findstring                         \
    test-filter                         \
    test-filter-out                         \
    test-sort                           \
    test-word                           \
    test-wordlist                           \
    test-words                          \
    test-firstword                          \
    test-lastword

print-vals:
    @echo ----------------------------------------------------------
    @echo FILES:=$(FILES)

test-subst:
    @echo ----------------------------------------------------------
    @echo subst関数
    @echo " \$$(subst src,SRC,\$$(FILES))"
    @echo "     => "$(subst src,SRC,$(FILES))

test-patsubst:
    @echo ----------------------------------------------------------
    @echo patsubst関数
    @echo " \$$(patsubst %.c,%.s,\$$(FILES))"
    @echo "     => "$(patsubst %.c,%.s,$(FILES))
    @echo " \$$(FILES:%.c=%.s)"
    @echo "     => "$(FILES:%.c=%.s)
    @echo " \$$(FILES:c=s)"
    @echo "     => "$(FILES:c=s)

test-findstring:
    @echo ----------------------------------------------------------
    @echo findstring関数
    @echo " \$$(findstring src1.c,\$$(FILES))"
    @echo "     => "$(findstring src1.c,$(FILES))
    @echo " \$$(findstring c src,\$$(FILES))"
    @echo "     => "$(findstring c src,$(FILES))
    @echo " \$$(findstring src3.c,\$$(FILES))"
    @echo "     => "$(findstring src3.c,$(FILES))

test-filter:
    @echo ----------------------------------------------------------
    @echo filter関数
    @echo " \$$(filter %.c %.s\$$(FILES))"
    @echo "     => "$(filter %.c %.s,$(FILES))
    @echo " \$$(filter %.a %.b,\$$(FILES))"
    @echo "     => "$(filter %.a %.b,$(FILES))

test-filter-out:
    @echo ----------------------------------------------------------
    @echo filter-out関数
    @echo " \$$(filter-out %.c %.s\$$(FILES))"
    @echo "     => "$(filter-out %.c %.s,$(FILES))
    @echo " \$$(filter-out %.a %.b,\$$(FILES))"
    @echo "     => "$(filter-out %.a %.b,$(FILES))

test-sort:
    @echo ----------------------------------------------------------
    @echo sort関数
    @echo " \$$(sort \$$(FILES))"
    @echo "     => "$(sort $(FILES))
    @echo " \$$(sort \$$(FILES) \$$(FILES))"
    @echo "     => "$(sort $(FILES) $(FILES))

test-word:
    @echo ----------------------------------------------------------
    @echo word関数
    @echo " \$$(word 2,\$$(FILES))"
    @echo "     => "$(word 2,$(FILES))
    @echo " \$$(word 10,\$$(FILES))"
    @echo "     => "$(word 10,$(FILES))

test-wordlist:
    @echo ----------------------------------------------------------
    @echo wordlist関数
    @echo " \$$(wordlist 2,4,\$$(FILES))"
    @echo "     => "$(wordlist 2,4,$(FILES))
    @echo " \$$(wordlist 2,10,\$$(FILES))"
    @echo "     => "$(wordlist 2,10,$(FILES))

test-words:
    @echo ----------------------------------------------------------
    @echo words関数
    @echo " \$$(words \$$(FILES))"
    @echo "     => "$(words $(FILES))

test-firstword:
    @echo ----------------------------------------------------------
    @echo firstword関数
    @echo " \$$(firstword \$$(FILES))"
    @echo "     => "$(firstword $(FILES))
    @echo " \$$(word 1,\$$(FILES))"
    @echo "     => "$(word 1,$(FILES))

test-lastword:
    @echo ----------------------------------------------------------
    @echo lastword関数
    @echo " \$$(lastword \$$(FILES))"
    @echo "     => "$(lastword $(FILES))
    @echo " \$$(word $(words \$$(FILES)),\$$(FILES))"
    @echo "     => "$(word $(words $(FILES)),$(FILES))

実行すると次のようになる。

# make
----------------------------------------------------------
FILES:=src2.c src1.c src2.o src1.o a.out
----------------------------------------------------------
subst関数
        $(subst src,SRC,$(FILES))
                => SRC2.c SRC1.c SRC2.o SRC1.o a.out
----------------------------------------------------------
patsubst関数
        $(patsubst %.c,%.s,$(FILES))
                => src2.s src1.s src2.o src1.o a.out
        $(FILES:%.c=%.s)
                => src2.s src1.s src2.o src1.o a.out
        $(FILES:c=s)
                => src2.s src1.s src2.o src1.o a.out
----------------------------------------------------------
findstring関数
        $(findstring src1.c,$(FILES))
                => src1.c
        $(findstring c src,$(FILES))
                => c src
        $(findstring src3.c,$(FILES))
                =>
----------------------------------------------------------
filter関数
        $(filter %.c %.s$(FILES))
                => src2.c src1.c
        $(filter %.a %.b,$(FILES))
                =>
----------------------------------------------------------
filter-out関数
        $(filter-out %.c %.s$(FILES))
                => src2.o src1.o a.out
        $(filter-out %.a %.b,$(FILES))
                => src2.c src1.c src2.o src1.o a.out
----------------------------------------------------------
sort関数
        $(sort $(FILES))
                => a.out src1.c src1.o src2.c src2.o
        $(sort $(FILES) $(FILES))
                => a.out src1.c src1.o src2.c src2.o
----------------------------------------------------------
word関数
        $(word 2,$(FILES))
                => src1.c
        $(word 10,$(FILES))
                =>
----------------------------------------------------------
wordlist関数
        $(wordlist 2,4,$(FILES))
                => src1.c src2.o src1.o
        $(wordlist 2,10,$(FILES))
                => src1.c src2.o src1.o a.out
----------------------------------------------------------
words関数
        $(words $(FILES))
                => 5
----------------------------------------------------------
firstword関数
        $(firstword $(FILES))
                => src2.c
        $(word 1,$(FILES))
                => src2.c
----------------------------------------------------------
lastword関数
        $(lastword $(FILES))
                => a.out
        $(word 1,$(FILES))
                => a.out
# 
  • 実行結果

    実行結果

このMakefileはエスケープの書き方としても参考になると思うので、いくつか自分で入力して試してみていただきたい。プログラミングする際に備えて、覚えておいて損はないはずだ。

あまり複雑にしないようにしよう

Makefileの基本的なルールを身に付け、関数を覚えれば、かなり複雑な記述ができるようになっている。抽象度を上げ、より汎用的に使えるMakefileが書けるはずだ。

しかし何度も説明しているように、Makefileは便利なツールではあるのだが、読みやすいシンタックスではない。いくらでも複雑に書くことができるため、他人が読んで理解しにくいどころか、未来の自分が見ても意味がわからないことがよくあるのだ。

使える機能をたくさん知ったとしても、闇雲にそれらをすべて使えばよいというものではない。常にわかりやすいようにシンプルに書くように心掛ける、これが将来役立つのだ。

参考