程式扎記: [GNU Make] 變數與巨集 : 變數

標籤

2010年9月20日 星期一

[GNU Make] 變數與巨集 : 變數

前言 : 
到目前為止, 我們看過了 makefile 的變數以及將它們應用在內建與自訂規則中. 不過這都是基礎, 變數與巨集越複雜, GNU make 的功能就越強大. 在我們繼續任何探討前, 最好先了解 make 所包含的兩種語言. 第一種語言用來描述工作目標與必要條件所組成的依序圖. 第二種語言是個巨集語言, 用來進行文字的替換. 如 make 允許你為較長的字符序列定義簡寫, 以及在你的程式中使用該簡寫. 巨集處理器將會認出你的簡寫以及作置換程展開後的形式. 雖然你可以把 makefile 的變數想成傳統語言的變數, 不過巨集變數和傳統變數之間還是有差別. 巨集變數會被 "就地" 展開, 其所產生的文字串還可以做一步的展開. 

變數名稱是大小寫有別的, 所以 cc 和 CC 所指的是不同變數. 要取的某個變數的值, 可用 $() 括住該變數名稱或使用大括號來展開變數, 例如 ${CC}. 現代化的 makefile 多半會使用小括號, 接下來也會以小括號作為範例說明. 當變數用來表示使用者在命令列或環境中所訂製的常數時, 習慣上會以全部大寫來撰寫其名稱, 單字之間並以底線符號 (_) 隔開. 至於只在 makefile檔中出現的變數, 則會全部用小寫來撰寫其名稱, 單字之間並以底線符號隔開. 參考如下範例 : 
  1. # 常數  
  2. CC := gcc  
  3. MKDIR := mkdir -p  
  4.   
  5. # 內部變數  
  6. sources = *.c  
  7. objects = $(subst .c,.o,$(sources))  
  8.   
  9. # 函式  
  10. maybe-make-dir = $(if $(wildcard $1),,$(MKDIR) $1)  
  11. assert-not-null = $(if $!,,$(error Illegal null value.))  
一個變數的值組成自賦值符號 , 右邊已刪除前導空格的所有單字. 跟在單字之後的空格則不會被刪除. 但這有時容易造問題, 舉例來說, 如果變數的值包含了跟在後面的空白符號, 而且被使用在命令稿中 : 
  1. LIBRARY = libio.a #LIBRARY  
  2. missing_file:  
  3.         touch $(LIBRARY)  
  4.         ls -l | grep '$(LIBRARY)'  
對這個 makefile 執行 make 將會得到如下結果 : 
touch libio.a
ls -l | grep 'libio.a '
gmake: *** [missing_file] Error 1

因為 grep 搜尋字串包含了跟在後面的空格, 所以無法在 ls 的輸出中找到該檔案的名稱. 

變數的用途 : 
一般來說, 以變數來代表外部程式是個不錯的主意. 這讓 makefile 的使用者必較容易針對他們特有的環境來進行改寫 makefile . 舉例來說一個系統常會包含 awk 的各種版本 : awk, nawk, gawk. 這個時候就可以建立一個 AWK 變數來保存 awk 程式的名稱, 讓 makefile 的使用較為容易. 變數可用來保存簡單的常數, 也可以用來存放使用者自訂的命令序列, 例如下列的設定可以用來回報尚未使用的磁碟空間 : 
  1. DF = df  
  2. AWK = awk  
  3. free-space := $(DF) . | $(AWK) 'NR == 2 {print $$4}'  

變數的類型 : 
make 變數有兩種類型 : 經簡單展開的變數以及經遞回展開的變數. 你可以用 := 賦值運算符來定義一個簡單展開的變數 (或稱簡單變數) : 
  1. MAKE_DEPEND := $(CC) -M  
之所以稱此變數為 "簡單展開" 是因為一但 make 從 makefiile 讀進該變數的定義, 賦值運算符的右邊部分會被立即展開. 賦值運算符的右邊只要出現 make 變數的參照就會被展開, 而展開後所產生的文字會被儲存成該變數的值. 此行為跟大多數的程式和命令稿沒有兩樣. 拿上面當例子, 展開後為 : 
gcc -M

然而如果上面的變數 $(CC) 尚未定義, 則此變數展開後一般會變成這個樣子 : 
-M

$(CC) 被展開成它的值 (並未包含任何字符) , 也就是空無一物 (即空值) , 變數沒有定義並不算錯誤. 事實上此特性相當有用. 大多數的內定命令都會包含未定義的變數, 以作為使用者訂製變數的佔位符. 如果使用者並未訂製該變數, 它就會變成空無一物. 現在注意前導的空格. make 首先會剖析賦值運算符右邊的部分, 也就是 $(CC) -M 這個字串. 當變數參照被展開成空無一物, make 不會重新掃描該值以及刪除前導的空格, 於是前導的空格就留下來的. 

第二種變數類型稱為經遞迴展開的變數. 你可以用 '=' 賦值運算符來定義一個經遞迴展開的變數 (會稱遞迴變數) : 
  1. MAKE_DEPEND = $(CC) -M  
之所以稱此變數為 "經遞迴展開" 是因為, make 只會讀進賦值運算符右邊的部分, 並將之儲存成該變數的值, 但不會進行任何展開的動作. 展開的動作會被延遲到該變數被使用的時候才進行. 將此變數稱為延後展開的變數或許比較恰當, 因為展開的動作會延遲到該變數實際被使用的時候才進行, 這種展開會導致令人意外的結果, 亦即變數的值可能會亂掉 : 
  1. MAKE_DEPEND = $(CC) -M  
  2. ...  
  3. # 稍後  
  4. CC = gcc  
這樣當 MAKE_DEPEND 被使用時, 即使 CC 並未定義, MAKE_DEPEND 在命令稿中的值也會被展成 gcc -M
事實上, 遞迴變數所進行並非真的是延後賦值的動作. 每當遞迴變數被使用時, make 就會對它的右邊部分進行重新求值的動作. 如果變數被定義成簡單的變數, 比如前面的MAKE_DEPEND, 作此區別是毫無意義的, 因為右邊部分的變數也都會是簡單的變數. 但試想如果右邊部分的某個變數被用來代表一個所要執行的程式, 例如 date. 每當遞迴變數被展開, date 程式就會被執行, 而且每次展開後的值都不一樣 (假定date 的執行前後都間隔一秒) . 有的時候這個特性可能非常有用, 但也可能非常煩人. 

其他的賦值類型 : 
我們在前面的範例看到兩種賦值類型, 其中 '=' 用來建立遞迴變數, 而 ':=' 用來建立簡單變數. 此外 make 還提供了另外兩種賦值運算符 : '?=' 和 '+='. 

'?=' 運算符稱為 附帶條件的變數賦值運算符 . 我們會把他簡稱為條件賦值. 此運算符只會在變數的值尚不存在的狀況下進行被要求的變數賦值動作. 考慮下面範例 : 
  1. # 將所要產生的每個檔案擺到 $(PROJECT_DIR)/out 目錄  
  2. OUTPUT_DIR ?= $(PROJECT_DIR)/out  
此處我們只會在輸出目錄變數 OUTPUT_DIR 的值尚不存在的狀況下對它進行賦值動作. 這個特性可以跟環境變數有很好的互動. 

'+=' 運算符通常稱為附加運算符. 正如其名此運算符會將文字附加到變數裡. 這似乎是沒什麼特別的, 但是當遞迴變數使用時, 它卻是一個重要特性. 尤其是賦值運算符右邊部分的值會在 "不影響變數中原有的值的狀況下" 被附加到變數理, 你可能會說 "這有什麼大不了, 附加的功能不就是這樣?". 沒錯但請繼續往下面看. 對簡單變數進行附加動作, 事情就會變得更加明顯. 考慮下面範例 : 
simple := $(simple) new_stuff

因為簡單變數中的值會被立即展開, 所以 make 會展開 $(simple) , 附加因而產生的文字, 最後進行賦值的動作. 但是遞迴變數會導致一個問題. 如果將 '+=' 運算符實作成下面這個樣子, 是不被允許的 : 
* makefile_3-1-3 內容 : 
  1. recursive = $(recursive) new_stuff  
  2. test:  
  3.         echo $(recursive)  
這會形成錯誤, 因為 make 沒辦法妥善加以處理. 如果 make 儲存 recursive 目前的定義加上 new_stuff , 則 make 就不能於執行時期再次展開它. 此外試圖展開一個自我參照的遞迴變數將產生無窮迴圈. 使用 make 執行上面 makefile 會的到如下錯誤 : 
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile_3-1-3
makefile_3-1-3:1: *** Recursive variable `recursive' references itself (eventually). Stop.

所以使用 '+=' 被特別實作成可將文字附加到遞迴變數, 以及做正確的事. 此運算符對於想收集到值遞增到變數的人特別有用. 

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!