GNU Make 相容的 Makefile 教學:Make 內建函式

PUBLISHED ON JUL 3, 2018 — BUILD AUTOMATION

在 GNU Make 4.0 版之前,make 的程式語言相關的特性相對單薄,如果和 Rake 或 Gradle 等新興的軟體編譯自動化方案比起來更是如此。在 GNU Make 4.0 版之後,可 (選擇性的) 將 Guile 內嵌在 make 執行檔中,藉此增強 Makefile 在程式語言相關的特性。然而,目前有一些系統上的 GNU Make 仍停留在 3.8 版,即使升到 4.0+ 版以後,往往也沒有將 Guile 編進去,故本文不考慮 Guile 這一部分,而以 3.8 版中已有的語法為主。

Make functions 是一組具有 LISP 風格的函式,這些函式主要的功能是進行一些簡單的文字處理,用來處理檔案名稱等。比起真正的函式庫,這些函式功能其實沒有特別強大,畢竟,在 Makefile 中塞入一整套程式語言並不是 Make 原本的意圖。像是 pastsubst 能用的 pattern 僅有 % (表萬用字元) 而已,如以下實例:

$(patsubst %.c,%.o,foo.c bar.c baz.c)

會得到 foo.o bar.o baz.o,因為將字尾的 .c 置換成 *.o*。

由於大部分的程式設計者對 LISP 相對陌生,筆者設計了 mktext 這個具有惡趣味的小程式,用來展示如何使用 Make functions 進行簡單的字串處理。

以下實例使用 mktext 進行排序:

$ ./mktext sort b d a e c
a
b
c
d
e

以下實例用 mktext 過濾不要的元素:

$ ./mktext filter "a b c" a b c d e f g
d
e
f
g

更多的使用方式,可到 mktext 的專案網站觀看。接下來,我們會說明 mktext 的實作。

由於 Make 本身無法妥善地處理命令列參數,我們將 Makefile 內嵌在 shell 命令稿中,藉由 shell 的功能處理命令列參數。程式架構如下:

#!/bin/sh

# Init the program.

# Parse command-line arguments.

# Embed Makefile and run it with `make`

初始化程式的部分僅是設置一些變數,請讀者自行前往專案網站觀看。

節錄剖析命令列參數的程式碼如下:

# Extract first parameter as action.
ACTION=$1; shift;

# Parse arguments for the actions *all*, *any*, *filter* and *select*.
if [ "$ACTION" == "all" ] || [ "$ACTION" == "any" ] \
	|| [ "$ACTION" == "filter" ] || [ "$ACTION" == "select" ]; then
	COND=$1; shift;
fi

mktext 中,第一個參數是程式的 action,也就是程式的子命令 (subcommand)。接著,按照不同的 action 陸續取出後續的參數。

關鍵的程式在以下這一行:

MAKE=`which make`

cat << END | $MAKE -f - -- $ACTION

# Embed Makefile here.

END

我們將整個 Makefile 以 here document 的方式內嵌在 shell 命令稿中,接著,將其傳給 Make 來執行。由這段程式碼可看出,其實我們先前的 action 會變成 Make 的 target。-f - 的意思是將先前的輸出做為暫存檔後當成本命令的設定檔。-- 之後的參數不是 Make 本身的參數,而是一般的參數,所以我們傳入 --help 時不會呼叫 make 本身的幫助文件,而會呼叫 mktext 中相對應的程式碼。

我們來看 sort action 如何撰寫:

SPACE=\$(empty) \$(empty)
NEWLINE=\\\\n

sort: OUT := \
	\$(strip \
		\$(subst \$(SPACE),\$(NEWLINE),\
			\$(sort $@)))
sort:
	@if ! [ -z \$(OUT) ]; then \
		printf "\$(OUT)\n"; \
	else \
		printf ""; \
	fi

由於本例的 Makefile 是內嵌在 shell 命令稿中,需要避開特殊符號,所以寫起來和原本的 Makefile 略有不同。

sort action 中,我們先用 Make functions 將傳入的參數排序後,將結果指派到變數 OUT 中,在指令中檢查 OUT 是否為空,若不為空則印出。

我們將 Make functions 的部分節錄並去除跳脫字元:

$(strip \
	$(subst $(SPACE),$(NEWLINE),\
		$(sort $@)))

這段程式的讀法是由內和外,在本例中,我們執行了三個 Make functions,依序是 sort -> subst -> strip。我們先將命令列參數以 sort 排序後,再將排序結果的空白替換成換行,最後再去掉頭尾多餘的空白。這樣的目的是為了將結果以 Unix 風格印出。

初心者在撰寫 LISP 風格的程式碼時會對許多的括號產生恐懼感,其實不用過度擔心。現在的編譯器都會協助我們檢查括號是否有對稱,不需要人工逐一檢查,其實不太會寫錯程式碼。

接著來看 filter action 的實作:

filter: OUT := \
	\$(strip \
		\$(subst \$(SPACE),\$(NEWLINE),\
			\$(filter-out $COND,$@)))
filter:
	@if ! [ -z \$(OUT) ]; then \
		printf "\$(OUT)\n"; \
	else \
		printf ""; \
	fi

在本段程式碼中,我們依序執行 filter-out -> subst -> strip 等三個 Make functions,整體的效果是將命令列參數中不要的元素去除。

接著來看 any action 的實作:

any: PRED := \
	\$(strip \
		\$(filter-out false,\
			\$(foreach v,$@,\
				\$(if \$(filter \$(v),$COND),\
					true,\
					false))))
any:
	@if ! [ -z "\$(PRED)" ]; then \
		echo true; \
	else \
		echo false; \
	fi

在本段程式碼中,我們以 foreach 將命令列參數進行迭代,在每一次迭代中,我們在 if 中用 filter 來「檢查」該參數是否和條件相等,若 filter 傳回非空字串,代表符合條件,若條件相符則回傳 true,條件不符則回傳 false。

接著,我們用 filter-out 將 false 過濾掉,最後再用 strip 將多餘的空白去除,最後,若 OUT 不為空字串,代表元素中有符合條件的元素,故回傳 true,反之,則回傳 false。

mktext 還有實作其他的 actions,有興趣的讀者可自行前往該專案網站觀看。透過本文,讀者應該可以了解 Make functions 如何使用。如果在類 Unix 系統上,也可用系統上的命令列工具搭配 Make 的巨集來取代內建函式,其實功能反而更加豐富。

comments powered by Disqus