Awk 執行個體,第 3 部分

來源:互聯網
上載者:User

格式化輸出
雖然大多數情況下 awk 的 print 語句可以完成任務,但有時我們還需要更多。在那些情況下,awk 提供了兩個我們熟知的老朋友 printf() 和 sprintf()。是的,如同其它許多 awk 組件一樣,這些函數等同於相應的 C 語言函數。printf() 會將格式化字串列印到 stdout,而 sprintf() 則返回可以賦值給變數的格式化字串。如果不熟悉 printf() 和 sprintf(),介紹 C 語言的文章可以讓您迅速瞭解這兩個基本列印函數。在 Linux 系統上,可以輸入 "man 3 printf" 來查看 printf() 協助頁面。

以下是一些 awk sprintf() 和 printf() 的樣本代碼。可以看到,它們幾乎與 C 語言完全相同。

x=1     b="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 test     foo-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 mystring     mystring="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 將對 $0(整個當前行)應用函數調用。要列印檔案中每一行的長度,使用以下 awk 指令碼:

{print length() }
            

財務上的趣事
幾星期前,我決定用 awk 編寫自己的支票簿結算程式。我決定使用簡單的 tab 定界文字檔,以便於輸入最近的存款和提款記錄。其思路是將這個資料交給 awk 指令碼,該指令碼會自動合計所有金額,並告訴我餘額。以下是我決定如何將所有交易記錄到 "ASCII checkbook" 中:

23 Aug 2000 food    -   -   Y   Jimmy's Buffet      30.25

此檔案中的每個欄位都由一個或多個 tab 分隔。在日期(欄位 1,$1)之後,有兩個欄位叫做“費用分類帳”和“收入分類帳”。以上面這行為例,輸入費用時,我在費用欄位中放入四個字母的別名,在收入欄位中放入 "-"(空白項)。這表示這一特定項是“食品費用”。:) 以下是存款的樣本:

23 Aug 2000 -   inco    -   Y   Boss Man        2001.00    

在這個執行個體中,我在費用分類帳中放入 "-"(空白),在收入分類帳中放入 "inco"。"inco" 是一般(薪水之類)收入的別名。使用分類帳別名讓我可以按類別產生收入和費用的明細分類帳。至於記錄的其餘部分,其它所有欄位都是不需加以說明的。“是否付清。”欄位("Y" 或 "N")記錄了交易是否已過帳到我的帳戶;除此之外,還有一個交易描述,和一個正的美元金額。

用於計算當前餘額的演算法不太難。awk 只需要依次讀取每一行。如果列出了費用分類帳,但沒有收入分類帳(為 "-"),那麼這一項就是借方。如果列出了收入分類帳,但沒有費用分類帳(為 "-"),那麼這一項就是貸方。而且,如果同時列出了費用和收入分類帳,那麼這個金額就是“分類帳轉帳”;即,從費用分類帳減去美元金額,並將此金額添加到收入分類帳。此外,所有這些分類帳都是虛擬,但對於跟蹤收入和支出以及預算卻非常有用。

代碼
現在該研究代碼了。我們將從第一行(BEGIN 塊和函數定義)開始:

balance,第 1 部分

#!/usr/bin/env awk -f     BEGIN {          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,$3] += amount         mybalance[0,$3] += amount     }          function doexpense(mybalance) {         mybalance[curmonth,$2] -= amount         mybalance[0,$2] -= amount     }          function dotransfer(mybalance) {         mybalance[0,$2] -= amount         mybalance[curmonth,$2] -= amount         mybalance[0,$3] += amount         mybalance[curmonth,$3] += amount     }  

 

調用 doincome() 或任何其它函數時,我們將交易記錄到兩個位置 -- mybalance[0,category] 和 mybalance[curmonth, category],它們分別表示全年的分類帳餘額和當月的分類帳餘額。這讓我們稍後可以輕鬆地產生年度或月度收入/支出明細分類帳。

如果研究這些函數,將發現在我的引用中傳遞了 mybalance 引用的數組。另外,我們還引用了幾個全域變數:curmonth,它儲存了目前記錄所屬的月份的數值,$2(費用分類帳),$3(收入分類帳)和金額($7,美元金額)。調用 doincome() 和其它函數時,已經為要處理的目前記錄(行)正確設定了所有這些變數。

主塊
以下是主代碼塊,它包含了分析每一行輸入資料的代碼。請記住,由於正確設定了 FS,可以用 $ 1 引用第一個欄位,用 $2 引用第二個欄位,依次類推。調用 doincome() 和其它函數時,這些函數可以從函數內部訪問 curmonth、$2、$3 和金額的當前值。請先研究代碼,在代碼之後可以見到我的說明。

balance,第 3 部分

{         curmonth=monthdigit(substr($1,4,3))         amount=$7                  #record all the categories encountered         if ( $2 != "-" )             globcat[$2]="yes"         if ( $3 != "-" )             globcat[$3]="yes"              #tally up the transaction properly         if ( $2 == "-" ) {             if ( $3 == "-" ) {                 print "Error: inc and exp fields are both blank!"                 exit 1             } else {                 #this is income                 doincome(balance)                 if ( $5 == "Y" )                     doincome(balance2)             }         } else if ( $3 == "-" ) {             #this is an expense              doexpense(balance)             if ( $5 == "Y" )                  doexpense(balance2)         } else {             #this is a transfer             dotransfer(balance)             if ( $5 == "Y" )                  dotransfer(balance2)         }                } 

 

在主塊中,前兩行將 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 {         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.22     Your account balance:2399.33

在 END 塊中,我們使用 "for (x in globcat)" 結構來迭代每一個分類帳,根據記錄在案的交易結算主要餘額。實際上,我們結算兩個餘額,一個是可用資金,另一個是帳戶餘額。要執行程式並處理您在檔案 "mycheckbook.txt" 中輸入的財務資料,將以上所有代碼放入文字檔 "balance",執行 "chmod +x balance",然後輸入 "./balance mycheckbook.txt"。然後 balance 指令碼將合計所有交易,列印出兩行餘額匯總。

升級
我使用這個程式的更進階版本來管理我的個人和企業財務。我的版本(由於篇幅限制不能在此涵蓋)會列印出收入和費用的月度明細分類帳,包括年度總合、淨收入和其它許多內容。它甚至以 HTML 格式輸出資料,因此我可以在 網頁瀏覽器中查看它。:) 如果您認為這個程式有用,我建議您將這些特性添加到這個指令碼中。不必將它配置成要 記錄任何附加資訊;所需的全部資訊已經在 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 一起共度時光。可通過 drobbins@gentoo.org 與 Daniel 聯絡。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.