總裁兼 CEO, Gentoo Technologies, Inc.
2001 年 4 月
在這篇 awk 系列的總結中,Daniel 向您介紹 awk 重要的字符串函數,以及演示了如何從頭開始編寫完整的支票簿結算程序。在這個過程中,您將學習如何編寫自己的函數,並使用 awk 的多維數組。學完本文之後,您將掌握更多 awk 經驗,可以讓您創建功能更強大的腳本。
格式化輸出
雖然大多數情況下 awk 的 print 語句可以完成任務,但有時我們還需要更多。在那些情況下,awk 提供了兩個我們熟知的老朋友 printf() 和 sprintf()。是的,如同其它許多 awk 部件一樣,這些函數等同於相應的 C 語言函數。printf() 會將格式化字符串打印到 stdout,而 sprintf() 則返回可以賦值給變量的格式化字符串。如果不熟悉 printf() 和 sprintf(),介紹 C 語言的文章可以讓您迅速了解這兩個基本打印函數。在 Linux 系統上,可以輸入 "man 3 printf" 來查看 printf() 幫助頁面。
以下是一些 awk sprintf() 和 printf() 的樣本代碼。可以看到,它們幾乎與 C 語言完全相同。
clearcase/" target="_blank" >cccccc" border="1">x=1b="foo"printf("%s got a %d on the last test\n","Jim",83)myout=("%s-%d",b,x)print myout
此代碼將打印:
Jim got a 83 on the last testfoo-1
字符串函數
awk 有許多字符串函數,這是件好事。在 awk 中,確實需要字符串函數,因為不能象在其它語言(如 C、C++ 和 Python)中那樣將字符串看作是字符數組。例如,如果執行以下代碼:
mystring="How are you doing today?"print mystring[3]
將會接收到一個錯誤,如下所示:
awk: string.gawk:59: fatal: attempt to use scalar as array
噢,好吧。雖然不象 Python 的序列類型那樣方便,但 awk 的字符串函數還是可以完成任務。讓我們來看一下。
首先,有一個基本 length() 函數,它返回字符串的長度。以下是它的使用方法:
print length(mystring)
此代碼將打印值:
24
好,繼續。下一個字符串函數叫作 index,它將返回子字符串在另一個字符串中出現的位置,如果沒有找到該字符串則返回 0。使用 mystring,可以按以下方法調用它:
print index(mystring,"you")
awk 會打印:
9
讓我們繼續討論另外兩個簡單的函數,tolower() 和 toupper()。與您猜想的一樣,這兩個函數將返回字符串並且將所有字符分別轉換成小寫或大寫。請注意,tolower() 和 toupper() 返回新的字符串,不會修改原來的字符串。這段代碼:
print tolower(mystring)print toupper(mystring)print mystring
……將產生以下輸出:
how are you doing today?HOW ARE YOU DOING TODAY?How are you doing today?
到現在為止一切不錯,但我們究竟如何從字符串中選擇子串,甚至單個字符?那就是使用 substr() 的原因。以下是 substr() 的調用方法:
mysub=substr(mystring,startpos,maxlen)
mystring 應該是要從中抽取子串的字符串變量或文字字符串。startpos 應該設置成起始字符位置,maxlen 應該包含要抽取的字符串的最大長度。請注意,我說的是 最大長度 ;如果 length(mystring) 比 startpos+maxlen 短,那麼得到的結果就會被截斷。substr() 不會修改原始字符串,而是返回子串。以下是一個示例:
print substr(mystring,9,3)
awk 將打印:
you
如果您通常用於編程的語言使用數組下標訪問部分字符串(以及不使用這種語言的人),請記住 substr() 是 awk 代替方法。需要使用它來抽取單個字符和子串;因為 awk 是基於字符串的語言,所以會經常用到它。
現在,我們討論一些更耐人尋味的函數,首先是 match()。match() 與 index() 非常相似,它與 index() 的區別在於它並不搜索子串,它搜索的是規則表達式。match() 函數將返回匹配的起始位置,如果沒有找到匹配,則返回 0。此外,match() 還將設置兩個變量,叫作 RSTART 和 RLENGTH。RSTART 包含返回值(第一個匹配的位置),RLENGTH 指定它占據的字符跨度(如果沒有找到匹配,則返回 -1)。通過使用 RSTART、RLENGTH、substr() 和一個小循環,可以輕松地迭代字符串中的每個匹配。以下是一個 match() 調用示例:
print match(mystring,/you/), RSTART, RLENGTH
awk 將打印:
9 9 3
字符串替換
現在,我們將研究兩個字符串替換函數,sub() 和 gsub()。這些函數與目前已經討論過的函數略有不同,因為它們 確實修改原始字符串 。以下是一個模板,顯示了如何調用 sub():
sub(regexp,replstring,mystring)
調用 sub() 時,它將在 mystring 中匹配 regexp 的第一個字符序列,並且用 replstring 替換該序列。sub() 和 gsub() 用相同的自變量;唯一的區別是 sub() 將替換第一個 regexp 匹配(如果有的話),gsub() 將執行全局替換,換出字符串中的所有匹配。以下是一個 sub() 和 gsub() 調用示例:
sub(/o/,"O",mystring)print mystringmystring="How are you doing today?"gsub(/o/,"O",mystring)print mystring
必須將 mystring 復位成其初始值,因為第一個 sub() 調用直接修改了 mystring。在執行時,此代碼將使 awk 輸出:
HOw are you doing today?HOw are yOu dOing tOday?
當然,也可以是更復雜的規則表達式。我把測試一些復雜規則表達式的任務留給您來完成。
通過介紹函數 split(),我們來匯總一下已討論過的函數。split() 的任務是“切開”字符串,並將各部分放到使用整數下標的數組中。以下是一個 split() 調用示例:
numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")
調用 split() 時,第一個自變量包含要切開文字字符串或字符串變量。在第二個自變量中,應該指定 split() 將填入片段部分的數組名稱。在第三個元素中,指定用於切開字符串的分隔符。split() 返回時,它將返回分割的字符串元素的數量。split() 將每一個片段賦值給下標從 1 開始的數組,因此以下代碼:
print mymonths[1],mymonths[numelements]
……將打印:
Jan Dec
特殊字符串形式
簡短注釋 -- 調用 length()、sub() 或 gsub() 時,可以去掉最後一個自變量,這樣 awk 將對 (整個當前行)應用函數調用。要打印文件中每一行的長度,使用以下 awk 腳本:
{ print length() }
財務上的趣事
幾星期前,我決定用 awk 編寫自己的支票簿結算程序。我決定使用簡單的 tab 定界文本文件,以便於輸入最近的存款和提款記錄。其思路是將這個數據交給 awk 腳本,該腳本會自動合計所有金額,並告訴我余額。以下是我決定如何將所有交易記錄到 "ASCII checkbook" 中:
23 Aug 2000 food - - Y Jimmy's Buffet 30.25
此文件中的每個字段都由一個或多個 tab 分隔。在日期(字段 1,)之後,有兩個字段叫做“費用分類帳”和“收入分類帳”。以上面這行為例,輸入費用時,我在費用字段中放入四個字母的別名,在收入字段中放入 "-"(空白項)。這表示這一特定項是“食品費用”。:) 以下是存款的示例:
23 Aug 2000 - inco - Y Boss Man 2001.00
在這個實例中,我在費用分類帳中放入 "-"(空白),在收入分類帳中放入 "inco"。"inco" 是一般(薪水之類)收入的別名。使用分類帳別名讓我可以按類別生成收入和費用的明細分類帳。至於記錄的其余部分,其它所有字段都是不需加以說明的。“是否付清?”字段("Y" 或 "N")記錄了交易是否已過帳到我的帳戶;除此之外,還有一個交易描述,和一個正的美元金額。
用於計算當前余額的算法不太難。awk 只需要依次讀取每一行。如果列出了費用分類帳,但沒有收入分類帳(為 "-"),那麼這一項就是借方。如果列出了收入分類帳,但沒有費用分類帳(為 "-"),那麼這一項就是貸方。而且,如果同時列出了費用和收入分類帳,那麼這個金額就是“分類帳轉帳”;即,從費用分類帳減去美元金額,並將此金額添加到收入分類帳。此外,所有這些分類帳都是虛擬的,但對於跟蹤收入和支出以及預算卻非常有用。
代碼
現在該研究代碼了。我們將從第一行(BEGIN 塊和函數定義)開始:
#!/usr/bin/env awk -fBEGIN { FS="\t+" months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"}function monthdigit(mymonth) { return (index(months,mymonth)+3)/4}
首先執行 "chmod +x myscript" 命令,那麼將第一行 "#!..." 添加到任何 awk 腳本將使它可以直接從 shell 中執行。其余行定義了 BEGIN 塊,在 awk 開始處理支票簿文件之前將執行這個代碼塊。我們將 FS(字段分隔符)設置成 "\t+",它會告訴 awk 字段由一個或多個 tab 分隔。另外,我們定義了字符串 months,下面將出現的 monthdigit() 函數將使用它。
最後三行顯示了如何定義自己的 awk 。格式很簡單 -- 輸入 "function",再輸入名稱,然後在括號中輸入由逗號分隔的參數。在此之後,"{ }" 代碼塊包含了您希望這個函數執行的代碼。所有函數都可以訪問全局變量(如 months 變量)。另外,awk 提供了 "return" 語句,它允許函數返回一個值,並執行類似於 C 和其它語言中 "return" 的操作。這個特定函數將以 3 個字母字符串格式表示的月份名稱轉換成等價的數值。例如,以下代碼:
print monthdigit("Mar")
……將打印:
3
現在,讓我們討論其它一些函數。
財務函數
以下是其它三個執行簿記的函數。我們即將見到的主代碼塊將調用這些函數之一,按順序處理支票簿文件的每一行,從而將相應交易記錄到 awk 數組中。有三種基本交易,貸方 (doincome)、借方 (doexpense) 和轉帳 (dotransfer)。您會發現這三個函數全都接受一個自變量,叫作 mybalance。mybalance 是二維數組的一個占位符,我們將它作為自變量進行傳遞。目前,我們還沒有處理過二維數組;但是,在下面可以看到,語法非常簡單。只須用逗號分隔每一維就行了。
我們將按以下方式將信息記錄到 "mybalance" 中。數組的第一維從 0 到 12,用於指定月份,0 代表全年。第二維是四個字母的分類帳,如 "food" 或 "inco";這是我們處理的真實分類帳。因此,要查找全年食品分類帳的余額,應查看 mybalance[0,"food"]。要查找 6 月的收入,應查看 mybalance[6,"inco"]。
balance,第 2 部分function doincome(mybalance) { mybalance[curmonth,] += amount mybalance[0,] += amount}function doexpense(mybalance) { mybalance[curmonth,] -= amount mybalance[0,] -= amount}function dotransfer(mybalance) { mybalance[0,] -= amount mybalance[curmonth,] -= amount mybalance[0,] += amount mybalance[curmonth,] += amount}
調用 doincome() 或任何其它函數時,我們將交易記錄到兩個位置 -- mybalance[0,category] 和 mybalance[curmonth, category],它們分別表示全年的分類帳余額和當月的分類帳余額。這讓我們稍後可以輕松地生成年度或月度收入/支出明細分類帳。
如果研究這些函數,將發現在我的引用中傳遞了 mybalance 引用的數組。另外,我們還引用了幾個全局變量:curmonth,它保存了當前記錄所屬的月份的數值,(費用分類帳),(收入分類帳)和金額(,美元金額)。調用 doincome() 和其它函數時,已經為要處理的當前記錄(行)正確設置了所有這些變量。
主塊
以下是主代碼塊,它包含了分析每一行輸入數據的代碼。請記住,由於正確設置了 FS,可以用 $ 1 引用第一個字段,用 引用第二個字段,依次類推。調用 doincome() 和其它函數時,這些函數可以從函數內部訪問 curmonth、、 和金額的當前值。請先研究代碼,在代碼之後可以見到我的說明。
{ curmonth=monthdigit(substr(,4,3)) amount= #record all the categories encountered if ( != "-" ) globcat[]="yes" if ( != "-" ) globcat[]="yes" #tally up the transaction properly if ( == "-" ) { if ( == "-" ) { print "Error: inc and exp fields are both blank!" exit 1 } else { #this is income doincome(balance) if ( == "Y" ) doincome(balance2) } } else if ( == "-" ) { #this is an expense doexpense(balance) if ( == "Y" ) doexpense(balance2) } else { #this is a transfer dotransfer(balance) if ( == "Y" ) dotransfer(balance2) } }
在主塊中,前兩行將 curmonth 設置成 1 到 12 之間的整數,並將金額設置成字段 7(使代碼易於理解)。然後,是四行有趣的代碼,它們將值寫到數組 globcat 中。globcat,或稱作全局分類帳數組,用於記錄在文件中遇到的所有分類帳 -- "inco"、"misc"、"food"、"util" 等。例如,如果 == "inco",則將 globcat["inco"] 設置成 "yes"。稍後,我們可以使用簡單的 "for (x in globcat)" 循環來迭代分類帳列表。
在接著的大約二十行中,我們分析字段 和 ,並適當記錄交易。如果 =="-" 且 !="-",表示我們有收入,因此調用 doincome()。如果是相反的情況,則調用 doexpense();如果 和 都包含分類帳,則調用 dotransfer()。每次我們都將 "balance" 數組傳遞給這些函數,從而在這些函數中記錄適當的數據。
您還會發現幾行代碼說“if ( == "Y" ),那麼將同一個交易記錄到 balance2 中”。我們在這裡究竟做了些什麼?您將回憶起 包含 "Y" 或 "N",並記錄交易是否已經過帳到帳戶。由於僅當過帳了交易時我們才將交易記錄到 balance2,因此 balance2 包含了真實的帳戶余額,而 "balance" 包含了所有交易,不管是否已經過帳。可以使用 balance2 來驗證數據項(因為它應該與當前銀行帳戶余額匹配),可以使用 "balance" 來確保沒有透支帳戶(因為它會考慮您開出的尚未兌現的所有支票)。
生成報表
主塊重復處理了每一行記錄之後,現在我們有了關於比較全面的、按分類帳和按月份劃分的借方和貸方記錄。現在,在這種情況下最合適的做法是只須定義生成報表的 END 塊:
END { bal=0 bal2=0 for (x in globcat) { bal=bal+balance[0,x] bal2=bal2+balance2[0,x] } printf("Your available funds: %10.2f\n", bal) printf("Your account balance: %10.2f\n", bal2) }
這個報表將打印出匯總,如下所示:
Your available funds:1174.22Your account balance:2399.33
在 END 塊中,我們使用 "for (x in globcat)" 結構來迭代每一個分類帳,根據記錄在案的交易結算主要余額。實際上,我們結算兩個余額,一個是可用資金,另一個是帳戶余額。要執行程序並處理您在文件 "mycheckbook.txt" 中輸入的財務數據,將以上所有代碼放入文本文件 "balance",執行 "chmod +x balance",然後輸入 "./balance mycheckbook.txt"。然後 balance 腳本將合計所有交易,打印出兩行余額匯總。
升級
我使用這個程序的更高級版本來管理我的個人和企業財務。我的版本(由於篇幅限制不能在此涵蓋)會打印出收入和費用的月度明細分類帳,包括年度總合、淨收入和其它許多內容。它甚至以 HTML 格式輸出數據,因此我可以在 Web 浏覽器中查看它。:) 如果您認為這個程序有用,我建議您將這些特性添加到這個腳本中。不必將它配置成要 記錄 任何附加信息;所需的全部信息已經在 balance 和 balance2 裡面了。只要升級 END 塊就萬事具備了!
我希望您喜歡本系列。有關 awk 的詳細信息,請參考以下列出的參考資料。
參考資料