[GNU Make] Makefile 教學:如何使用 Make 內建函式進行字串處理等任務

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    在 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 的巨集來取代內建函式,其實功能反而更加豐富。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: GNU MAKE, MAKE, MAKEFILE