## 注釋符
`#`?符號是行注釋符。不過,與我們所熟悉的注釋文本不同,m4 的注釋文本會被發送到輸出流。例如:
~~~
define(`VERSION',`A1')
VERSION # VERSION `quote' unmatched`
~~~
會被展開為:
~~~
A1 # VERSION `quote' unmatched`
~~~
可以用?`changecom`?宏修改 m4 默認的注釋符,例如
~~~
changecom(`@@')
~~~
這樣,`@@`?就變成了注釋符。
如果你需要塊注釋符,也可以做到,例如:
~~~
changecom(/*,*/)
~~~
如果不向?`changecom`?提供任何參數,其他 m4 實現會恢復默認的注釋符,但是 GNU m4 不會恢復默認的注釋符,而是關閉m4 的注釋功能。如果要恢復默認的注釋符,必須這樣:
~~~
changecom(`#')
~~~
如果不希望 m4 回顯注釋文本,可以用?`dnl`?宏替換注釋符,例如:
~~~
define(`VERSION',`A1')
VERSION dnl VERSION `quote' unmatched`
~~~
`dnl`?會將其后的內容一直連同行尾的換行符統統干掉。
如果讓塊注釋文本不回顯,需要基于條件語句進行一些 hack。不過,由于注釋這種東西并沒有存在的必要,所以就不再理睬它了。之所以說,注釋不重要,是因為我們有更強大的注釋機制——[文式編程](http://segmentfault.com/a/1190000004011686)!
## 引號,逃逸以及非 ASCII 字符
m4 有一個不足之處,它沒有專用的逃逸符。對于非引號字符的字符,引號總是可以作為逃逸符使用。但是,怎么對引號本身進行逃逸呢?畢竟很多場合需要左引號字符作為普通字符出現。
> 事實上,這篇文檔是用 Markdown 標記寫的,我也無法將左引號符號以 Markdown 行內代碼標記表現出來。
雖然可以在引號的外層再封裝一層引號從而將前者變為普通字符,例如:
~~~
I said, ``Quote me.'' # -> I said, `Quote me.'
~~~
但是,有些時候你只想以普通文本的形式顯示左引號,不希望出現一個與之配對的右引號。對于這個問題,可以使用`changequote`?宏修改 m4 默認的引號定界符,例如:
~~~
changequote(<!,!>)
a `quoted string
~~~
m4 會將其處理為:
~~~
a `quoted string
~~~
因為此時,真正的引號是?`<!`?與?`!>`。
如果不向?`changequote`?提供任何參數,就恢復了默認的引號定界符。例如:
~~~
changequote(<!,!>)dnl
a `quoted string
changequote`'dnl
a `quoted string'
~~~
m4 的處理結果為:
~~~
a `quoted string
a quoted string
~~~
一般情況下,應該避免使用?`changequote`,而是將引號字符定義為宏:
~~~
define(`LQ', `changequote(<,>)`dnl'
changequote`'')
define(`RQ',`changequote(<,>)dnl`
'changequote`'')
~~~
m4 遇到?`LQ`?宏將其展開為「`」字符,遇到?`RQ`?宏就將其展開為「'」字符。這兩個宏的定義所體現的技巧是,臨時的改變 m4 默認的引號定界符,然后再改回來。
不過,有時候需要全局性的修改 m4 的默認引號定界符,例如有些鍵盤上沒有「`」字符,或者 m4 要處理的文本必須將「`」字符視為普通字符。使用?`changequote`?一定要小心陷阱:GNU m4 提供的?`changequote`?與其早期版本以及 m4 的其他實現有區別。
為了可移植,要么向?`changequote`?提供 2 個參數來調用它,要么就不提供任何參數,例如:
~~~
changequote
~~~
`changequoe`?會改變宏的定義,例如:
~~~
define(x,``xyz'')
x # -> xyz
changequote({,})
x # -> `xyz'
~~~
不要用同樣的字符作為引號的定界符,這樣做,就無法進行引號的嵌套了。
> Markdown 用于格式化行內代碼的標記用的就是相同的『左引號』與『右引號』……這樣的錯誤,誕生于上個世紀 70 年代的 m4 沒有犯。
不要將引號定界符更改為以字母、下劃線或數字開頭的字符。m4 雖然不反對這樣做,但是它不認為這種字符是引號定界符。數字作為引號定界符,雖然可以被 m4 認可,但是當它作為一個記號本身的組成元素時,它就失去了引號定界符的身份了。
現在的 GNU m4 可以支持非 ASCII 字符,因此也可以用它們來作為引號定界符,例如:
~~~
changequote(左引號, 右引號)
a 左引號quoted string右引號 # -> a quoted string
define(我是宏, 我知道你是宏)
我是宏
~~~
但是最好不要這么干,特別是不要將它們用于宏名。因為,使用 8 位寬的字符,就已經讓 m4 行為有些怪異了。GNU m4 1.4.17 版本(本文寫作過程中所用的 m4 版本)的手冊中說:GNU m4 不理解多字節文本,它只是將文本視為以字節為單位的數據,并且支持 8 位寬的字符作為宏名與引號定界符,但?`NUL`?字符(即?`'\0'`)除外。
m4 能處理中文,這是一種巧合。這種巧合應該只發生在 UTF-8 編碼的輸入流中。因為 UTF-8 的編碼機制決定了中文字符的任何一個字節都與 ASCII 碼不同。如果是 GB2312,GB18030 這樣的字符集,或許就沒有這么好的運氣了。
## 條件
m4 提供了兩種條件宏,`ifdef`?宏用于判斷宏是否定義,`ifelse`?宏是判斷表達式的真假。
~~~
ifdef(`a', b)
~~~
對于上述條件宏,如果?`a`?是已定義的宏,那么這條語句的展開結果是?`b`。
~~~
ifdef(`a', b, c)
~~~
對于上述條件宏,如果?`a`?是未定義的宏,這條語句的展開結果是?`c`。
被測試的宏,它的定義可以是空字串,例如:
~~~
define(`def')
`def' is ifdef(`def', , not) defined. # -> def is defined.
~~~
`ifelse(a,b,c,d)`?會比較字符串?`a`?與?`b`?是否相同,如果它們相同,這條語句的展開結果是字符串?`c`,否則展開為字符串`d`。
`ifelse`?可以支持多個分支,例如:
~~~
ifelse(a,b,c,d,e,f,g)
~~~
它等價于:
~~~
ifelse(a,b,c,ifelse(d,e,f,g))
~~~
## 數字
m4 只認識文本,所以在它看來,數字也是文本。不過 m4 提供了內建宏?`eval`,這個宏可以對整型數的運算表達式進行『求值』——求值結果在 m4 看來依然是文本。
例如:
~~~
define(`n', 1)dnl
`n' is ifelse(eval(n < 2), 1, less than, eval(n == 2), 1, , greater than) 2
~~~
`eval(n < 2)`?是對?`n < 2`?這個邏輯表達式進行『求值』,結果是字符串?`1`,因此?`ifelse`?的第一個參數與第二個參數相等,因此?`ifelse`?宏的展開結果是其第三個參數?`less than`,所以展開結果為:
~~~
n is less than 2
~~~
我覺得沒必要用 m4 來計算,因為它提供的計算功能太孱弱。可以考慮用 GNU bc 來彌補它的不足。在 m4 中,可以通過`esyscmd`?宏訪問 Shell,例如:
~~~
2.1 ifelse(eval(esyscmd(`echo "2.1 > 2.0" | bc')), 1, `greater than', `less than') 2.0
~~~
展開結果為:
~~~
2.1 greater than 2.0
~~~
不過,`esyscmd`?是 GNU m4 對?`syscmd`?的擴展,別的 m4 的實現可能沒有這個宏。
## 挑戰
(1) 如果用 m4 處理 C 代碼文件,將?`#`?符號作為 m4 的行注釋符,會有哪些顯而易見的好處?
(2) 借助 GNU m4 提供的?`esyscmd`?宏,結合 GNU bc,寫一個可以計算數字平方根的的宏。