make-filter-out - filter-out関数の使い方
目的
GNU make の関数 $(filter-out) の使い方をメモしておく。
結果
filter-outを使うと、変数内の特定の単語を、完全一致/前方一致/後方一致で取り除くことができる。
以下、詳細
filter-outとは
GNU Makeのmanual(GNU make: Text Functions)を参照すればまあ書いてあるのだが、textからwordにマッチしない要素を取り出す関数。text, wordともにスペース区切りで複数要素を指定可能。
たとえばMakefile中のCFLAGS指定などで、
CFLAGS = -O2 -c -fomit-frame-pointer -fstrength-reduce
などと指定されている時に、CFLAGSは流用したいけど、最適化はオフにしたい(-O2オプションはいらない)、という場合に使用する。
なにができるのか、試してみる
filter-outの実力を試すテストMakefileを作成した。
- filter-out.mk
- https://sssvn.jp/svn/spikelet/make/filter-out.mk
CFLAGS = -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os test: @echo ORIGINAL: $(CFLAGS) @echo 'filter-out with "-O" :' $(filter-out -O, $(CFLAGS)) @echo 'filter-out with "-O1" :' $(filter-out -O1, $(CFLAGS)) @echo 'filter-out with "-O2" :' $(filter-out -O2, $(CFLAGS)) @echo 'filter-out with "-O%" :' $(filter-out -O%, $(CFLAGS)) @echo 'filter-out with "%s" :' $(filter-out %s, $(CFLAGS)) @echo 'filter-out with "-f%r" :' $(filter-out -f%r, $(CFLAGS)) @echo 'filter-out with "%s%" :' $(filter-out %s%, $(CFLAGS)) @echo 'filter-out with "-f% -O%" :' $(filter-out -f% -O%, $(CFLAGS))
実行結果は以下:
% make -f filter-out.mk ORIGINAL: -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os filter-out with "-O" : -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os filter-out with "-O1" : -O -O2 -c -fomit-frame-pointer -fstrength-reduce -Os filter-out with "-O2" : -O -c -O1 -fomit-frame-pointer -fstrength-reduce -Os filter-out with "-O%" : -c -fomit-frame-pointer -fstrength-reduce filter-out with "%s" : -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce filter-out with "-f%r" : -O -O2 -c -O1 -fstrength-reduce -Os filter-out with "%s%" : -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os filter-out with "-f% -O%" : -c
考察:
- wordは完全一致させないといけない("-O"では"-O1"などが取り除かれない)
- "%"を使えば、前方/後方/前方と後方一致もできる ("-O%"で"-O1", "-O2", "-Os"がすべて取り除かれている)
- "%"は空文字列にもマッチする ("-O%" で "-O" も取り除かれる)
- "%"を2回以上使うことはできない ("%s" ではなにも取り除かれない)
- 削除対象のwordが複数個でも "%" は普通に使える
make のソースを見てみる
filter-out が実際になにをしているのか、興味本位で解析してみる。
また、Make中での関数の定義方法、呼び出し方もついでの抑えておく。
対象バージョン
GNU make 3.80(RHEL4のmakeのバージョン)。
わかったことの概要
- Makeのビルトイン関数処理
- 各関数は、function.c の function_table_init[] に定義されている(ここ以外にはない)
- 処理関数(のポインタ)は function_table_init の最後のメンバーで、全て function.c に書かれている
- 関数の引数が少ない場合はエラーになるが、多い場合は後ろの方を1つの引数として処理する
- Make内部では、ハッシュを使って関数名でのエントリ検索を高速化している
- ビルトイン関数は、実は $(call..) を使っても呼ぶことができる
- 関数名に '_' は使えない(英小文字とハイフンだけ)
- 処理関数の引数は、(1)展開結果バッファのWrite Pointer、(2)引数の配列、(3)呼び出された関数名、を受け取る
- 変数名の展開結果などは、variable_buffer_output() を読んで書き出す
- 処理関数の返り値は、展開結果バッファのWrite Pointerを返す(展開結果などを返すわけではない)
処理中のMakefileのポインタ
Makeの関数定義の場所
Makeの関数はソース中の function.c の function_table_init 配列に置いてある。
struct function_table_entry { const char *name; /* 関数名 */ unsigned char len; /* 関数名の長さ(hash中での比較の高速化のため) */ unsigned char minimum_args; /* 引数の個数の最小値 */ unsigned char maximum_args; /* 引数の個数の最大値(0ならば無制限) */ char expand_args; /* 実行前に全引数を展開するか?(foreachなどは展開されると都合が悪いので0にする) */ char *(*func_ptr) PARAMS ((char *output, char **argv, const char *fname)); /* 関数本体 */ }; ... static struct function_table_entry function_table_init[] = { /* Name/size */ /* MIN MAX EXP? Function */ ... { STRING_SIZE_TUPLE("filter-out"), 2, 2, 1, func_filter_filterout},
関数のハッシュテーブル
Make内では関数の検索がしやすいように、配列からハッシュテーブルを作成している。
作成箇所: function.c 内の hash_init_function_table() 内で
static struct hash_table function_table; .... void hash_init_function_table () { hash_init (&function_table, FUNCTION_TABLE_ENTRIES * 2, function_table_entry_hash_1, function_table_entry_hash_2, function_table_entry_hash_cmp); hash_load (&function_table, function_table_init, FUNCTION_TABLE_ENTRIES, sizeof (struct function_table_entry)); }
hash関連の関数は hash.c 内にある。
hash_init()でMakeの関数テーブルを初期化する。function_table は 2のべき乗のサイズを持つハッシュ。function_table_entry_hash1/2がhash関数で、hash2は強制的に奇数の値を算出する(ハッシュテーブルを効率良く使うため)。function_table_entry_hash_cmpは、ハッシュのキーを文字列として比較する関数。
hash_load()で、function_table_initの内容をhashに入れる。
lookup_function() で文字列からハッシュを検索して、該当する function_table_entry を返す(見つからなければ0を返す)。
関数名は、正規表現で書くと、/^[a-z-]+[\0\t ]/ にマッチすること。
関数の呼び出し
Make関数の呼び出しは、二通りの方法がある(ことを今知った)。
一つは、$(func ...)形式、もう一つはユーザ定義関数を呼び出す call 関数内でMakeのビルトイン関数を指定した場合。
どちらも最終は expand_builtin_function() に飛ぶので、$(func)形式の時の処理だけを示す:
- variable_expand_string() in expand.c
- '$'の次が '('か'{'なら、とりあえず関数か試すために handle_function()を呼ぶ (返り値が非0なら=関数だったら、展開完了)
- handle_function() in function.c
- 関数名をハッシュテーブルで検索して、無ければ return 0
- 引数の解析して、対応する閉じカッコ(')' か '}')が無ければ return 0
- この時、maximum_args より引数が多いときは、セパレータのコンマ以降も一つの引数とみなしてくっつける(『引数が多過ぎる』エラーは出さない)
- expand_args が非0なら、引数を展開する(展開は関数・変数どちらも行うので、入れ子の関数もここで処理する)
- expand_builtin_function() 呼出し (関数展開が成功しても失敗しても、handle_function は return 1)
- expand_builtin_function() in function.c
- minimum_argsのチェック、少なすぎたらfatal()で異常終了
- func_ptr(Make関数の実体)がNULLなら (unimplementedなら) fatal()で異常終了
- Make関数の実体(関数ポインタ)を呼ぶ
filter-outの場合
func_ptrは func_filter_filterout()。
これは、filterとfilter-outで同じ関数で処理している。
- func_filter_filterout() in function.c の処理
- pattern(第一引数)の各要素取り出し、'%'があるかもチェック(最初にある'%'のみチェック。%はバックスラッシュでquoteできる)
- word(第二引数)の各要素取り出し
- 検索にhashを使った方が効率的と判断したら、wordのハッシュテーブルを作る(patternに'%'無しのリテラルが2個以上、など)
- 各patternごとに、wordの各要素と比較 ('%'を含む場合は pattern_matches()で比較するが、やはり '%' は1つしか想定していない)
- 呼びだし関数名が "filter" ならマッチしたwordを、そうでなければマッチしなかったwordを展開結果として出力 (variable_buffer_output()での出力)
- 関数の返り値は、出力バッファのWrite Pointer
他に気付いたこと: GNU Makeのコーディングスタイル
3.80 は K&R スタイル(引数の型は関数定義のカッコの外に書く)。ただし、一部 void 型などは使っている。
3.81 は ANSI スタイル(引数の型と名前両方をカッコの中に書く)。
おそらく、GNU Make 自体のポータビリティを高めるために K&R スタイルで書いていたんだろうなあ。
3.81 では、ansi2knr.c という 関数宣言のANSIスタイル→K&Rスタイル変換ツールが入っていて、configure時にコンパイラがK&Rしか対応していないかをチェックし、その場合にはansi2knrを通してソースを整形してからコンパイルする、という処理が入っている。ご苦労さまです。