當你開始以 make 變數取代簡單的常數之後, 你將發現自己越來越想以複雜的方式來操作變數以及它們的內容. GNU make 提供不少內建函式可用來操作變數和它們的內容. make 的函式可以分類為: 字串操作, 檔名操作, 流程控制, 使用者自訂函式 以及若干 (重要的) 雜項函式.
但首先你應該多知道一些函式的語法, 所有的函式都會具備有如下的形式:
$( 之後式內建函式的名稱, 接著是函式的引數. 第一個引數的前導空白會被刪除, 但是後續的任何引數若包含的前導 (當然也包含內嵌的和跟在後面的) 空白則都會被保留下來! 函式引數是以逗號為分隔符, 所以只有一個引數的函數並不需要使用逗號, 具有兩個引數的函式需要一個逗號並以此類推. 許多只接受一個引數的函式會把它的引數視為一串以空格隔開的單字. 對這些函數而言, 它們會以空白作為分隔符.
作者喜歡空白, 因為它可以讓函式的描述較具可讀性並且容易維護. 然而有時空白在引數串列或變數定義中可能會讓 make 無法正確解讀我們所做的描述. 這個時候你沒有多少選擇, 只能移除有問題的空白! 在之前看過一個例子, 在該例子中一個結尾的空白會被意外地插入到 grep 命令的搜尋樣式裡. 後面還會看到更多例子, 屆時我們會指出問題在哪.
make 有需多函式允許你以樣式為引數, 此樣式的語法如同樣式規則中所使用的樣式 (參見 2.4 節的樣式規則). 樣式之中可以包含一個 % 符號, 以及前導或跟在後面的字符 (或者 both). % 字符代表零或多個任何類型的字符. 進行工作目標字串比對時, 此樣式必須比對整個字串, 而不只是字串的子集. 稍後我們會舉例以說明. 在樣式中, % 字符是一個選項, 通常你可以適時予以省略.
字串函式
make 的內建函式大部分都可以操作兩種形式的文字, 不過有些函式具有特別強的字串處理能力, 這就是這裡要探討的內容. 在 make 中常見的字串操作就是從一份檔案清單選出一組檔案來. shell script 中之所以會常用到 grep 就是這個原因. 在 make 中我們有 filter, filter-out 和 findstring 等函式可以使用. 首先來看 filter:
filter 函式會將 text 視為一系列被空格隔開的單字, 與 pattern 比對之後, 接著會傳回相符者. 舉例來說, 為了建立使用者介面 (user-interface) 的程式庫, 我們可能只想從 ui 子目錄中選出目的檔. 接下來我們將會從檔名清單中取出開頭為 ui/ 而結尾為 .o 的檔案. % 字符將會相符其間任何數目的字符:
- $(ui_library): $(filter ui/%.o,$(objects))
- $(AR) $(ARFLAGS) $@ $^
- Make3370_1
- words := he the hen other the%
- get-the:
- @echo he matches: $(filter he, $(words))
- @echo %he matches: $(filter %he, $(words))
- @echo he% matches: $(filter he%, $(words))
- @echo %he% matches: $(filter %he%, $(words))
第一個樣式只會相符單字 'he', 因為該樣式必須比對整個單字, 而不是單字的一部分. 樣式之中只能包含一個 % 字符. 如果樣式包含了額外的 % 字符, 第一個 % 字符除外, 其餘 % 字符都會被視為自面字符 (literal character). filter 無法在單字裡比對子字串而且只接受一個通配字符, 這看起來或許有些奇怪, 當你需要特殊比對的功能不存在, 可能會需要一些 tricky 的做法. 不過你可以透過 "迴圈" 和 "條件測試" 來實作你想要的比對. (稍後會介紹) 接著來看 filter-out 函數:
filter-out 函式所做的事剛好跟 filter 相反, 用來選出不相符的每個單字. 所以下面的例子可以從檔名清單中選出所有非 C 標頭檔的檔案:
- all_source := count_words.c counter.c lexer.l counter.h lexer.h
- to_compile := $(filter-out %.h, $(all_source))
此函式會將 text 裡搜尋 string. 如果字串找到了, 此函式就會傳回 string; 否則它會傳回空值. 乍看之下此函式有點像字串搜尋函式 grep, 但並非如此, 首先且最重要的是此函數的回傳值只是 "搜尋字串", 而不是比對到的單字, 其次 "搜尋字串" 無法包含通配字符 (就算你在 "搜尋字串" 中使用 % 字符, 也是按照字面比對). 此函式通常會跟梢後討論的 if 函式一起使用. 不過下面狀況適合單獨使用此函式:
- Make3370_2
- # Figure out which path is contained by current working directory
- find_tree:
- # PWD = $(PWD)
- # $(findstring /test/book/admin, $(PWD))
- # $(findstring /root, $(PWD))
接著我們要來看兩個 "搜尋與代換" 的函式, 首先是 subst:
這是一個不具通配符能力的 "搜尋與替換" 函式. 它最常被用在檔名清單中將一個副檔名替換成另一個副檔名:
- sources := count_words.c counter.c lexer.c
- objects := $(subst .c,.o,$(sources))
- Make3370_3
- sources := count_words.c counter.c lexer.c
- objects := $(subst .c, .o, $(sources))
- showObjs:
- @echo ${objects}
這不是我們預期的結果! 問題出在 .o 引數之前的空格是 "代換字串" 的一部分, 所以擺在 .c 引數前的空格沒有問題, 因為第一個引數之前的任何空白符號都會被 make 移除. 事實上 $(sources) 之前的空格也是能避免則避免, 雖然在這個範例並無問題. 最後請注意 subst 並不知道什麼是副檔名, 它只知道字符所構成的字串, 所以只要 $(sources) 中只要出現 .c 字樣就會被替換掉. 例如檔名 car.cdr.c 將會被轉換成 car.cdr.o. 或許這不是你想要的結果. 接著來看 patsubst:
這是一個具備通配符能力的 "搜尋和替換" 函式. 照例此處比對樣式只可包含一個 % 字符. replace-pattern 中的百分比符號會被展開與樣式相符的文字. 切記 search-pattern 必須比對 text 的整個值. 例如下面範例將只會刪除 text 裡結尾的斜線符號, 而不是每個 text 中的每個斜線符號:
- strip-trailing-slash = $(patsubst %/,%,$(directory-path))
search 可以是一個簡單的字串; 如果是這樣只要該字串出現在一個單字的結尾 (亦即後面接著空白符號或變數值的結尾), 就會被替換成 replace. 此外 search 可以包含一個代表通配符 %: 如果是這樣, make 會依照 patsubst 的規則進行搜尋和替換的操作. 這個功能的語法相對 patsubst 來說可讀性較差. 一個測試範例如下:
- Make3370_4
- TEST_VAR := hellojohn
- TEST_VAR2 := $(TEST_VAR:john=peter)
- ShowTest:
- @echo "TEST_VAR=$(TEST_VAR)"
- @echo "TEST_VAR2=$(TEST_VAR2)"
如我們先前所見, 變數通常會包含一串單字, 接下來我們看到可以從一份清單中選出所需要的單字的函式, 計算清單長度的函數等等. 如同所有其他的 make 函式, 單字清單中式以空白作為分隔符.
此函式會回傳 text 中單字的數目. 一個使用範例如下:
- Make3370_5
- CURRENT_PATH := $(subst /, ,$(HOME))
- words:
- @echo My HOME path has $(words $(CURRENT_PATH)) directories.
此函式會回傳 text 中第 n 個單字. 第一個單字的編號是 1. 如果 n 的值大於 text 中的單字個數, 此函式會傳回空值. 一個範例如下:
- Make3370_6
- VERSION_LIST := $(subst ., ,$(MAKE_VERSION))
- MINOR_VERSION := $(word 2, $(VERSION_LIST))
- ShowVar:
- @echo VERSION_LIST=$(VERSION_LIST)
- @echo MINOR_VERSION=$(MINOR_VERSION)
此函式會傳回 text 中的第一個單字, 功能等同於 $(word 1,text). 一個使用範例如下:
- Make3370_7
- VERSION_LIST := $(subst ., ,$(MAKE_VERSION))
- MAJOR_VERSION := $(firstword $(VERSION_LIST))
- ShowVar:
- @echo VERSION_LIST=$(VERSION_LIST)
- @echo MAJOR_VERSION=$(MAJOR_VERSION)
此函數傳回 text 中範圍從 start (含) 到 end (含) 的單字. 如同 word 函式, 第一個單字的編號是 1. 如果 start 的值大於單字個數, 則含式傳回空值. 如果 end 的值大於單字個數, 則函式回傳自 start 開始到最後的單字清單. 一個使用範例如下:
- Make3370_8
- USER_NAME := john
- PGU = $(wordlist 3,4, \
- $(subst :, , \
- $(shell grep "^$1:" /etc/passwd)))
- Show:
- @echo Demo:
- # $(call PGU,$(USER_NAME))
重要的雜項函數
在我們使用這些函數來管理檔名之前, 讓我們先來了解兩個非常有用的函式: sort 和 shell
sort 函式會排序它的 list 引數並且移除重複的項目. 此函式執行之後會回傳依照字典排序之下的單字清單, 並且以空白作為分隔符. 此外此函式還會移除前導與接在後面的空格. 一個簡單範例如下:
因為 sort 函式是由 make 直接實作, 所以它並不支援 sort 程式所提供的命令列參數.
shell 函式的引數會被展開 (就像所有其它的引數) 並傳遞給 subshell 執行. 然後 make 會讀取 command 的標準輸出, 並將之傳回成函式的值. 輸出中所出現一連串的換行符號會被縮減成單一空白符號. 任何接在最後面的換行符號都會被刪除. 標準錯誤以及任何程式的結束狀態都不會被傳回. 一個測式範例如下:
- Make3370_9
- STDOUT := $(shell echo normal message)
- STDERR := $(shell echo error message 1>&2)
- SHELL_VALUE:
- # $(STDOUT)
- # $(STDERR)
因為 shell 函式可用來調用任何外部程式, 所以使用時應該注意小心. 特別是要考慮簡單變數與遞迴變數之間的差異. 考慮範例如下:
- START_TIME := $(shell date)
- CURRENT_TIME = $(shell date)
現在我們的工具箱已經能夠撰寫出極為有用的函式. 考慮下面函式 has-duplicate 可用來測試一個值是否包含重複的內容:
- Make3370_10
- has-duplicate = $(filter \
- $(words $1),\
- $(words $(sort $1)))
- WORD_LIST = john peter mary
- DWORD_LIST = john peter mary john
- Show:
- @echo WORD_LIST='$(WORD_LIST)' has duplicate? $(call has-duplicate,$(WORD_LIST))
- @echo DWORD_LIST='$(DWORD_LIST)' has duplicate? $(call has-duplicate,$(DWORD_LIST))
另一個常見的用法是可以使用時間戳記產生檔名:
- RELEASE_TAR := mpwm-$(shell date +%F).tar.gz
- RELEASE_TAR := $(shell date +mpwm-%F.tar.gz)
makefile 的撰寫通常會花許多時間在檔案的處理上, 因此有許多相關的 make 函式提供來處理此類工作.
之前提過通配符可以使用在工作目標, 必要條件以及命令稿等語境中, 但如果我們想將此功能用在其他語境, 例如變數定義, 該怎麼辦? 使用 subshell 來展開樣式可以達成目的, 但式執行起來會非常慢. 此時我們可以借用 wildcard 函式, 例如:
- Make3370_11
- SOURCES := $(wildcard *.c *.h)
- SHOW:
- @echo SOURCES='$(SOURCES)'
dir 函式會回傳 list 中每個單字代表路徑的目錄部分. 下面使用者自訂函式會傳回包含 C 原始檔的每個子目錄:
- source-dirs = $(sort \
- $(dir \
- $(shell find . -name '*.c')))
此函式會傳回檔案路徑的檔名部分. 我們經常可以看到 dir 和 notdir 會被一起使用來產生必要的輸出. 舉例來說, 假設你必須在輸出檔所在的目錄執行自訂 shell 命令稿以產生輸出檔:
- $(OUT)/myfile.out: $(SRC)/source1.in $(SRC)/source2.in
- cd $(dir $@); \
- generate-myfile $^ > $(notdir $@)
此函式會傳回引數中每個單字的後綴 (即檔案名稱的副檔名). 例如下面的使用者自訂函式會將測試清單中所有單字是否具備相同的後綴:
- same-suffix = $(filter 1,$(words $(sort $(suffix $1))))
此函式是 suffix 函式的捕函式. basename 函式傳回的是不含後綴部分的檔案名稱. 呼叫 basename 之後任何前導的路徑元件都會被原封不動的保留下來. 一個簡單範例如下:
- Make3370_12
- FILES := /tmp/john.c g.c a.c d.h
- Show:
- @echo FILES='$(FILES)'
- @echo Suffix: $(suffix $(FILES))
- @echo Basename: $(basename $(FILES))
addsuffix 函式會將你所指定的 suffix 附加到 name 中所包含的每個單字之後. suffix 可以是任何值.
addprefix 是 addsuffix 的捕函式. 下面的使用者自訂函式可用來測試一組檔案是否存在而且不為空:
- Make3370_13
- SRC_FILES := ./src/a.c ./src/b.c
- SRC_FILES_TEST := ./src/a.c not_exist
- valid-files = test -s . $(addprefix -a -s ,$1) && echo 'yes' || echo 'no'
- Show:
- @echo SRC_FILES='$(SRC_FILES)' all exist?
- $(call valid-files,$(SRC_FILES))
- @echo SRC_FILES_TEST='$(SRC_FILES_TEST)' all exist?
- $(call valid-files,$(SRC_FILES_TEST))
join 是 dir 和 notdir 的捕函式. 此函數的引數是兩個串列: prefix-list 和 suffix-list, 它會把 prefix-list 的第一個元素與 suffix-list 的第一個元素銜接在一起, 然後把 prefix-list 的第二個元素與 suffix-list 的第二個元素銜接並以此類推.
流程控制
到目前為止我們所看到的函式有需多倍時做成針對串列 (清單) 進行處理, 所以即使不使用迴圈結構, 它們也能夠運作的很好. 然而如果不提供實際的迴圈運算符以及某種條件處理能力, make 的巨集語言將會受到非常大的限制! 還好 make 有支援以上所提到的兩種功能. 這一節還會談到 "無可挽回" 的 error 函式, 此函式顯然是流程控制中最極端的一種做法. 首先來看第一個流程控制 if 函式:
if 函式 (不要跟之前介紹的 ifeq, ifne, ifdef 和 ifndef 搞混) 會根據條件表示式 (conditional expression) 的求值結果, 從接下來的兩個巨集選一個出來進行展開的動作. 如果 condition 展開之後包含任何字符 (即使是空格), 那麼它的求值結果為 "真", 於是會對 then-part 進行展開的動作; 否則如果 condition 展開後空無一物, 那麼其求值結果為 "假", 於是會對 else-part 進行展開動作.
一個使用範例是測試 makefile 是否在 Windows 上執行藉以設定路徑的分隔符為 ";" 或是 Unix/Linux 的 ":", 方法很簡單. 尋找 COMSPECT 環境變數就行了, 因為只有 Windows 會定義此環境變數:
- PATH_SEP := $(if $(COMSPEC),;,:)
- $(if $(filter $(MAKE_VERSION),3.80),,$(error This makefile requires GNU make version 3.80.))
- $(if $(filter $(MAKE_VERSION),3.80 3.81 3.82),,$(error This makefile requires GNU make version -.))
error 函式可用來打印 "無可挽回" 的錯誤結果訊息 (fatal error message). 在此函式印出訊息後, make 將會以 2 這個結束狀態中止執行. 輸出中包含當前的 makefile 名稱, 當前的列數以及訊息文字. 接下來讓我們為 make 實作常見的 assert 編成結構:
- Make3370_14
- # $(call assert,condition,message)
- define assert
- $(if $1,,$(error Assertion failed: $2))
- endef
- # $(call assert-file-exists,wildcard-pattern)
- define assert-file-exists
- $(call assert,$(wildcard $1),$1 does not exist)
- endef
- # $(call assert-not-null,make-variable)
- define assert-not-null
- $(call assert,$($1),The variable "$1" is null)
- endef
- err-exist:
- $(call assert-not-null,NOT_EXISTENT)
warning 函式 可以打印跟 error 一樣的訊息, 但是它不會終止 make 的執行狀態. 接著來看迴圈相關的函式.
這個函式可讓你在反覆展開文字時, 將不同的值代換進去. 這跟你使用不同的引數反覆的執行函式的狀況不太一樣. 首先來看一個範例:
- Make3370_15
- letters := $(foreach letter,a b c d,$(letter))
- show-words:
- # letters has $(words $(letters)) words: '$(letters)'
當 foreach 函式被執行, 它會反覆展開迴圈主體並將值以變數名稱 letter 依序傳入清單的單字 "a", "b", "c" 與 "d". 每次展開得到的輸出會被累積起來並使用空格為分隔符連接起來. 接著來看第二個範例, 透過下面的自訂函式可用來測試一組變數是否有定義:
- Make3370_16
- VARIABLE_LIST := SOURCES OBJECTS NOT_EXIST
- SOURCES := src
- OBJECTS := obj
- test_variables = $(foreach i,$(1), \
- $(if $($i),, \
- $(shell echo $i has no value > /dev/stderr)))
- Show:
- $(call test_variables,$(VARIABLE_LIST))
稍早我們提到要使用迴圈與條件測試來從清單中找尋符合樣式的單字, 下面函式將從單字清單找出包含特定字串的所有單字來:
- Make3370_17
- # $(call grep-string, serch-string, word-list)
- define grep-string
- $(strip \
- $(foreach w,$2, \
- $(if $(findstring $1,$w),$w)))
- endef
- words := count_words.c counter.c lexer.l lexer.h counter.h
- find-src:
- @echo $(call grep-string,.c,$(words))
變數何時該使用小括號
之前提過單一字母形式的 make 變數不需要小括號. 例如所有基本變數其名稱都採用單一字母形式. 就像你在 GNU Make 文件上所看到的, 所有的自動變數都沒有加上小括號. 不過強烈建議在處理變數時盡量加上小括號以避免不可預期錯誤. 下面是使用 foreach 函式時候因為前述說明造成的常見錯誤 ($INCLUDE -> $(I)NCLUDE -> 因為變數 $I 為空, 故清單為 'NCLUDE'):
- INCLUDE_DIRS := ...
- INCLUDES := $(foreach i,$INCLUDE_DIRS,-I $i)
- # INCLUDES 現在值為 "-I NCLUDE_DIRS" !!!!
最後還有一些較不重要的內建函式, 儘管它們使用頻率不如 foreach 或 call, 但是搭配起來使用還是相當方便:
strip 函式將會從 text 移除所有前導與接在後面的空白符號, 並以單一空白符號來置換所有內部的空格. 此函式常用來清理 "條件表示式" 中所使用的變數. 當變數和巨集的定義橫跨多列時, 可以透過此函式從中移除非必要的空白符號. 不過如果函式會受引數之前的前導空格影響, 為函式引數 $1, $2... 等加上 strip 也是個不錯的想法.
origin 函式將回傳描述變數來自何處的字串. 這個函式可以協助你決定如何使用一個變數的值. 舉例來說: 如果變數來自環境, 或許你會想要忽略該變數的值; 如果變數來自於命令列, 你就不會這麼做. 而此函數可能的輸出如下:
讓我們來看一個比較具體的範例. 下面是一個新的斷言函式, 可用來測試一個變數是否有定義:
- Make3370_18
- # $(call assert,condition,message)
- define assert
- $(if $(strip $1),,$(error Assertion failed: $2))
- endef
- # $(call assert-file-exists,wildcard-pattern)
- define assert-file-exists
- $(call assert,$(wildcard $1),$1 does not exist)
- endef
- # $(call assert-not-null,make-variable)
- define assert-not-null
- $(call assert,$($1),The variable "$1" is null)
- endef
- # $(call assert-defined,variable-name)
- define assert-defined
- $(call assert, \
- $(filter-out undefined,$(origin $1)),\
- '$1' is undefinied)
- endef
- EXIST := TEST
- err-exist:
- @echo === Test ===
- # origin of EXIST = $(origin EXIST)
- # origin of NOT_EXIST = $(origin NOT_EXIST)
- $(call assert-defined,EXIST)
- @echo === Done ===
如果替換成 $(call assert-defined,NOT_EXIST) , 執行結果能偵測到未定義變數並輸出如下:
warning 函式類似於 error 函式, 不過此函式不會導致 make 結束執行. 如同 error 函式, warning 的輸出包括當前的 makefile 檔名, 當前的列號以及所要顯示的訊息內容. 一個使用範例如下:
- JAVAC := /usr/lib/jvm/java-1.7.0-openjdk-1.7.0.91-2.6.2.1.el7_1.x86_64/bin/javac
- $(if $(wildcard $(JAVAC)),,$(warning The java compiler variable, JAVAC ($(JAVAC)), is not properly set.))
- ...
Supplement
* GNU Make Doc - Functions for Transforming Text
* GNU Make Doc - Conditional Parts of Makefiles
* [GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案
* GNU Make Doc - Functions for File Names
沒有留言:
張貼留言