本文從兩個錯誤的常式開始,提出 Awk 中全域變數汙染的現象,並分析其發生的原因。接下來,針對 Awk 變數範圍的特點,提出兩種避免全域變數汙染的常用方法,引出 Awk 中定義局部變數的方法,並提出修改過後的代碼。然後,通過 Awk 的變數調試功能,提出修改過後代碼存在的不足,並引出編寫通用函數應注意的地方。最後,通過簡單說明 Awk 中包含標頭檔的方法,倡導大家更科學,更有效使用 Awk 這一文本處理利器。文章的結尾提供了一些常用的 Awk 參考文檔,供大家學習參考。
在 C、PHP 等大多數程式語言中,函數內聲明的變數都會自動成為局部變數,變數的生命週期只在函數執行期,函數返回時變數自動銷毀。在 Awk 使用的大多數情況下,我們用到的都是全域變數,不曾關心過變數範圍問題。在幾十上百行的小型指令碼中,一線到底的指令碼的確無可厚非,沒有因函數調用引起的變數交叉引用,固然也不會發生全域變數汙染。隨著指令碼規模的擴大,必將以結構化和模組化的方式來編寫 Awk 指令碼,自訂函數也就成了家常便飯。也許某一天增加一個小功能後發現結果不如所意,而這段代碼改得如此的清晰以至於沒有人會懷疑它的正確性。判斷得出問題一定是曆史性的,但是調試這種問題顯然是既費時又費力的。筆者曾經在這個問題上花費過將近半天的時間,飽受調試痛苦之後,才注意到
Awk 中變數範圍的問題。在此把這些經驗教訓總結成文,以供大家參考。
變數汙染髮生的情況
讓我們通過兩個常式來看看變數汙染髮生的情況:
清單 1. fac1.awk 列印 1 到 10 的階乘
1 #
2 # fac1.awk
3 # original version of fac1.awk
4 #
5
6 function factorial(n)
7 {
8 s=1;
9
10 for (i=1; i<=n; i++)
11 {
12 s *= i;
13 }
14
15 return s;
16 }
17
18 {
19 for (i=1; i<=10; i++)
20 {
21 value = factorial(i);
22 printf("fac(%d) = %d/n", i, value);
23 }
24 }
25
運行並查看結果:
[robert@saphires awk_var]$ echo "" | awk -f fac1.awk
fac(2) = 1
fac(4) = 6
fac(6) = 120
fac(8) = 5040
fac(10) = 362880
列印只顯示出 2, 4, 6, 8, 10 的階乘,而且結果還不對。很明顯程式執行流程出了問題。原因在哪裡呢?由於這個程式比較簡單,經過簡單分析可以得到全域變數 i 在自訂函數 factorial() 中被覆蓋,影響了程式的工作流程,導致結果異常。
上例中 i 這樣的迴圈變數受影響,是最典型的全域變數受汙染的例子。另外,在一些遞迴形式的函數實現中,也很有可能出現變數汙染的情況。讓我們來看一看下面的常式:
清單 2. fac2.awk 列印 1 到 10 的階乘(函數以遞迴方式實現)
1 #
2 # fac2.awk
3 # original version of fac2.awk
4 #
5
6 function factorial(n)
7 {
8 if (n == 1)
9 {
10 i = 1;
11 return i;
12 }
13 else
14 {
15 i = factorial(n-1) * n;
16 return i;
17 }
18 }
19
20 {
21 for (i=1; i<=10; i++)
22 {
23 value = factorial(i);
24 printf("fac(%d) = %d/n", i, value);
25 }
26 }
27
運行並查看結果:
[robert@saphires awk_var]$ echo "" | awk -f fac2.awk
fac(1) = 1
fac(2) = 2
fac(6) = 6
fac(5040) = 5040
結果也很奇怪。究其原因,還是全域變數 i 受汙染所致。雖然上面這一段程式造得有點生硬,一般情況下不會在函數中使用 i 來處理函數的傳回值,不過暴露出來的問題正在 Awk 中全域變數受汙染的問題。
以上兩個常式出現問題的原因是全域變數汙染,但是發生全域變數汙染的原因又是什麼呢? gawk 使用者手冊中有提到,在傳統的 Awk 中,是不支援函數的,程式順序解釋並執行,第一次出現的變數隨即完成初始化,在以後的代碼中得以引用。我們目前在 Linux 下使用的大多數是 gawk ,它是 Awk 的一個擴充實現。在原來 Awk 處理變數的原則下,gawk 引用了自訂函數,改變了原有順序執行流程,即代碼可能跳轉。在沒有局部變數實現的 Awk 中,變數汙染就發生了。接下來我們將分析如何避免因為曆史原因造成的全域變數汙染問題。
避免變數汙染的方法
以上兩個錯誤的常式簡單說明了在 Awk 中易導致變數汙染的常見情況。那麼防止這種情況的方法有哪些呢?最笨的辦法,就是使函數內使用的變數與全域變數不重名。筆者很久以前使用的就是這個笨方法,對於每個函數內的“局部變數”,都以自訂的函數名稱縮寫作為開頭,以防止在全域範圍內的變數名衝突。在一定程度上,這的確是一種有效方法。但是從邏輯上來講,這種方法決不是萬全之計。
理想的解決方案當然是在函數內定義局部變數,其實也是最容易想到的方法。在 C 語言中,函數內定義的變數自動成為局部變數,在函數結束時自動銷毀。在 bash 中,雖然也有跟 Awk 類似的全域變數汙染的情況,但是可以通過 local 關鍵字完成函數內局部變數的聲明,來避免全域變數汙染問題。在 Awk 中如何定義局部變數這個問題好像不是那麼明顯,至少在 gawk 手冊的 “VARIABLES” 一節中沒有關於局部變數的任何線索。非常有趣的是,雖然大部分人第一個想到是這個方案,但最終都走回了第一個方案。筆者也是在一次偶然的閑暇時光重讀
sed & awk 一書時,發現有這樣一段說明:“Awk 提供了一種蹩腳的方式來定義局部變數,那就是通過函數的參數列表。”不久之後,筆者又在 gawk 手冊中 “USER-DEFINED FUNCTIONS” 一節中找到了相似的一段話:“由於原來的 Awk 不支援函數,局部變數在 Awk 中的實現相當笨拙,通過給函數定義額外的參數來實現。按照慣例,在真實參數後面多加幾個空格,以分隔真實參數與局部變數聲明。”
局部變數定義的問題解決之後,讓我們回到剛才有問題的程式,以 fac1.awk 為例,我們來看修改過後的代碼:
清單 3. fac1-2.awk 傳統的局部變數定義
1 #
2 # fac1-2.awk
3 # version 0.2 of fac1.awk
4 #
5
6 function factorial(n, i)
7 {
8 s=1;
9
10 for (i=1; i<=n; i++)
11 {
12 s *= i;
13 }
14
15 return s;
16 }
17
18 {
19 for (i=1; i<=10; i++)
20 {
21 value = factorial(i);
22 printf("fac(%d) = %d/n", i, value);
23 }
24 }
25
運行並查看結果:
[robert@saphires awk_var]$ echo "" | awk -f fac1-2.awk
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800
是的,就這麼簡單,問題解決了!正如手冊中所說,這是一個曆史問題,通過形參來定義函數內局部變數只是一個折衷解決方案。對於不知道其中奧妙的人來講,原來的函數就成了一個可變參數的函數。對於上面的例子,既可以通過 factorial(n) 調用,也可以通過 factorial(n, i) 調用,對於不明白的代碼閱讀者或維護者很可能就此陷入困境。因為在函數的定義中雖然有兩個形參,實際上第二個是我們不希望在函數調用時引用的。
比起書上寫的加空格的方法,筆者更傾向於在正常的形參後面加一個名為 _ARGVEND_ 的參數,表示正常調用所需的形參到此結束,在此標識以後的形參都是“假形參”,實際上只是局部變數的定義。天知道哪天誰看到這樣的多個空格分隔參數的函數會抱怨說“這是哪個蹩腳程式員寫的函式宣告”然後把這幾個空格去掉甚至在函數調用時引用呢。有了這樣的標識,那個真正的“蹩腳程式員”至少會想一下為什麼這裡會有個 _ARGVEND_ 呢?當然這個標識你可以使用任何你喜歡的,但是有一點你必須注意,傳統畢竟是傳統,在筆者提出的方式成為傳統之前,你必須知道多個空格之間的參數是怎麼回事,以免讓你自己成為那個真正的“蹩腳程式員”。下面是以筆者的方式修改過後的代碼:
清單 4. fac1-3.awk 筆者的局部變數定義
1 #
2 # fac1-3.awk
3 # version 0.3 of fac1.awk
4 #
5
6 function factorial(n, _ARGVEND_, i)
7 {
8 s=1;
9
10 for (i=1; i<=n; i++)
11 {
12 s *= i;
13 }
14
15 return s;
16 }
17
18 {
19 for (i=1; i<=10; i++)
20 {
21 value = factorial(i);
22 printf("fac(%d) = %d/n", i, value);
23 }
24 }
25
運行並查看結果:
[robert@saphires awk_var]$ echo "" | awk -f fac1-3.awk
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800
調試全域變數
上面談到了變數汙染髮生的條件以及解決方案。這裡再補充一下出現相關問題時的一個簡單調試方法,那就是 awk 的 -dump-variables 參數,它可以把程式運行結束過後的所有全域變數列印到一個文字檔提供調試。我們來看下面的例子,對 fac1-3.awk 執行的輸出結果:
ass=code-outline>
[robert@saphires awk_var]$ echo "" | awk -f fac1-3.awk --dump-variables=/tmp/var.dump
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800
[robert@saphires awk_var]$ cat /tmp/var.dump
ARGC: number (1)
ARGIND: number (0)
ARGV: array, 1 elements
BINMODE: number (0)
CONVFMT: string ("%.6g")
ERRNO: number (0)
FIELDWIDTHS: string ("")
FILENAME: string ("-")
FNR: number (1)
FS: string (" ")
IGNORECASE: number (0)
LINT: number (0)
NF: number (0)
NR: number (1)
OFMT: string ("%.6g")
OFS: string (" ")
ORS: string ("/n")
RLENGTH: number (0)
RS: string ("/n")
RSTART: number (0)
RT: string ("")
SUBSEP: string ("/034")
TEXTDOMAIN: string ("messages")
i: number (11)
s: number (3628800)
value: number (3628800)
我們可以看到,s 這個在 factorial() 函數中變數其實是被當成了一個全域變數,雖然 fac1-3.awk 的運行結果已經完全符合我們的功能需要,但是如果把這個叫 factorial() 的函數移植到另外一個不是我們寫的代碼裡面去,天知道會不會與其它一個叫 s 的全域變數混用引起變數汙染呢?如果那一天真的到來的話,也許我們要花上好幾個小時才能找出這樣的汙染根源。如何在第一時間避免這樣的災難發生呢?當然是把 s 也定義成局部變數,樣本如下:
清單 5. fac1-4.awk 最終版本
1 #
2 # fac1-4.awk
3 # version 0.4 of fac1.awk
4 #
5
6 function factorial(n, _ARGVEND_, i, s)
7 {
8 s=1;
9
10 for (i=1; i<=n; i++)
11 {
12 s *= i;
13 }
14
15 return s;
16 }
17
18 {
19 for (i=1; i<=10; i++)
20 {
21 value = factorial(i);
22 printf("fac(%d) = %d/n", i, value);
23 }
24 }
25
然後我們再看一下 fac1-4.awk 的 vardump :
[robert@saphires awk_var]$ echo "" | awk -f fac1-4.awk --dump-variables=/tmp/var.dump
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800
[robert@saphires awk_var]$ cat /tmp/var.dump
......(omitted)
i: number (11)
value: number (3628800)
從上面的結果可以看到,在程式運行結束過後,只有兩個我們覺得應該有的全域變數 i 和 value 存在。這樣, factorial() 函數基本上可以作為一個通用函數被移植到其它的 Awk 指令碼中去了。通過對 Awk 全域變數的調試輸出,我們也得出一個書寫 Awk 函數需要注意的原則:那就是只要是局部變數,都應該在參數列表中進行定義,只有這樣才能完全避免全域變數汙染髮生。
使用包含檔案
上面的 fac1-4.awk 中的 factorial() 函數已經可以視為安全的 Awk 函數了。在 Awk 的應用中,是否可以象C語言的#include或bash中的source一樣包含其它源檔案呢?答案是可以的。讓我們先把 factorial() 函數單獨存到 fac-lib.awk 中:
清單 6. fac-lib.awk Awk 函數庫
1 #
2 # library for awk
3 #
4
5 function factorial(n, _ARGVEND_, i, s)
6 {
7 s=1;
8
9 for (i=1; i<=n; i++)
10 {
11 s *= i;
12 }
13
14 return s;
15 }
16
一種方法是通過引用多個 awk 指令碼來實現,這種方式不需要有任何包含源檔案相關的標誌:
清單 7. fac3.awk 不包含 Awk 函數庫的主程式
1 #
2 # fac3.awk
3 # original version of fac3.awk
4 #
5
6 {
7 for (i=1; i<=10; i++)
8 {
9 value = factorial(i);
10 printf("fac(%d) = %d/n", i, value);
11 }
12 }
13
運行並查看結果:
[robert@saphires awk_var]$ echo "" | awk -f fac-lib.awk -f fac3.awk
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800
下面這種方式則更象其它語言中的原始碼包含,但是對於有函數包含的 Awk 指令碼,我們需要用 igawk 來執行。igawk 實際上也只是一個指令碼,它在運行時分析 Awk 指令碼中的 @include 標誌,並把 @include 包含的檔案合并到當前指令碼的 @include 行,然後進行解釋執行。一方面,Awk 所謂的包含功能也這麼的不地道,另一方面,這個包含功能的擴充本身也是由 Awk 完成的,非常有趣。下面來看一下利用 igawk 運行包含函數庫的 Awk 指令碼。
清單 8. fac3-2.awk 包含 Awk 函數庫的主程式
1 #
2 # fac3-2.awk
3 # original version of fac3-2.awk
4 #
5
6 @include fac-lib.awk
7
8 {
9 for (i=1; i<=10; i++)
10 {
11 value = factorial(i);
12 printf("fac(%d) = %d/n", i, value);
13 }
14 }
15
運行並查看結果:
[robert@saphires awk_var]$ echo "" | igawk -f fac3-2.awk
fac(1) = 1
fac(2) = 2
fac(3) = 6
fac(4) = 24
fac(5) = 120
fac(6) = 720
fac(7) = 5040
fac(8) = 40320
fac(9) = 362880
fac(10) = 3628800