總裁兼 CEO,Gentoo Technologies, Inc.
在 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 語言完全相同。
|
此代碼將打印:
|
字符串函數
awk 有許多字符串函數,這是件好事。在 awk 中,確實需要字符串函數,因爲不能象在其它語言(如 C、C++ 和 Python)中那樣將字符串看作是字符數組。例如,如果執行以下代碼:
|
將會接收到一個錯誤,如下所示:
|
噢,好吧。雖然不象 Python 的序列類型那樣方便,但 awk 的字符串函數還是可以完成任務。讓我們來看一下。
首先,有一個基本 length() 函數,它返回字符串的長度。以下是它的使用方法:
|
此代碼將打印值:
|
好,繼續。下一個字符串函數叫作 index,它將返回子字符串在另一個字符串中出現的位置,如果沒有找到該字符串則返回 0。使用 mystring,可以按以下方法調用它:
|
awk 會打印:
|
讓我們繼續討論另外兩個簡單的函數,tolower() 和 toupper()。與您猜想的一樣,這兩個函數將返回字符串並且將所有字符分別轉換成小寫或大寫。請注意,tolower() 和 toupper() 返回新的字符串,不會修改原來的字符串。這段代碼:
|
……將產生以下輸出:
|
到現在爲止一切不錯,但我們究竟如何從字符串中選擇子串,甚至單個字符?那就是使用 substr() 的原因。以下是 substr() 的調用方法:
|
mystring 應該是要從中抽取子串的字符串變量或文字字符串。startpos 應該設置成起始字符位置,maxlen 應該包含要抽取的字符串的最大長度。請注意,我說的是最大長度;如果 length(mystring) 比 startpos+maxlen 短,那麼得到的結果就會被截斷。substr() 不會修改原始字符串,而是返回子串。以下是一個示例:
|
awk 將打印:
|
如果您通常用於編程的語言使用數組下標訪問部分字符串(以及不使用這種語言的人),請記住 substr() 是 awk 代替方法。需要使用它來抽取單個字符和子串;因爲 awk 是基於字符串的語言,所以會經常用到它。
現在,我們討論一些更耐人尋味的函數,首先是 match()。match() 與 index() 非常相似,它與 index() 的區別在於它並不搜索子串,它搜索的是規則表達式。match() 函數將返回匹配的起始位置,如果沒有找到匹配,則返回 0。此外,match() 還將設置兩個變量,叫作 RSTART 和 RLENGTH。RSTART 包含返回值(第一個匹配的位置),RLENGTH 指定它佔據的字符跨度(如果沒有找到匹配,則返回 -1)。通過使用 RSTART、RLENGTH、substr() 和一個小循環,可以輕鬆地迭代字符串中的每個匹配。以下是一個 match() 調用示例:
|
awk 將打印:
|
字符串替換
現在,我們將研究兩個字符串替換函數,sub() 和 gsub()。這些函數與目前已經討論過的函數略有不同,因爲它們確實修改原始字符串。以下是一個模板,顯示瞭如何調用 sub():
|
調用 sub() 時,它將在 mystring 中匹配 regexp 的第一個字符序列,並且用 replstring 替換該序列。sub() 和 gsub() 用相同的自變量;唯一的區別是 sub() 將替換第一個 regexp 匹配(如果有的話),gsub() 將執行全局替換,換出字符串中的所有匹配。以下是一個 sub() 和 gsub() 調用示例:
|
必須將 mystring 復位成其初始值,因爲第一個 sub() 調用直接修改了 mystring。在執行時,此代碼將使 awk 輸出:
|
當然,也可以是更復雜的規則表達式。我把測試一些複雜規則表達式的任務留給您來完成。
通過介紹函數 split(),我們來彙總一下已討論過的函數。split() 的任務是“切開”字符串,並將各部分放到使用整數下標的數組中。以下是一個 split() 調用示例:
|
調用 split() 時,第一個自變量包含要切開文字字符串或字符串變量。在第二個自變量中,應該指定 split() 將填入片段部分的數組名稱。在第三個元素中,指定用於切開字符串的分隔符。split() 返回時,它將返回分割的字符串元素的數量。split() 將每一個片段賦值給下標從 1 開始的數組,因此以下代碼:
|
……將打印:
|
特殊字符串形式
簡短註釋 -- 調用 length()、sub() 或 gsub() 時,可以去掉最後一個自變量,這樣 awk 將對 $0(整個當前行)應用函數調用。要打印文件中每一行的長度,使用以下 awk 腳本:
|
財務上的趣事
幾星期前,我決定用 awk 編寫自己的支票簿結算程序。我決定使用簡單的 tab 定界文本文件,以便於輸入最近的存款和提款記錄。其思路是將這個數據交給 awk 腳本,該腳本會自動合計所有金額,並告訴我餘額。以下是我決定如何將所有交易記錄到 "ASCII checkbook" 中:
|
此文件中的每個字段都由一個或多個 tab 分隔。在日期(字段 1,$1)之後,有兩個字段叫做“費用分類帳”和“收入分類帳”。以上面這行爲例,輸入費用時,我在費用字段中放入四個字母的別名,在收入字段中放入 "-"(空白項)。這表示這一特定項是“食品費用”。:) 以下是存款的示例:
|
在這個實例中,我在費用分類帳中放入 "-"(空白),在收入分類帳中放入 "inco"。"inco" 是一般(薪水之類)收入的別名。使用分類帳別名讓我可以按類別生成收入和費用的明細分類帳。至於記錄的其餘部分,其它所有字段都是不需加以說明的。“是否付清?”字段("Y" 或 "N")記錄了交易是否已過帳到我的帳戶;除此之外,還有一個交易描述,和一個正的美元金額。
用於計算當前餘額的算法不太難。awk 只需要依次讀取每一行。如果列出了費用分類帳,但沒有收入分類帳(爲 "-"),那麼這一項就是借方。如果列出了收入分類帳,但沒有費用分類帳(爲 "-"),那麼這一項就是貸方。而且,如果同時列出了費用和收入分類帳,那麼這個金額就是“分類帳轉帳”;即,從費用分類帳減去美元金額,並將此金額添加到收入分類帳。此外,所有這些分類帳都是虛擬的,但對於跟蹤收入和支出以及預算卻非常有用。
代碼
現在該研究代碼了。我們將從第一行(BEGIN 塊和函數定義)開始:
balance,第 1 部分
|
首先執行 "chmod +x myscript" 命令,那麼將第一行 "#!..." 添加到任何 awk 腳本將使它可以直接從 shell 中執行。其餘行定義了 BEGIN 塊,在 awk 開始處理支票簿文件之前將執行這個代碼塊。我們將 FS(字段分隔符)設置成 "/t+",它會告訴 awk 字段由一個或多個 tab 分隔。另外,我們定義了字符串 months,下面將出現的 monthdigit() 函數將使用它。
最後三行顯示瞭如何定義自己的 awk 。格式很簡單 -- 輸入 "function",再輸入名稱,然後在括號中輸入由逗號分隔的參數。在此之後,"{ }" 代碼塊包含了您希望這個函數執行的代碼。所有函數都可以訪問全局變量(如 months 變量)。另外,awk 提供了 "return" 語句,它允許函數返回一個值,並執行類似於 C 和其它語言中 "return" 的操作。這個特定函數將以 3 個字母字符串格式表示的月份名稱轉換成等價的數值。例如,以下代碼:
|
……將打印:
|
現在,讓我們討論其它一些函數。
財務函數
以下是其它三個執行簿記的函數。我們即將見到的主代碼塊將調用這些函數之一,按順序處理支票簿文件的每一行,從而將相應交易記錄到 awk 數組中。有三種基本交易,貸方 (doincome)、借方 (doexpense) 和轉帳 (dotransfer)。您會發現這三個函數全都接受一個自變量,叫作 mybalance。mybalance 是二維數組的一個佔位符,我們將它作爲自變量進行傳遞。目前,我們還沒有處理過二維數組;但是,在下面可以看到,語法非常簡單。只須用逗號分隔每一維就行了。
我們將按以下方式將信息記錄到 "mybalance" 中。數組的第一維從 0 到 12,用於指定月份,0 代表全年。第二維是四個字母的分類帳,如 "food" 或 "inco";這是我們處理的真實分類帳。因此,要查找全年食品分類帳的餘額,應查看 mybalance[0,"food"]。要查找 6 月的收入,應查看 mybalance[6,"inco"]。
balance,第 2 部分
|
調用 doincome() 或任何其它函數時,我們將交易記錄到兩個位置 -- mybalance[0,category] 和 mybalance[curmonth, category],它們分別表示全年的分類帳餘額和當月的分類帳餘額。這讓我們稍後可以輕鬆地生成年度或月度收入/支出明細分類帳。
如果研究這些函數,將發現在我的引用中傳遞了 mybalance 引用的數組。另外,我們還引用了幾個全局變量:curmonth,它保存了當前記錄所屬的月份的數值,$2(費用分類帳),$3(收入分類帳)和金額($7,美元金額)。調用 doincome() 和其它函數時,已經爲要處理的當前記錄(行)正確設置了所有這些變量。
主塊
以下是主代碼塊,它包含了分析每一行輸入數據的代碼。請記住,由於正確設置了 FS,可以用 $ 1 引用第一個字段,用 $2 引用第二個字段,依次類推。調用 doincome() 和其它函數時,這些函數可以從函數內部訪問 curmonth、$2、$3 和金額的當前值。請先研究代碼,在代碼之後可以見到我的說明。
balance,第 3 部分
|
在主塊中,前兩行將 curmonth 設置成 1 到 12 之間的整數,並將金額設置成字段 7(使代碼易於理解)。然後,是四行有趣的代碼,它們將值寫到數組 globcat 中。globcat,或稱作全局分類帳數組,用於記錄在文件中遇到的所有分類帳 -- "inco"、"misc"、"food"、"util" 等。例如,如果 $2 == "inco",則將 globcat["inco"] 設置成 "yes"。稍後,我們可以使用簡單的 "for (x in globcat)" 循環來迭代分類帳列表。
在接着的大約二十行中,我們分析字段 $2 和 $3,並適當記錄交易。如果 $2=="-" 且 $3!="-",表示我們有收入,因此調用 doincome()。如果是相反的情況,則調用 doexpense();如果 $2 和 $3 都包含分類帳,則調用 dotransfer()。每次我們都將 "balance" 數組傳遞給這些函數,從而在這些函數中記錄適當的數據。
您還會發現幾行代碼說“if ( $5 == "Y" ),那麼將同一個交易記錄到 balance2 中”。我們在這裏究竟做了些什麼?您將回憶起 $5 包含 "Y" 或 "N",並記錄交易是否已經過帳到帳戶。由於僅當過帳了交易時我們纔將交易記錄到 balance2,因此 balance2 包含了真實的帳戶餘額,而 "balance" 包含了所有交易,不管是否已經過帳。可以使用 balance2 來驗證數據項(因爲它應該與當前銀行帳戶餘額匹配),可以使用 "balance" 來確保沒有透支帳戶(因爲它會考慮您開出的尚未兌現的所有支票)。
生成報表
主塊重複處理了每一行記錄之後,現在我們有了關於比較全面的、按分類帳和按月份劃分的借方和貸方記錄。現在,在這種情況下最合適的做法是隻須定義生成報表的 END 塊:
balance,第 4 部分
|
這個報表將打印出彙總,如下所示:
|
在 END 塊中,我們使用 "for (x in globcat)" 結構來迭代每一個分類帳,根據記錄在案的交易結算主要餘額。實際上,我們結算兩個餘額,一個是可用資金,另一個是帳戶餘額。要執行程序並處理您在文件 "mycheckbook.txt" 中輸入的財務數據,將以上所有代碼放入文本文件 "balance",執行 "chmod +x balance",然後輸入 "./balance mycheckbook.txt"。然後 balance 腳本將合計所有交易,打印出兩行餘額彙總。
升級
我使用這個程序的更高級版本來管理我的個人和企業財務。我的版本(由於篇幅限制不能在此涵蓋)會打印出收入和費用的月度明細分類帳,包括年度總合、淨收入和其它許多內容。它甚至以 HTML 格式輸出數據,因此我可以在 Web 瀏覽器中查看它。:) 如果您認爲這個程序有用,我建議您將這些特性添加到這個腳本中。不必將它配置成要 記錄任何附加信息;所需的全部信息已經在 balance 和 balance2 裏面了。只要升級 END 塊就萬事具備了!
我希望您喜歡本系列。有關 awk 的詳細信息,請參考以下列出的參考資料。
參考資料
- 請閱讀 Daniel 在 developerWorks 上發表的 awk 系列中的前幾篇文章:awk 實例,第 1 部分和第 2 部分。
- 如果想看好的老式書籍,O'Reilly 的 sed & awk, 2ndEdition 是極佳選擇。
- 請參考 comp.lang.awkFAQ。它還包含許多附加 awk 鏈接。
- Patrick Hartigan 的 awk tutorial 還包括了實用的 awk 腳本。
- Thompson's TAWKCompiler 將 awk 腳本編譯成快速二進制可執行文件。可用版本有 Windows 版、OS/2 版、DOS 版和 UNIX 版。
- The GNUAwk User's Guide 可用於在線參考。
關於作者
Daniel Robbins 居住在新墨西哥州的 Albuquerque。他是 Gentoo Technologies, Inc. 的總裁兼 CEO,Gentoo Linux(用於 PC 的高級 Linux)和 Portage 系統(Linux 的下一代移植系統)的創始人。他還是 Macmillan 書籍 Caldera OpenLinux Unleashed、SuSE Linux Unleashed 和 Samba Unleashed 的合作者。Daniel 自二年級起就與計算機結下不解之緣,那時他首先接觸的是 Logo 程序語言,並沉溺於 Pac-Man 遊戲中。這也許就是他至今仍擔任 SONY Electronic Publishing/Psygnosis 的首席圖形設計師的原因所在。Daniel 喜歡與妻子 Mary 和新出生的女兒 Hadassah 一起共度時光。可通過 [email protected] 與 Daniel 聯繫。