讓這世界再多一份 GNU m4 教程 (2)

上一篇見:http://segmentfault.com/a/119...前端

自定義一個 m4 宏所用的基本格式以下:segmentfault

define(宏名, 宏體)

上一節,咱們定義的一個很簡單的 say_hello_world 宏:緩存

define(say_hello_world, Hello World!)

say_hello_world 是宏名,Hello World 是宏體。若是在宏定義以後的文本中出現了 say_hello_world,例如:bash

define(say_hello_world, Hello World!)

blab blab ... say_hello_world

假設上述文本均處於非負號緩存,那麼當 m4 從輸入流中讀取到 say_hello_world 時,它可以檢測出該文本片斷是一個被定義了的宏,因而它就將這個宏展開爲 Hello World,並使用這個展開結果替換文本片斷 say_hello_world,因此,上述文本通過 m4 處理後發送到輸出流,就變成:函數

blab blab ... Hello World!

上述輸出結果中的空行,應該沒什麼玄機可言了,只是須要注意:宏定義語句自己也會被 m4 展開,由於 define 自己就是一個宏,只不過它的展開結果是一個空的字符串。網站

有參數的宏

宏能夠有參數。遵循 POSIX 標準的 m4,容許一個宏最多有 9 個參數(彷佛 Shell 腳本里的函數也最多支持 9 個參數),在宏體中可以使用用 $1, ..., $9 來引用它們。GNU 的 m4 不限制宏的參數數量。code

對於下面這段 C 語言的宏定義與調用:圖片

#define DEF_PAIR_OF(dtype) \
typedef struct pair_of_##dtype { \
        dtype first; \
        dtype second; \
} pair_of_##dtype##_t

DEF_PAIR_OF(int);
DEF_PAIR_OF(double);
DEF_PAIR_OF(MyStruct);

用 m4 的有參數的宏可給出等價表示:文檔

divert(-1)
define(DEF_PAIR_OF,
`typedef struct pair_of_$1 {
        $1 first;
        $1 second;
} pair_of_$1')
divert(0)dnl

DEF_PAIR_OF(int);
DEF_PAIR_OF(double);
DEF_PAIR_OF(MyStruct);

它們可以展開爲一樣的 C 代碼(C 語言宏由 C 預處理器展開,m4 宏由 m4 展開):字符串

typedef struct pair_of_int {
        int first;
        int second;
} pair_of_int;
typedef struct pair_of_double {
        double first;
        double second;
} pair_of_double;
typedef struct pair_of_MyStruct {
        MyStruct first;
        MyStruct second;
} pair_of_MyStruct;

注意,C 宏與 m4 宏的調用有點區別。在 C 中,調用一個宏,宏名與其後的 ( 能夠有空格,而 m4 宏的調用不容許這樣

m4 版本的 DEF_PAIR_OF 宏的宏體爲:

`typedef struct pair_of_$1 {
        $1 first;
        $1 second;
} pair_of_$1'

這個宏體是一個帶引號的字符串,形如:

`... ... ...'

注意左引號與右引號對應的符號。在大部分鍵盤上,左引號與 ~ 符號同鍵,右引號與 " 同鍵,它們是單引號。不要對引號掉以傾心,它在 m4 中的重要地位僅位列宏之下,若是沒有它,宏的世界會異常的混亂。後面,我會在單獨給引號的基本用法留出一節專門闡述。在此只需將引號理解爲一段文本的封裝。

事實上,對於 m4 版本的 DEF_PAIR_OF 宏體,不用引號也不會出問題(能夠去掉引號試一下)。可是,在複雜一些的宏體內,可能會出現 , 符號,若是這樣的宏體不用引號囊括起來,那麼 , 會被 m4 誤認爲宏參數的分隔符。因此,必定要記住:, 會被 m4 捕獲爲宏參數分隔符,而引號可以使之逃逸

小實踐:reStructuredText 插圖標記的簡化

reStructuredText 是一種輕量級的文本標記語言,與 Markdown 屬於同類,通常用於記筆記,而後以網頁的形式發佈。之因此要用輕量級文本標記,是由於直接手寫 HTML 太繁瑣了。我在個人機器上搭建的 Nikola 靜態網站,默認用的就是 reStructuredText,我用它來整理個人一些筆記。

在使用 reSturcturedText 寫文檔時,我以爲它的插圖標記過於繁瑣。我經常使用的插圖標記以下:

.. figure:: 圖片文件路徑
    :align: center
    :width: 寬度值

上述標記文本塊先後必需要留出空行,不然 reStructuredText 的解析器就會抱怨。alignwidth 前面的縮進也是必須的,不然 reStructuredText 的解析器就會抱怨……

爲了簡化這個標記,我用 m4 定義了一個宏:

divert(-1)
define(place_figure, `
.. figure:: $1
    :align: center
    :width: $2
')
divert(0)dnl

而後我就能夠愉快的像下面這樣在 reStructuredText 文本中插入一幅圖片了。

place_figure(`/images/amenta-needles/0001.png', 480)

用這種辦法能夠簡化許多繁瑣的文本標記,甚至能夠實現 reStructuredText 不具有的功能,例如參考文獻的管理。若是你不打算研究如何改造 reStructuredText 解析器來知足本身的需求,在這種前提下,用 m4 簡單的 hack 一下,使得 reStructuredText 的易用性顯著加強,這就是宏語言最大的用途。

不妨將宏語言視爲生活中的方便袋。

宏的陷阱

m4 容許宏的重定義,結果是新的宏定義會覆蓋舊的。例如:

define(LEFT, [)dnl
LEFT
define(LEFT, {)dnl
LEFT

若是你按照我說『新的宏定義會覆蓋舊的』來判斷,可能會認爲上述文本流經 m4 會變爲:

[
{

然而,事實上 m4 的處理結果是:

[
[

與理解這個詭異的結果是如何產生的,就須要認真的回顧一下 m4 的工做過程。

我將 m4 處理第一個 LEFT 宏定義的過程大體拆解爲:

  1. 在輸入流中,m4 遇到了 define,在它的記憶裏,define 是一個宏;

  2. 接下來它遇到了一個 (,它會認爲這是 define 宏參數列表的左定界符;

  3. 接下來,它遇到了 LEFT,,它會認爲 , 以前的文本是 define 的第一個參數;

  4. 接下來,它遇到了 [),他會認爲 [define 的第二個參數,而 )define 參數列表的右定界符;

  5. 它如今終於明白了,define(LEFT, [) 是在調用 define 宏;

  6. m4 對 define(LEFT, [) 進行展開,具體的展開過程,咱們不得而知,由於 define 是 m4 內建的宏。咱們只知道在 define(LEFT, [) 的展開過程當中,m4 會爲咱們定義 LEFT 宏,而且 define(LEFT, [) 宏展開完成後,m4 會向輸出流發送一個空字串。

當 m4 遇到第二個 LEFT 宏定義時,它的過程大體以下:

  1. 在輸入流中,m4 遇到了 define,在它的記憶裏,define 是一個宏;

  2. 接下來它遇到了一個 (,它會認爲這是 define 宏參數列表的左定界符;

  3. 接下來,它遇到了 LEFT,,它會認爲 , 以前的文本——LEFTdefine 的第一個參數。可是 m4 隨即發現 LEFT 是一個宏,因而它就將這個宏展開,結果爲 [,它認爲 [ 纔是真正的 define 的第一個參數;

  4. 接下來,它遇到了 {),他會認爲 {define 的第二個參數,而 )define 參數列表的右定界符;

  5. 它如今終於明白了,define([, {) 是在調用 define 宏;

  6. m4 對 define([, {) 進行展開,具體的展開過程,咱們不得而知,由於 define 是 m4 內建的宏。咱們只知道在 define([, {) 的展開過程當中,m4 會爲咱們定義 [ 宏,而且 define([, {) 宏展開完成後,m4 會向輸出流發送一個空字串。

m4 處理輸入流的過程,很是像人類,急功近利,目光短淺,一葉障目,不見泰山,管中窺豹,略見一斑……如今明白了吧!第二個 LEFT 宏定義,表面上看起來是重定義了 LEFT 宏,實際上定義的是 [ 宏。

因爲 m4 容許用任何符號做爲宏名,因此定義一個 [ 宏,這種行爲是合法的,只不過 m4 不會真正的將它視爲宏。我一直沒有提 m4 的宏命名規則,如今是談談它的最好的時機,可是沒什麼好說的,在 m4 眼裏,只有像 C 函數名的宏名纔是真正的宏,也就是說,m4 的宏名名規則是:只容許使用字母、數字以及下劃線構造宏名,而且宏名只能以字母或下劃線開頭只有符合宏名規則的宏,m4 纔會將它視爲真正的宏。不過,不符合宏名規則的宏,也是有辦法調用的,之後再講。

若真的想對已定義的宏的從新定義,須要藉助引號。例如:

define(`LEFT', [)dnl
LEFT
define(`LEFT', {)dnl
LEFT

在 m4 語法中,單重引號具備逃逸的做用:當 m4 讀到帶單重引號的文本片斷 S 時,它會將 S 的引號消除,而後繼續處理 S 以後的文本。

如今能夠這樣來理解引號的做用:

  • m4 將一切沒有引號的文本都視爲宏。對於已定義的宏,m4 會將其展開;對於未定義的宏,m4 會按其字面將其輸出。

  • 加了引號的文本,m4 再也不檢測它們是否是宏,而是將其做爲普通文本按字面輸出。

也就是說,加了引號的文本,可讓 m4 不須要判斷它是否是宏。

記號

如今,咱們繼續探究 m4 究竟對於輸入流都作了些什麼。這件事,已經討論了 3 次了,雖然每一次都比前一次更深刻一些,可是迄今爲止,真相依然未能堪破。如今應該到堪破真相的時候了。

m4 對輸入流是以記號(Token)爲單元進行讀取的。通常狀況下,m4 會將讀取的每一個記號直接發送到輸出流,可是當 m4 發現某個單詞是已定義的宏名時,它會將這個宏展開。在對宏進行展開的過程當中,m4 可能會須要讀入更多的文本以獲取宏的參數。宏展開的結果會被插入到輸入流剩餘部分的前端,也就是說,宏展開後所獲得的文本會被 m4 從新讀取,解析爲記號,繼續處理

上面這段文字尤其重要。當 m4 不能如你預期的那樣展開你定義的宏,都應該從新理解上面這段文字。

什麼樣的文本對於 m4 而言是一個記號?帶引號的字符串、宏名、宏參數列表、空白字符(包括換行符)、數字以及其餘符號(包括標點符號),像這些類別的文本,對於 m4 而言都是記號。對於每種記號,m4 都有相應的處理機制。數字與標點符號(西文的),它們自己是記號,同時也是某些記號的邊界,除非它們出現於帶引號的字符串或者宏的參數列表中

來看一個例子:

define(`definenum', `define(`num', `99')') num

若這行文本流經 m4,那麼 m4 讀到的第一個記號是 define。由於 define 後面尾隨的是 (。因爲 ( 便是記號,也是某些記號的邊界。m4 讀取 define 文本以後,就遇到了邊界,所以 define 是 m4 遇到的一個記號。

而後,m4 開始對 define 這個記號進行處理,它發現這個記號是一個帶參數的宏。因此它暫停對 define 的處理,繼續讀取並分析 define 以後的文本,看是否能得到 define 宏的參數列表。

接下來, m4 讀取的是 (,這是個記號,並且是宏參數列表的左定界符。這對 m4 而言,已經開始經進入了一段多是參數列表的文本。它指望接下來能遇到一個 , 或者 ),以獲得完整的參數列表記號。

可是接下來,m4 讀到的是一個左引號。這時,對 m4 而言,已經開始進入了一個多是帶引號的字符串文本,它指望接下來能遇到一些文本或右引號,以獲得一個完整的字符串記號。

可是接下來,m4 讀到是文本片斷 definenum,再讀下去,就讀到了右引號。這時, m4 很高興,它肯定本身已經讀取了一個帶引號的字符串記號,而後它就將包圍這個字符串的引號消除,繼續讀取後面的文本。m4 之因此不在這時將 definenum 發送到輸出端,由於它沒有忘記本身還有一個使命:爲 define 宏搜尋完整的參數列表。

接下來,m4 讀到了 ,——這是宏參數記號的邊界。m4 很高興,它終於獲得了 define 宏的第一個參數,即 definenum。此時,m4 認爲剛纔讀到的 , 就沒什麼用了,因而就將 , 消除了,而後它認爲後面也許還會有第二個參數,決定繼續前進。

接下來,m4 遇到了一個空格。在宏參數列表中,在 , 以後的空格是無心義的字符,m4 將這個空格扔掉,繼續前進。而後它遇到了左引號……因而就像剛纔處理 definenum 同樣,m4 能夠獲得一個帶引號的字符串:

`define(`num', `99')'

m4 將這個字符串的引號消除,而後繼續前進,結果碰到了 )。此時,m4 吁了口氣,它終於爲 define 宏得到了一個完整的參數列表,儘管這個參數列表只含有兩個參數。

接下來,m4 對 define 宏進行展開。這個過程,咱們沒法得知,由於 define 是 m4 內建的宏,可是咱們知道在 define 的展開過程當中確定發生了一系列計算,而後 definenum 變成了一個宏,最終

define(`definenum', `define(`num', `99')')

的展開結果是一個空的字符串。因爲宏展開的結果會被插入到輸入流剩餘部分的前端,也就是說,宏展開後所獲得的文本會被 m4 從新讀取,解析爲記號,繼續處理,所以 m4 會將

define(`definenum', `define(`num', `99')')

的展開結果視爲它下一步繼續要讀取並處理的文本。當 m4 繼續前進時,它就會讀到到一個空的字符串。空的字符串,雖然不具有被 m4 發送到輸出流的資格,可是它能夠做爲其餘記號的邊界記號使用。

接下來,m4 遇到了一個空格字符。空格字符也是個記號,並且是其餘記號的邊界。m4 將空格記號直接發送到輸出流,繼續前進。

接下來,m4 一口氣讀到了輸入流的末尾,獲得了 num 記號。之因此說 num 是一個記號,是由於 num 的左側與右側都有邊界,左側是空格,右側是輸入流終止符。m4 將 num 這個記號視爲宏,而後它肯定這個宏沒有被定義,所以沒法對其進行展開,因此只好將它做爲字符串發送到輸出流。

挑戰

對於如下 m4 文本

define(`definenum', define(`num', `99')) definenum num

推測一下 m4 的處理結果,而後執行 m4 命令檢驗所作的推測是否正確,而後再回顧一次 m4 的工做過程,最後用:

$ m4 -dV your-m4-file

查看一下輸出,根據輸出信息再回顧一次 m4 的工做過程。

下一篇見:http://segmentfault.com/a/119...

相關文章
相關標籤/搜索