まずは、patsubst の使い方から。
SRCS = main.c util.c parser.c
OBJS = $(patsubst %.c,%.o,$(SRCS))
これで OBJS は main.o util.o parser.o になります。% がワイルドカードで、「.c で終わる各単語を、同じ名前の .o に置き換える」という意味です。この記事は patsubst を軸に、周辺でよく一緒に使う wildcard filter foreach までを逆引きでまとめたチートシートです。挙動はすべて手元の GNU Make で実行して確認しています(macOS標準の 3.81 と Homebrew の 4.4.1。関数の意味はこの範囲で違いはありませんでした)。
網羅的な仕様は GNU Make 4.4 日本語マニュアル 第8章「テキストを変換する関数」 にあります。この記事はその逆引き版です。
patsubst: パターン置換の基本
$(patsubst 検索パターン,置換パターン,対象の文字列) の3引数です。対象の文字列を空白で区切った単語ごとに、検索パターンにマッチしたものだけを置換パターンに差し替えます。
SRCS = main.c util.c parser.c
OBJS = $(patsubst %.c,%.o,$(SRCS))
demo:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
この記事の出力例は、どれもこの形の Makefile を make demo で実行した結果です。
SRCS = main.c util.c parser.c
OBJS = main.o util.o parser.o
% は「任意の文字列」にマッチし、検索側の % にマッチした部分(stem と呼びます)が置換側の % にそのまま入ります。上の例では main util parser がそれぞれ stem で、後ろの拡張子だけが .c から .o に変わっています。
% を使わずに書くと、単語まるごとの完全一致になります。
FILES = main.c util.c parser.c
demo:
@echo "$(patsubst util.c,util.o,$(FILES))"
main.c util.o parser.c
util.c という単語だけがぴたり一致して util.o になり、他はそのままです。拡張子の一括変換ではなく、特定のファイル名だけを差し替えたいときはこの形が使えます。
patsubstの略記: $(VAR:%.c=%.o)
拡張子を付け替えるだけなら、もっと短い書き方があります。変数参照の中に :置換 を書く形で、これを置換参照(substitution reference)と呼びます。
SRCS = main.c util.c parser.c
OBJS1 = $(SRCS:%.c=%.o)
OBJS2 = $(SRCS:.c=.o)
demo:
@echo "OBJS1 = $(OBJS1)"
@echo "OBJS2 = $(OBJS2)"
OBJS1 = main.o util.o parser.o
OBJS2 = main.o util.o parser.o
$(SRCS:%.c=%.o) は $(patsubst %.c,%.o,$(SRCS)) とまったく同じ結果です。さらに $(SRCS:.c=.o) のように % を省くと「末尾の .c を .o にする」という略記になり、これも同じ結果になります。末尾の付け替えだけならこの最短形が読みやすいので、Makefileでは $(SRCS:.c=.o) の形をよく見かけます。
途中にパターンを挟むこともできます。$(SRCS:src/%.c=build/%.o) と $(patsubst src/%.c,build/%.o,$(SRCS)) は同じ結果になります(手元の両バージョンで確認)。末尾の付け替えだけなら最短形の $(SRCS:.c=.o)、パターンを挟むなら % 形の置換参照か patsubst、と覚えておけば迷いません。
wildcard: ファイル一覧を取得する
$(wildcard パターン) は、シェルのグロブと同じ書き方で実在するファイルの一覧を返します。ソースファイルを手で並べる代わりに使う定番です。
demo:
@echo "c files = $(wildcard *.c)"
@echo "src c = $(wildcard src/*.c)"
@echo "headers = $(wildcard *.h)"
c files = main.c parser.c util.c
src c = src/a.c src/b.c
headers = parser.h util.h
返ってくるのは実際に存在するファイルだけです。マッチしなければ空になります。結果はアルファベット順に並びます(GNU make のマニュアルにも wildcard の結果はソートされると明記があります)。重複を除きたいときは $(sort $(wildcard *.c *.h)) のように sort を重ねられます。wildcard はマニュアルだと関数の章ではなく 第4章「ルールの書き方」 側で説明されていますが、実務では patsubst とセットで使うことがほとんどです。
filter / filter-out: パターンで抽出・除外する
一覧から欲しいものだけを取り出すのが filter、要らないものを捨てるのが filter-out です。$(filter パターン,対象) の形で、パターンはスペース区切りで複数指定できます。
FILES = main.c util.c parser.h util.h README.md
demo:
@echo "only c = $(filter %.c,$(FILES))"
@echo "c and h = $(filter %.c %.h,$(FILES))"
@echo "no headers = $(filter-out %.h,$(FILES))"
only c = main.c util.c
c and h = main.c util.c parser.h util.h
no headers = main.c util.c README.md
filter %.c %.h のようにパターンを2つ並べると、そのどれかにマッチする単語が残ります。wildcard でまとめて拾ったファイル群から、拡張子で C ソースだけ選ぶ、といった場面で効きます。
foreach: リストの各要素に処理をかける
$(foreach 変数,リスト,処理) は、リストの各単語を順に変数へ入れながら処理を評価し、結果をスペースでつないで返します。
DIRS = src lib test
demo:
@echo "$(foreach d,$(DIRS),$(d)/*.c)"
src/*.c lib/*.c test/*.c
d に src lib test が順に入り、それぞれ $(d)/*.c に展開されて連結されます。複数のディレクトリからソースを集めたいときに wildcard と組み合わせると便利です。
実務でよく使う組み合わせ
ソース一覧 → オブジェクト一覧を自動生成する
いちばん出番が多いのがこれです。wildcard で .c を全部拾い、patsubst(または略記)で .o の一覧に変換します。
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))
demo:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
SRCS = main.c parser.c util.c
OBJS = main.o parser.o util.o
ソースを1本足しても Makefile を書き換えずに済むので、小さなプロジェクトの雛形として使い回せます。
複数のディレクトリからまとめて集める
ソースが1階層に収まらないときは、foreach で各ディレクトリに wildcard をかけます。
DIRS = . src
SRCS = $(foreach d,$(DIRS),$(wildcard $(d)/*.c))
OBJS = $(SRCS:.c=.o)
demo:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
SRCS = ./main.c ./parser.c ./util.c src/a.c src/b.c
OBJS = ./main.o ./parser.o ./util.o src/a.o src/b.o
foreach が各ディレクトリの wildcard 結果を連結し、そのまま置換参照で .o に変換しています。
つまずきポイント
%が効くのは検索・置換パターンで1個だけです。2個目の%はただの文字として扱われます。たとえば$(patsubst %.%,X,main.c util.c)は、パターンが「任意の文字列+.%」という意味になり、.%を含む単語が無いので何も置換されずmain.c util.cのまま返りました(手元で確認)。%を2つ書いてうまく置換されないときは、これを疑ってください。- 区切りは空白です。make の関数は対象をスペース区切りの単語リストとして扱うので、カンマで区切ったり、パスにスペースが混ざったりすると、意図しないところで分割されます。
patsubstとsubstは別物です。substはパターンを使わない単純な文字列置換で、単語の途中だろうと問答無用で置き換えます。たとえば$(subst o,0,foo.o)はf00.0になります(拡張子の.oだけでなくfooの o まで置き換わる。手元で確認)。
demo:
@echo "subst = $(subst .c,.o,main.c util.c)"
@echo "findstring 1 = [$(findstring util,main.c util.c)]"
@echo "findstring 2 = [$(findstring xyz,main.c util.c)]"
subst = main.o util.o
findstring 1 = [util]
findstring 2 = []
subst は $(subst 検索文字列,置換文字列,対象) で、% を使いません。拡張子の付け替えのように「末尾だけ」を狙うなら patsubst(や略記)の方が安全です。findstring は文字列が含まれていればその文字列を、無ければ空を返すので、ifeq などと組み合わせて「特定の語を含むか」の判定に使います。条件分岐の書き方は Makefileの条件分岐 ifeq/ifdef 完全ガイド にまとめています。
さいごに
patsubst は「%.c を %.o に」の拡張子付け替え、その最短形が $(SRCS:.c=.o) の略記、というところだけ押さえれば大半の Makefile は読めます。ファイル収集は wildcard、選り分けは filter、繰り返しは foreach、と役割で覚えると迷いません。自動変数($@ $< $^)や := = の使い分けは Makefile変数チートシート に、関数の網羅的な仕様は GNU Make 4.4 日本語マニュアル にまとまっています。