在 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。
其實 Make functions 沒有特別強大,但好處是這些函式不依賴外界工具,因此可以跨平台。
由於大部分的程式設計者對 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 的巨集來取代內建函式,其實功能反而更加豐富。