在命令列上定義的巨集(Macros Defined on the Command Line)
Shell變數 (Shell Variable)
指派巨集的優先順序(Priority of Macro Assignments)
把環境變數當作內定值(Relying on Environment Variable for Defaults)
巨集字串的代換(Macro String Substitution)
給必備檔案與目標所用的內部巨集(Internal Macros for Prerequisites and Targets)
前一章的描述檔仍然稍嫌繁瑣,因為裡頭包含了一些重複的內容. 你能夠很容易的想像,在真實的專案(project)中,目標(targets)都會根據許多的檔案來建立,而且很可能會以各個不同的觀點來建造,因此大量的重複內容是無法避免的. 但是大多數實際的描述檔都十分簡潔—大概只有10~50行. 會造成這些檔案如此簡潔的可能原因在於make中兩個十分有用的特徵: 巨集(macro)與後置規則(suffix rule). 我們在本章討論巨集,而在第三章討論後置規則.
在描述檔中的項目(entry),如果有以下形式
name = text string
就是巨集的定義. 接下來的描述檔內容,如果使用了
$(name) 或是 ${name}
就會被翻譯成 text string
所以下列這些範例都是有效的巨集定義
LIBES = -lX11
objs = draw.o plot_points.o root_data.o
CC = /usr/fred/bin/cc
23 = “This is the (23)rd run”
OPT =
DEBUG_FLAG = #empty now, bug assign –g for debugging
BINDIR = /usr/local/bin
在同一個檔案若是定義了上述的巨集,則稍後的內容中如果看起來像是下面這樣:
plot : ${objs}
${CC} –o plot ${DEBUG_FLAG} ${objs} ${LIBES}
mv plot ${BINDIR}
當你下了指令
make plot
之後,上面的內容會被翻譯成
/usr/fred/bin/cc –o plot draw.o plot_points.o root_data.o -lX11
mv plot /usr/local/bin
來執行.
這個簡短的範例顯示了我們使用巨集的主要理由. 第一,你可以方便的引用一再重複出現於描述檔裡頭的各種檔案(如${objs})或是指令項目(如${CC}). 在這個範例中,當我們要建造plot這個項目時,objs引用了目的檔,而LIBES引用了函式庫目錄. 巨集使得我們在修改描述檔時,可以很容易地避免不一致的情況發生,例如如果我們引用了兩次CC這個巨集,當我們有一天想用gcc時,我們只要修改CC的定義,就可以同時修改兩個地方,但是如果沒有使用巨集,就必須修改兩次,當引用次數很多時,就十分可能出現錯誤,而產生不一致的情形. 第二,巨集可以允許這次建造時的定義跟下次建造時的定義不一樣,例如DEBUG_FLAG控制了編譯器是否要產生符號資訊(symbolic information)給除錯器(debugger)使用,我們要除錯時,只要將DEBUG_FLAG變成 –g即可,而一般要產生發行版本(release)時,就將DEBUG_FLAG變成空巨集,什麼也沒有定義就可以了. 在巨集中定義目錄是司空見慣的事,像是範例中定義的BINDIR,就是就是為了避免重複,並且在建造時便於修改而定義的巨集.
文法規則(Syntax Rules)
在一行巨集定義中包含了一個等號. make把等號左邊的名稱(name,通常是一大串文字與數字的組合)和等號右邊的一大串字元關連起來. 你不必使用單引號或是雙引號來限定這一大串字元的範圍. 如果你使用引號來限定字串的範圍,就像前面的23這個巨集定義,那麼引號會被當作是字串的一部份,這一點與C/C++的用法略有差距,請使用者注意.
在一個描述檔裡頭的任何一行中,井字號(#)代表一個巨集定義結束,而且開始註解的部份. 有時候巨集本身(name)與其定義都相當長,你可以在行末使用一個倒斜線(\)來代表這一行尚未結束,並且和下一行是連續的. 為了保證make可以分辨巨集定義和指令(command)或依附關係列(dependency line)的差別,在巨集名稱(name)前不允許有tab字元,並且在等號前也不能有冒號(:).
因為在使用make時,每個字串(string)的最終目的都是用在shell指令(shell command)上,空白(white space)的處理相當隨便(casually). 緊接在等號左邊或是右邊的空白或是tab字元都會被去除. 當你使用倒斜線(\)要延續一行到下一行時,make會自動把這個倒斜線以空白取代之,所有在倒斜線周圍的空白,tab字元,換行字元(new lines),最後都會被簡化成單一的空白. 除了在倒斜線或是等號這些特出的地方以外,make會保留內嵌在字串裡頭的所有空白和tab字元.
巨集名稱(macro name,上面我們都簡寫成name)如果根據傳統的話,裡頭的英文字母都是大寫.但是就如同前面的範例一樣,你仍然可以使用你喜歡的大小寫字母,數字,或是底線來組合成你想要的巨集名稱. 如果你要使用其他的標點符號,有時候可以正常運作(也就是有些符號的使用會發生錯誤),但是我們建議你避免使用他們. 很難去預測哪些符號會使make產生文法剖析錯誤(parse error). 但是特別要避免使用構成shell提示符號的字元(shell metacharacter),例如 > 或是 \.
當你引用了巨集—也就是說,你想要make把這個巨集名稱(name)取代成巨集定義裡頭等號的右半部(text string)—你必須將巨集名稱用小括號(())或是大括號({})涵括起來,然後在前面加上錢字號($). 在本書中,我們使用大括號,因為在函式庫關係(relation to libraries)中有另外的意義(這個主題我們會在第三章 後置規則(suffix rule) 中討論).
單獨一個字元的巨集名稱不需要用小括號或是大括號括起來,但是使用括號把巨集名稱括起來是個好習慣. 假設有下面這個巨集定義,
在巨集定義中引用巨集名稱也是允許的,例如:
ABC =XYZ
FILE = TEXT.${ABC}
如果我們使用${FILE}最後就會以TEXT.XYZ取代
一個在等號右邊沒有字串的巨集定義(像是OPT和DEBUG_FLAG),就是被指派了空字串(null string),因此,在前面的範例中,cc這個指令在執行時,${DEBUG_FLAG}就會以”什麼都沒有”來取代,好像${DEBUG_FLAG}沒有在那裡一樣.
此外,如果你引用了一個巨集名稱,但是卻沒有定義它,make會把它以空字串來取代. 因此,引用未定義的巨集名稱不會得到任何錯誤訊息.
你不必擔心巨集定義的順序,這使得巢狀定義(nest definition)變得很簡單就可以做到(雖然一旦你定義了遞迴式巨集(recursive macro),make可以正確的離開然後向你抱怨;也就是說,就算你定義了無法結束的遞迴式巨集,make仍然有辦法離開這個巨集,而不會停留在無法結束的巨集之中):
MY_SRC = parse.c search_file.c
SHARED_DIR = /users/b_proj/src
SHARED_SRC = ${SHARED_DIR}/depend.c
parse.c search_file.c /users/b_proj/depend.c
取代之.(但是這裡有一個限制:巨集定義必須出現在任何一個依附關係列(dependency line)出現其巨集名稱之前.) 這種順序上的自由也隱含了一個限制,就是你不能在描述檔裡頭的一處定義一個巨集,然後在描述檔的某一處再重新定義它. make會使用最新的定義來取代所有引用到此巨集的地方,不管巨集的引用出現在最新的巨集定義之前或是最新的巨集定義之後(也就是離檔案開頭越遠的巨集定義就會被當成每一個引用此巨集時所參考的定義,不管你重新定義了多少次). 如果你想要動態地重新定義巨集,你必須遞迴式地叫用make,這是一個我們會在第五章 專案管理(Project Management)裡頭會討論到的主題.
定義在make內部的巨集(Internally Defined Macros)
make已經用巨集定義的方式,事先定義了許多公用的命令. 例如${CC}這個巨集被make用來代表C的編譯器,而${LD}這個巨集則被用來代表連結器(linker). 這些事先定義的巨集,它們的優點在於使用了我們在第三章才會提到的後置規則(suffix rule). 但是如果你了解後置規則,當你看到下面的描述檔內容時,你就可以很容易地了解:
上面這兩行產生的效果相當於
basic.o : basic.c
CC –c basic.c
如果你回過頭去看本章的第一個範例,你就會發現重新定義了CC,所以使得CC這個巨集名稱的內容變成/usr/fred/bin/cc. 你會在多變(volatile)的發展環境裡頭的巨集中,常常發現這一種巨集使用方法,一般所使用的工具像是C編譯器,在這種環境底下,都被徹底地換掉了. 所以很可能/usr/fred/bin/cc是某個人自己個人特有的編譯器,而不再是原來的cc這個C編譯器了. 雖然大多數的程式設計師都不必擔心他們的編譯器的穩定度,這個例子展示了你要如何使用make裡頭的巨集功能來支援身處更種不同條件下的人所做的工作.
對每一個make裡頭的指令來說,它們都以巨集的形式來定義,但是還有另外一些巨集被用來定義命令(command)的選項(option). 程式設計師一般把他們用在C編譯器裡頭的選項儲存在CFLAGS這個巨集名稱裡頭,ld(連結器)的選項儲存在LDFLAG裡頭,還有其他林林總總. 這些都會在第三章 後置規則(suffix rule)裡頭被討論到,因為它們跟後置規則比較有關
第三章也會提到利用-p這個選項來顯示出所有定義在make內部的巨集.
在命令列上定義的巨集(Macros Defined on the Command Line)
你可以在命令列上使用make的同時也定義巨集. 下面就是一個例子:
$make jgref DIR=/usr/obj
上面這個命令把字串/usr/obj指派給DIR這個巨集名稱;在描述檔裡頭我們就可以透過${DIR}來取用/usr/obj這個字串.我們常常會發現我們要建造的程式(也就是目標(target))會出現在巨集定義之前,例如上面的例子,目標jgref就在巨集定義DIR 之前,但是順序上的先後跟所得到的結果是完全沒有關係的. 這是因為等號(=)使得命令列上的參數被當做是巨集定義,也因此就符合前面所說的—各個巨集定義在描述檔中出現的順序跟結果完全無關.
如果在命令列上的巨集定義包含了許多的文字,請把它們用單引號或是雙引號括起來. 這純粹只是shell語法上的問題,這些引號保證shell會將這些巨集定義當做是同一個參數傳給make(在命令列上,以空白間隔的每個字都會被當成獨立的參數,除非把這些字用引號括起來,否則就會被shell解譯成單一的參數).
例如:
$make jgref “DIR=/usr/proj /usr/lib /usr/proj/lib”
最後我們要講到在Bourne shell與Korn shell之中,你可以把巨集定義放在make命令之前:
$DIR=/usr/proj make jgref
這個順序上的不同,一但遇到定義有相衝突時,在採用定義時的優先順序上會有微小的差別. 我們在本章的後面會討論這個次序上的問題.
Shell變數(Shell Variable)
當你使用make命令時,shell變數(shell variable)也是執行環境的一部份,因此在描述檔裡頭,可以把這些shell變數拿來當做巨集使用. 但是在引用它們之前,如果這些shell變數的名稱多於一個字元的話,就必須使用小括號或是大括號將它們括起來(就如同本章前面部份所說的一樣). 因此,如果你在使用make之前就先定義了變數給shell:
$DIR=/usr/proj ; export DIR
$make jgref
然後你可以在描述檔裡面用DIR這個shell變數,就好像把DIR當做巨集名稱一樣:
SRC = ${DIR}/src
jgref :
cd ${DIR}; …
上面的命令在Bourne shell和Korn shell之中都可以正常地運作,但是C shell的使用者如果要增加shell變數給你所在的環境,就必須使用setenv這個指令:
$setenv DIR /usr/proj
所以,藉由上述定義shell變數的方法(註:shell變數也可以叫做環境變數(environment variable)),任何你在.profile或是.login或是用任何其他方法所定義的shell變數,都可以在描述檔裡頭以巨集的方式來使用它們.
記住這些”使用方法跟巨集相同的環境變數”和我們在第四章 命令(command)的最後所討論到在使用的可能性上有限的”被動態指派的shell變數”兩者之間的差別. 後面這種環境變數在使用時必須用到兩個錢字號($$),它可以不需要用小括號或是大括號括起來,而且只要經過換行符號(\n),這個環境變數就無效了.
指派巨集的優先順序(Priority of Macro Assignments)
我們已經看過不少在描述檔裡頭所引用之巨集的來源:利用目前的環境變數,在命令列上直接定義巨集,或是make裡頭內定(default)的定義,還有在描述檔裡頭定義,使得我們可以在描述檔裡頭引用利用這些方法所定義的巨集. 如果以不同方式定義的巨集互相有衝突時(也就是不同的來源定義了相同的巨集名稱),make會採用哪個定義呢? 比如說,如果在描述檔裡頭就定義了DIR,其內容為/usr/proj,但是在目前的環境變數中,也有一個叫做DIR的環境變數,其內容為/usr/proj/lib,那麼make會採用哪一個定義呢? 下面就是優先權的順序,從優先權最小到優先權最大:
1.make內部的(default,內定的)已有的定義.
2.目前的環境變數. 在Korn shell和Bourne shell裡頭,如果把巨集定義在make指令之前的話,此巨集相當於環境變數.
3.在描述檔裡頭的巨集定義.
4.在使用make指令時,所定義的巨集,但是是位於make指令之後,相較於第2項,雖然同樣都是在命令列上,但是因為順序上的差異引發了微小的不同.
所以我們可以知道,描述檔裡頭的定義最終決定了使用make指令時要採用哪個定義.我們在描述檔裡頭的巨集定義不會被環境變數(包括在make指令之前的定義)或是make內部的巨集定義所覆蓋掉,但是會被你在使用make指令時所定義(在make指令之後)的巨集覆蓋.
優先權最低的環境變數有時候是我們所需要的,因為專案發展小組有時候會希望不同成員所使用的發展環境一致. 在一些情況下,你也會希望環境變數的內容去覆蓋掉描述檔裡頭的定義,舉例來說,你可能會希望把專案裡頭內定的巨集定義置換成你自己的巨集定義,而且你必須一直不斷地執行make. 最簡單的解決方法就是在你的環境變數裡頭定義巨集,然後強制這些環境變數去覆蓋描述檔裡頭的巨集定義. 在使用make時加上 –e 這個選項,這樣一來,就會把不同來源之巨集定義的優先權變成下面的順序,由最低到最高:
1.make內部的(default,內定的)已有的定義.
2.在描述檔裡頭的巨集定義.
3.目前的環境變數. 在Korn shell和Bourne shell裡頭,如果把巨集定義在make指令之前的話,此巨集相當於環境變數.
4.在使用make指令時,所定義的巨集,但是是位於make指令之後,相較於第2項,雖然同樣都是在
這裡有一個範例,在描述檔裡頭有下列這個項目:
TESTER = default_test
test : srcfile.c
${TESTER} srcfile.c
當你定義了TESTER這個環境變數:
$TESTER=new_test
$export TESTER
則下面的指令會忽略掉你在環境變數中的定義,而採用在描述檔裡頭的定義:
$make test
default_ srcfile.c
但是下面的指令會使用你在環境變數裡頭的定義,而忽略掉在描述檔裡頭的定義:
$make –e test
new_test srcfile.c
把環境變數當作內定值(Relying on Environment Variable for Defaults)
環境變數傳統上被用來處理系統之間或是使用者之間的差異(而且很可能被過度的使用). 舉例來說,如果一個軟體販售者或是系統管理者希望軟體工具被安裝在/usr/local這個目錄之下,而其他人卻比較喜歡把軟體工具安裝在/usr/share這個目錄之下,安裝這些軟體工具所用的工具,藉由在路徑名稱(pathname)裡頭安插一個變數,然後告訴使用者為他們的所使用的系統適當地定義這個變數,就可以滿足他們各自的喜好.
既然make把環境變數當成是巨集來看待,當你在建造你的程式時,你就可以使用相同的技巧.很多人當他們必須執行遞迴式make(recursive make)的時候很喜歡使用環境變數,就如同在第五章 專案管理(project management)裡頭所看到的一樣. 使用環境變數比起在每次呼叫make時在命令列上定義巨集以傳遞給make還要來的方便多了.
這裡有一些範例巨集和一個從描述檔裡面摘錄下來的項目,它們都廣泛地使用環境變數. 範例中假設你將你的個人工具放在每個系統裡頭一個叫做tool的目錄之中. 在這個範例裡頭有個實際上並不存在的特殊的工具叫做filter.
TOOL_DIR = ${PROJECT_TREE}/tool
FILTER = ${TOOL_DIR}/filter
source_stream:
cat *.c | ${FILTER} …
這個範例可以保持獨立性的關鍵在於在這個檔案裡頭的每樣東西都把PROJECT_TREE當作是根目錄(root directory)但是檔案裡頭沒有任何一處定義了PROJECT_TREE, 定義PROJECT_TREE這件事必須由每個使用者來做. 因此,如果你的系統中tool目錄位於/usr/local,使用者就在他們的.profile(使用Bourne shell或Korn shell時)這個檔案中加入下面這行:
PROJECT_TREE=/usr/local ; export PROJECT_TREE
或者是在他們的.cshrc(使用C shell時)這個檔案中加入下面這行:
setenv PROJECT_TREE /usr/local
這個定義就會在整個描述檔裡頭使用,因此TOOL_DIR與FILTER就可以設定成適當於此系統的值,最後我們使用:
make source_stream
這個指令後,就會產生正確的指令:
cat *.c | /usr/local/tool/filter
如果你把PROJECT_TREE定義在描述檔裡頭,對於其他tool目錄不是放在/usr/local目錄底下的人就會造成很大的不便. 這些人為了覆蓋掉在描述檔裡頭的巨集定義,就必須在定義環境變數後在make命令後頭加上 –e 這個選項,或是呼叫make命令以後在命令列上定義PROJECT_TREE,這些都是較不方便的作法.
我們可以發現,把PROJECT_TREE留著先不要定義會比較具有彈性—只要使用者確定他們定義了這個環境變數. 如果他們沒有定義這個環境變數會發生什麼事呢? 會出現一些麻煩,這就是環境變數最大的缺點. 在上面的範例之中,如果我們沒有定義PROJECT_TREE,當我們在描述檔裡頭引用到這個巨集時,就會被空字串(null string)所取代. 因此當我們下指令:
make source_stream
時,就會產生指令:
cat *.c | /tool/filter
然而/tool/filter這個軟體工具並不存在於這個位置上,所以這行指令就會出錯. 使用者很可能根本不知道究竟哪裡出了錯,所以很多同一個團隊裡的人會浪費很多時間就為了抓出造成這個問題的原因,而造成這個問題的主因卻又是如此簡單.
順道一提,就是如果你把下面這一行放在描述檔裡頭,它不代表PROJECT_TREE是未定義的巨集.
PORJECT_TREE =
上面這行把PROJECT_TREE定義成空字串,如果你在環境變數中也定義了PROJECT_TREE,那麼這行會覆蓋掉你在環境變數中的定義.
也許你想要做的只是設定一個符合大多數使用者的內定(default)巨集定義,而且使用者需要時也可以很容易地更改成他們所希望的定義. 也就是說,如果大多數系統上的根(root)是/usr/local,你可以採用這個定義,而使用者不必去擔心這個定義的內容. 但是到了其他的系統上,而根的位置又不相同時,使用者可以在.profile或是.cshrc裡頭設定環境變數,而你的描述檔可以顧及這件事.
很令人驚訝的是,你在make裡頭無法做到上述這件事. 你可以不定義一個巨集,或者你可以定義一個巨集然後讓它很難被修改. 如果真的要實作出可以支援內定(default)觀念的描述檔,你要跳開make然後改成使用shell命令稿(shell script).
一個符合上述功能的shell命令稿—通常叫做包裝者(wrapper)—就如同下面所看到的一樣,這個命令稿會去確認使用者是否定義了PROJECT_TREE這個環境變數. 如果定義了,就保留原先使用者的定義,如果沒有,就使用你內定的定義. 最後一行呼叫make指令,把所有命令列上的參數傳遞給它.
If
test –z “$PROJECT_TREE”
then
PROJECT_TREE=/usr/local ; export PROJECT_TREE
fi
make $*
現在你必須訓練你的使用者來使用這個命令稿而不是使用make指令. 舉例來說,如果這個命令稿叫做make_local,他們要建造目標時必須下的指令為:
$make_local source_stream
上面的程序整體看起來非常的拐彎抹角,但是幾乎可以正常地運作. 最可能會讓人疑惑地方在於複雜的命令參數;例如,參數中的引號會被截掉,最後make指令並不會看到引號. 也就是說,上述的命令跟下面這個指令:
$make_local “source_stream”
會產生相同的結果.
如同環境變數,你要把使用包裝者命令稿(wrapper script)作為最後手段,不到必要的時候絕對不去使用,因為在解決了就問題後,他們常常會帶來新的問題. 舉例來說,有一個公用程式你不能在這一節裡頭的範例給予定位(locate)—就是make_local這個命令稿本身
巨集字串的代換(Macro String Substitution)
(這個功能並不是每一種版本的make都會提供;請參閱附錄C,可以找到方法來檢查你的make是否提供此功能)
假設你定義了一個巨集:
SRCS = defs.c redraw.c calc.c
然後在描述檔裡頭有下面這一行指令:
ls ${SRCS:.c=.o}
如果指令要求列出的檔案存在的話,就在螢幕上會產生的結果如下;
calc.o defs.o redraw.o
當巨集的引用使用了相當艱深的語法,這種用法所產生的效用十分簡單. make會先使用由${SRC}所代換而來的字串,然後此字串中,如果含有冒號(:)之後等號(=)之前的字串(此範例中是.c),就用等號後面的字串(此範例中是.o)來取代. 這種功能使得我們可以輕易的維護許多群只有檔案名稱結尾不同的檔案. 比如說:
SRCS = defs.c redraw.c calc.c
DEBUG_OBJS = ${SRC:.c=_dbg.o}
defs.o redraw.o calc.o
而${DEBUG_OBJS}會變成
defs_dbg.o redraw_dbg.o calc_dbg.o
如果你想要完全由那些檔名結尾為 _dbg.o 的檔案來建造你的可執行檔,你可以在描述檔裡頭加上下面的項目:
plot_debug: ${DEBUG_OBJS}
${CC} –o plot_debug ${DEBUG_OBJS}
字串的取代有十分嚴格的限制;只能在一個巨集內容的結尾或是剛好在空白之前.所以,如果我們像下面這種做法:
LETTERS =xyz xyzabc abcxyz
…
echo ${LETTERS:xyz=DEF}
就只會在螢幕裡輸出:
DEF xyzabc abcDEF
我們可以發現第二個字串並不在空白之前,所以沒有被取代掉,而最後一個xyz是巨集內容的結尾,所以被取代了…這個結果完全符合我們上面所提到的限制.
在使用巨集字串的代換功能時,等號右邊的字串可以是空字串(null string),而等號右邊的字串不可以是空字串,舉例來說:
SOURCES = src1.c src2.c glob.c
EXECS = ${SOURCES:.c=}
所以最後${EXECS}會被
src1 src2 glob
所取代.
在其他種版本的make裡頭,還有其他種更具威力的巨集字串取代方法可以使用,但是每一種版本的差異性都非常大,因此請多參閱你所使用make版本的說明文件.
有很多時候,你會希望只要定義單一一個集合的指令,就可以在各種不同型態的檔案上照常運作.編譯.c的檔案造成.o的檔案是一個十分明顯的例子. 第三章 後置規則(suffix rule)中將會介紹一般在make裡頭所使用的解決辦法.但是你還是會發現巨集和字串取代對於處理和確認字尾以外的樣式(pattern)上還是十分有用的;例如這一節所提到的 _dbg.o 這個樣式就是一個很好的例子.
給必備檔案與目標所用的內部巨集(Internal Macros for Prerequisites and Targets)
每一次make讀進一列描述檔裡頭的相依關係時,它會自己定義許多屬於它自己的巨集.這會使得描述檔的撰寫較簡單,而且當我們要修改或增加描述檔裡面的項目(entry)時也容易多了.這些一般性的巨集我們可以在附錄A 快速參閱 裡面看到列表.
當我們引用 $@ 這個巨集時,最後會被代換成”目前的目標”,這個巨集在描述檔裡頭相當普遍,因為目標(target)通常與你所想要建造(build)的檔案有相同的名稱.因此,一個用來編譯出執行檔的項目都用 @ 這個巨集來當作輸出檔案的名稱:
plot_prompt : basic.o prompt.o
cc –o $@ basic.o prompt.o
當我們引用 $? 這個巨集時,最後會被代換成”一列比目前目標還要新的必備檔”. 舉例來說,假設你有一個叫做libops的函式庫,裡面包含了三個目的檔 interact.o sched.o gen.o .而下面這個項目(entry)的目的就是要用比函式庫還要新的目的檔來重建此函式庫:
libops : interact.o sched.o gen.o
ar r $@ $?
當你第一次建造libops這個項目時,這三個模組(目的檔)都會被用來建造新的函式庫,因為make把不存在的目標當作是過期的目標(因此三個模組一定都比我們的目標來的新).
$make libops
ar r libops interact.o sched.o gen.o
之後如果你重新建造了sched.o,則當我們要重新再建造libops函式庫時,在此函式庫中只有sched.o這個模組會被最新建造的sched.o取代:
$cc –c sched.c
$make libops
ar r libops sched.o
我們會在 第三章 後置規則(suffixes)裡頭更詳細地談到函式庫.
在描述檔裡面的一個單一的項目中,你可以在依附關係列裡面使用多個目標和多個必備檔,我們假設有下面這麼一個項目:
new_spec new_impl : menus hash store
date >> $@
ls $? >> $@
假設我們已經建造了new_spec與new_impl兩個目標.如果有人在昨天修改了menus且重新建造new_impl,而今天某人修改了store,我們可以知道,對new_spec來說,menus和store都比它新,但是對new_impl來說,目前只有store比它新.因此當我們重新建造這兩個目標時,會發生下面情形:
$make new_spec
date >> new_spec
ls menus store >> new_spec
$make new_impl
date >> new_impl
ls store >> new_impl
另一種 $@ 的形式是 $$@ ,此巨集有兩個錢字號.(這個用法並非所有不同版本的make都適用;請參閱附錄C來檢查你的系統是否有此特徵) 這個巨集只有在依附關係列上出現才具有意義—也就是說,這個巨集只有在指定必備檔案時才具有意義(位於依附關係列的冒號右邊). 相較之下, $@ 與 $? 只能出現在命令列上.
其實 $$@ 與 $@ 都參考到相同的東西,即目前的目標名稱.因為make讀入並解譯描述檔的每一列時有一定的順序,因此兩個錢字號是必要的.如果描述檔裡頭有下面這樣的依附關係列:
docmk : $$@.c
則最後會被代換成
docmk : docmk.c
這種用法在我們建立大量其原始檔只有一個檔案的執行檔時非常有用.舉例來說,在建造UNIX系統命令(UNIX下每個命令都是一個執行檔)所在的原始碼目錄下,常常會有類似下面內容的描述檔:
CMDS = cat dd echo date cc cmp comm ar ld chown
${CMDS} : $$@.c
${CC} –O $? –o $@
當我們在命令列下指令:
$make echo
的時候,結果就好像我們在描述檔裡頭有下面這個項目一樣:
echo : echo.c
cc –O echo.c –o echo
同理,如果我們在命令列上打入:
#make cat date cmp ar
的話,則make會依序建造每個目標,比如說,當make把cat當成目標名稱時, $$@ 就會被代換成cat,而當make把date當作目標來建造時, $$@ 就會被代換成date,其他以此類推.
順道一提,當我們使用名稱只有單一字元的巨集時,所使用的語法亦適用於這一節所提到的巨集名稱(@和?).只要你喜歡,你可以寫成 $(@) 或 ${@} 或 $@ ,結果都相同.
還有許多額外的內部巨集並沒有在這裡提到,這些內部巨集同樣的可以從巨集定義中萃取出檔名部分或是目錄部分.你會在第三章和第五章看到這些內部巨集的應用.附錄A裡頭有這些內部巨集的列表以及它們所能做到的變化.
把在本章中所學到的東西全部綜合起來,在第一章裡頭的描述檔就會變得比精簡,但是可讀性就不高了:
OBJS = main.o iodat.o dorun.o io.o
LIB = /usr/proj/lib/crtn.a
program : ${OBJS} ${LIB}
${CC} –o $@ ${OBJS} ${LIB}
main.o : main.c
${CC} –c $?
iodat.o : iodat.c
${CC} –c $?
dorun.o : dorun.c
${CC} –c $?
lo.o : lo.s
${AS} –o $@ $?
你很可能會覺得編譯program這個項目所使用的指令裡頭太過麻煩,應該可以用 $? 這個巨集來取代 ${OBJS} ${LIB}.但是如果我們這樣做的話,會出現問題.因為 $? 所表示的只是比目標檔案還要新的必備檔案,但是cc指令在建造program的時候,必須連結所有的目的模組才可以.也就是說,如果我們用$? 這個巨集來取代 ${OBJS} ${LIB},萬一OBJS與LIB裡面定義的檔案在第一次建造後又再度被更新了,那麼下次建造時cc將不能成功的連結每一個建造program所需要的目的模組,因為 $? 只被代換成已經被更新過的目的模組而已.
上面這個簡化過的描述檔裡面的後面四個項目看起來還是覺得太過繁瑣.而且當整個程式所需的檔案日漸增多的時候,如果要在這個描述檔裡面增加新項目或刪減項目,必須要特別小心,因此也造成了此描述檔在維護上的困難.如果你使用的make版本有巨集字串代換的功能,那麼你可以用下面撰寫方法讓將來在描述檔的維護上更簡單:
C_OBJS = main.o iodat.o dorun.o
${C_OBJS} : $${@:.o=.c}
${CC} –c $?
這樣一來,將來就可以十分簡單地增加新的必備檔案或是刪除就有的必備檔案.
不過make還提供一種更明瞭的解決方法,可以用來解決檔案集合(files in aggregates).在下一章中,我們將可以看到,後置規則如何完成自動化的工作,並且如何大大地減少我們撰寫描述檔所必須花費的功夫.
譯者:王森(moli)
1995國立成功大學工程科學系
1999國立交通大學科技管理研究所
行動電話:0939278600
如有問題,請email至moli@tomail.com.tw
請大家指教..