Makefileと変数
これまで数回に渡り、Makefileで使える変数の特性や使い方などについて取り上げてきた。「実行時に評価される」という特徴を持っていることや、後から値を追加して使えることなど、現在主流のプログラミング言語で使っている変数とは少しばかり違うところがあることをお分かりいただけているはずだ。
変数についての説明は、今回でいったん終了となる。節目となる今回は、変数にアクセスする段階で拡張子を変更したり、文字列を置換したりする方法について取り上げる。
前回、ファイルパスが含まれた自動変数からディレクトリとファイル名を個別に取り出す方法を説明した。同じようなやり方で変数を参照する際、その変数の値をファイル名と仮定して拡張子を変更することができる。また、もうちょっと汎用的に、文字列の置換を行った後の文字列にアクセスすることもできる。
「拡張子を変更する機能」というのは、Makefileに特有の機能だと言える。こうした機能を持つのは、そもそもMakefileがビルドに使われるツールだからだ。C言語で書かれたソースコードファイル(拡張子は「.c」)をコンパイルすると、通常は「.o」という拡張子のオブジェクトファイルが生成される。こういった動作に合わせるために、拡張子だけを変更して変数にアクセスするという機能が用意されているのだ。
こうした機能は、Makefileを作業の整理目的で使う場合にはあまり用がないかもしれない。とは言え、この機能を使って記述されているMakefileは多いので、動作を知っておいて損はない。こういった書き方ができるんだな、ということを把握しておいていただきたい。
今回紹介する機能は実際の動作を見るとわかりやすいので、以降では、サンプルを見ながら説明していこう。
変数:拡張子を変更しながらの参照
まず最もシンプルな使い方をみてみよう。次のようなMakefileを用意する。
SRC:= src1.c src2.c src3.c header.h
test:
echo $(SRC)
echo $(SRC:.c=.o)
$(SRC)には「src1.c src2.c src3.c header.h」が収められている。この変数を$(SRC:.c=.o)のようにアクセスする。「:.c=.o」という表記が含まれているわけだが、この部分が「拡張子.cを拡張子.oへ置換する」という指定になっている。
実行すると次のようになる。
実行サンプル
% make
echo src1.c src2.c src3.c header.h
src1.c src2.c src3.c header.h
echo src1.o src2.o src3.o header.h
src1.o src2.o src3.o header.h
%
変数に含まれているファイルのうち、拡張子が「.c」のものが「.o」として表示されていることがお分かりいただけるだろう。変数そのものは書き換わっていない。変数にアスセスして得られる結果が、拡張子が書き換わったものになっている。
これは一番シンプルな書き方だが、GNU Makefileではもう少し細かい書き方ができる。
変数:置換をしながらの参照
先ほどと同じ拡張子の置換を、もう少し細かい書き方をするために置き換えると次のようになる。
SRC:= src1.c src2.c src3.c header.h
test:
echo $(SRC)
echo $(SRC:%.c=%.o)
「$(SRC:.c=.o)」という表記が「$(SRC:%.c=%.o)」になっている。「%」という表記はワイルドカードだ。拡張子までの全ての文字列に一致するので、結局「:.c=.o」という書き方と意味が同じになる※1。
この表記を正規表現と後方参照に置き換えるとすれば、「^(.*)[.]c$」というパターンに一致した文字列を「\1.o」に置き換える、といった感じになる。パターンの(.*)が1つ目の「%」であり、置換後の「\1」が2つ目の「%」を意味するわけだ。正規表現にすると意味がわかりにくくなるかもしれないが、「:%.c=%.o」という表記でなんとなく「%」が意味することは予想できるのではないか、と思う。
実行すると次のようになる。
% make
echo src1.c src2.c src3.c header.h
src1.c src2.c src3.c header.h
echo src1.o src2.o src3.o header.h
src1.o src2.o src3.o header.h
%
ワイルドカード「%」を使った指定は拡張子の書き換えにとどまらず、汎用的に使用できる。例えばMakefileを次のように書き換えてみる。
SRC:= src1.c src2.c src3.c header.h
test:
echo $(SRC)
echo $(SRC:%=/tmp/objs/%.o)
実行すると次のようになる。
% make
echo src1.c src2.c src3.c header.h
src1.c src2.c src3.c header.h
echo /tmp/objs/src1.c.o /tmp/objs/src2.c.o /tmp/objs/src3.c.o /tmp/objs/header.h.o
/tmp/objs/src1.c.o /tmp/objs/src2.c.o /tmp/objs/src3.c.o /tmp/objs/header.h.o
%
上記の書き方の場合、「%」だけしか書いていないので変数の値全てに一致することになり、それが「/tmp/objs/%.o」という表記で置き換え指定されているので、ディレクトリとして/tmp/objs/が追加され、ファイル名の最後に「.o」が追加されるという動作になっている。「.c」が「.o」に置き換わるのではなく、ただ追加されているところに注目しよう。指定が「%」しかないので「.c」も含めた文字列全部が使われているのだ。
では、Makefileを次のように書き換えてみよう。
SRC:= src1.c src2.c src3.c header.h
test:
echo $(SRC)
echo $(SRC:%.c=/tmp/objs/%.o)
実行すると次のようになる。
% make
echo src1.c src2.c src3.c header.h
src1.c src2.c src3.c header.h
echo /tmp/objs/src1.o /tmp/objs/src2.o /tmp/objs/src3.o header.h
/tmp/objs/src1.o /tmp/objs/src2.o /tmp/objs/src3.o header.h
%
今度は「%.c=/tmp/objs/%.o」という指定にしてあるので、「header.h」が置換の対象になっていない。header.hは最後が「.h」で終わっているので、「%.c」というパターンには一致しないのだ。
こんな感じで、Makefileでは変数にアクセスする際に置換処理をした結果にアクセスすることができる。Makefileの変数は1つの文字列ではなく、複数の文字列が含まれているものとして処理されるので、個々に値に対して置換処理が行われるのも大きな特徴だ。
※1 実際には「:.c=.o」という書き方が「:%.c=%.o」という表記の短縮表記になっているという説明の方が適切だ。
変数:置換をしながらの参照の実体は関数
そして「$(SRC:%.c=/tmp/objs/%.o)」という変数の書き方なのだが、実際には置換を行う「関数」を使った処理の別の書き方だったりする。こうした書き方ができるようになってはいるものの、GNUのMakefileでは、実際には次のような書き方になっている。
SRC:= src1.c src2.c src3.c header.h
test:
echo $(SRC)
echo $(patsubst %.c,/tmp/objs/%.o,$(SRC))
「$(SRC:%.c=/tmp/objs/%.o)」は実際には「$(patsubst %.c,/tmp/objs/%.o,$(SRC))」という関数を使った処理の別の書き方だ。
実行すると次のようになる。
% make
echo src1.c src2.c src3.c header.h
src1.c src2.c src3.c header.h
echo /tmp/objs/src1.o /tmp/objs/src2.o /tmp/objs/src3.o header.h
/tmp/objs/src1.o /tmp/objs/src2.o /tmp/objs/src3.o header.h
%
GNU Makefileでは関数という機能を使うことができる。書き方は「$(関数 引数)」で、変数の書き方に似ている。
「$(patsubst 引数...)」というのはパターン置換を行うための関数だ。パターン置換(pattern substitution)で「patsubst」という名前になっている。GNU Makefileにはこのように文字列を編集するための機能のほか、繰り返し、シェルコマンド置換といった処理を関数を経由して行えるようになっている。これでMakefileで行える処理がぐっと広がってくる。
今回は関数についての説明は行わないが、このように変数参照が実際には関数で処理されていることは知っておくとよいだろう。
覚えてなくても知っているだけで読みやすくなる
これまでに説明してきた内容で、GNU Makefileの変数は結構理解できるようになったと思う。GNU Makefileは変数と関数が似たような書き方なので、もう少し読み方を学んでいく必要があるが、今の時点でも結構読めるようになっているはずだ。
Makefileを読む際、全てを細かく理解しなければならないということはない。それが変数なのか、関数なのか、意図として何をしようとしているのかがざっくり分かればOKだ。後は本当に詳しく知る必要があるときだけ、マニュアルを読み返して理解すればよい。
Makefileは読みにくいところがあるものの、仕組み自体はそれほど難しいものではない。少しずつで構わないので、機能を知っていこう。