起因:近日google一把awk的教程,翻到IBM developerworks上一個初級的awk系列文章,是Gentoo Linux創始人Daniel Robbins在多年前寫的。細看一下覺得很不錯,可developerworks中國上面的中文版卻是缺斤少量,比起原文整段整段的丟失,讓人看得糊塗。不知是機器翻譯問題還是版面問題?於是只有去翻看英文版(本人英語那個差啊,逼著看),遂有重新翻譯一遍,權當為自己保留,也為其他有需要的朋友保留吧。儘管是初級內容,還是很值得一看,確實沒想到Gentoo的創始人會寫這麼基礎的東西來普渡眾生。
原文地址: http://www.ibm.com/developerworks/linux/library/l-awk1/index.html
Common threads: Awk by example, Part 1
一個名字怪怪的強大語言介紹
概要:awk是一個有著奇怪名字的強大語言。本文是這個系列中的第一篇,本系列共包括三篇文章。本篇中Daniel將帶你快速掌握awk的編程技巧,隨著系列的進展,將包括更多進階的主題,最終示範一個真實的進階awk應用程式。
為AWK辯護
在這一系列文章中,我將帶領你成為一個熟練的awk程式員。我承認awk並沒有一個漂亮又時尚的名字,並且GNU版的awk(名叫gawk)聽起來也很奇怪。那些不熟悉這個語言的人聽到awk就會想到一團亂糟糟的落伍又過時的代碼,他們認為它甚至導致最博學的UNIX專家也到了瘋狂的邊緣(引起他們不斷的使用“kill -9!”,就像使用咖啡機一樣)。
確實,awk沒有動聽的名字,但他是一個強大的語言。AWK很適合文文書處理和報表產生,還有很多精心設計的特性允許進行嚴謹的編程。並且不像其他有些語言,awk的文法是你很熟悉的,他借用了C、python和bash等語言的一些最好部分(儘管在技術上awk比起python和bash來說出現得要早)。 AWK是這樣一種語言——你一旦學會,他將會成為你編程戰略庫中的一個重要部分。
第一個awk程式
讓我們開始來玩玩awk,看他是如何工作的吧。在命令列輸入下面的命令:
$ awk '{ print }' /etc/passwd |
你將看到你的/etc/passwd檔案出現在你的眼前。現在我們來解釋一下awk做了什麼。我們調用awk時,我們指定/etc/passwd作為輸入檔案。awk執行期間,他依次對/etc/passwd中的每一行執行print命令。所有的輸出都發送到標準輸出,我們得到的結果跟執行cat /etc/passwd一樣。現在解釋一下{ print }代碼塊。類似C語言一樣,awk中使用花括弧組織語句塊。在我們的代碼塊中只有一個print命令。awk中只有print命令單獨出現的時候,當前行的所有內容都會被列印出來。
下面還有另外一個awk例子,他幹上面那個語句所做的相同的事情:
$ awk '{ print $0 }' /etc/passwd |
在awk中,$0代表當前行整行,所以print和print $0實際上是幹一樣的事情。如果你喜歡,你也能建立一個awk程式來輸出與輸入資料完全不相干的資料。下面就是一個例子:
$ awk '{ print "" }' /etc/passwd |
每當你傳遞“”字串給print命令時,他列印一個空白行。如果你測試上面這個指令碼,你將發現awk為/etc/passwd中的每一行都輸出一個空白行。這是因為awk為輸入檔案的每一行都執行你的指令碼。下面還有另外一個例子:
$ awk '{ print "hiya" }' /etc/passwd |
運行這個指令碼,你的螢幕將充滿hiya。:)
多個欄位
Awk處理分為多個邏輯欄位的文本那是真的真的很方便,awk允許你從指令碼中毫不費力地引用到每個單獨的欄位。下面的指令碼將列印出你系統中所有使用者的列表:
$ awk -F":" '{ print $1 }' /etc/passwd |
在上面的例子中,調用awk時,使用-F選項指定“:”作為欄位分割符。當awk處理print $1命令時,他將列印出現在輸入檔案每行中的第一個欄位。這兒是另外一個例子:
$ awk -F":" '{ print $1 $3 }' /etc/passwd |
這兒是這個指令碼的輸出摘錄:
halt7 operator11 root0 shutdown6 sync5 bin1 ....etc. |
如你所見,awk列印出了/etc/passwd檔案中的第一個和第三個欄位,剛好是使用者名稱和uid欄位。現在,儘管這個指令碼工作了,但他並不完美——兩個輸出欄位間沒有任何空格!如果你習慣在bash或python中編程,你可能期望print $1 $3命令在兩個欄位間插入空格。然而,awk程式中當兩個字串相鄰出現時,awk不會增加中間空格來串連他們。下面的命令將在兩個欄位間插入空格:
$ awk -F":" '{ print $1 " " $3 }' /etc/passwd |
當你使用這種方式調用print時,他將串連$1," ",$3,這就建立了一個易讀的輸出。當然,如果有需要,我們也可以插入一些文字標籤:
$ awk -F":" '{ print "username: " $1 "\t\tuid:" $3" }' /etc/passwd |
這個命令的輸出如下:
username: halt uid:7 username: operator uid:11 username: root uid:0 username: shutdown uid:6 username: sync uid:5 username: bin uid:1 ....etc. |
外部指令碼
對於小的單行指令碼來說,作為命令列參數傳給awk非常方便,但當指令碼變成複雜的多行程式,你絕對想要將指令碼組織在一個外部檔案中。然後通過-f選項將該指令檔傳給awk:
$ awk -f myscript.awk myfile.in |
將指令碼放到單獨的文字檔中也允許你利用awk額外的特性。例如,下面這個多行指令碼與我們早前的一個單行指令碼幹一樣的事:列印/etc/passwd中每行的的第一個欄位:
BEGIN { FS=":" } { print $1 } |
這兩種方法的不同之處在於我們如何設定分割符。這個指令碼中,在代碼內部指定分隔字元(通過設定FS變數),而前面的例子則通過在awk命令列中傳遞-F":"選項。僅因為這樣做你可以得到少記一個命令列參數的好處,因此在指令碼中設定分隔字元通常也是最好的。我們將在這篇文章中更詳細的介紹FS變數
BEGIN和END塊
通常情況,awk在輸入檔案的每行上執行指令碼中每個代碼塊。還有在許多編程環境下,你需要在開始處理輸入檔案的文本前執行一些初始化動作。對於這種情況,awk允許你定義BEGIN塊。我們在前面的例子中已經用過一次BEGIN塊。因為BEGIN塊是在awk開始處理輸入檔案前執行的,因此它是進行諸如初始化FS變數、列印頭行或者初始化一些全域變數(程式後面會用到的變數)的好地方。
Awk也提供了一個叫END塊的另一個特殊代碼塊。在輸入檔案的所有行都處理完成後awk執行這個代碼塊的內容。典型的,END塊用來執行最後的計算或者列印那些在輸出資料流末尾應該出現的總結性內容。
Regex和代碼塊
Awk允許使用Regex,根據Regex是否匹配當前行來有選擇的執行一個代碼塊。這兒是一個執行個體指令碼,用來輸出那些包含字元序列“foo”的行:
當然,你也可以使用更加複雜的Regex,下面這個指令碼將僅列印包含浮點數的那些行:
/[0-9]+\.[0-9]*/ { print } |
運算式和代碼塊
還有許多其他的辦法來選擇性的執行代碼塊。我們可以在代碼塊前放置任何類型的布林運算式來控制碼塊何時可以執行。只有代碼塊前的布林運算式值為真時,awk才執行該代碼塊。下面的指令碼例子將輸出第一個欄位值為“fred”的所有行的第三個欄位。如果當前行的第一個欄位不等於“fred”,awk將不會在當前行執行print語句,並繼續處理檔案的其餘行:
$1 == "fred" { print $3 } |
Awk提供了全部的比較子操作,包括使用"==", "<", ">", "<=", ">=", 和 "!="。此外,awk還提供了"~" 和 "!~"運算子,他們分別代表“匹配”和“不匹配”。這兩個運算子左邊是指定的變數,右邊是一個Regex。這兒的例子將僅列印第五個欄位包含字元序列"root"的行的第三個欄位:
條件陳述式
Awk也提供了非常好的類似於C語言的if語句。如果你願意,你可以使用if語句重寫前面的指令碼:
{ if ( $5 ~ /root/ ) { print $3 } } |
兩個指令碼的功能是一樣的。第一個例子中,布林運算式放在代碼塊外面,第二個例子中,在每個輸入行上執行代碼塊,我們將使用if語句有選擇性的執行print命令。兩個方法都可用,你可以選一個最適合你的方式來使用。
這兒還有個更加複雜的awk的if語句例子。如你所見,如此複雜嵌套的if語句看起來跟C語言中的if語句是一樣的:
{ if ( $1 == "foo" ) { if ( $2 == "foo" ) { print "uno" } else { print "one" } } else if ($1 == "bar" ) { print "two" } else { print "three" } } |
使用if語句,我們可以將下面這個代碼:
! /matchme/ { print $1 $3 $4 } |
轉換為:
{ if ( $0 !~ /matchme/ ) { print $1 $3 $4 } } |
兩個指令碼都將只輸出那些不包含“matchme”字串的行。同樣,你也可以選擇最適合你的代碼,他們都幹相同的事情。
Awk也允許使用布爾操作符"||" (邏輯或)以及 "&&"(邏輯與) 來建立複雜的布林運算式:
( $1 == "foo" ) && ( $2 == "bar" ) { print } |
這個例子將只列印那些第一個欄位為foo並且第二個欄位為two的行。
數值變數
到目前為止,我們用awk列印了字串、整行或者特定欄位。然而,awk也允許我們進行整數和浮點數的的數學運算。使用算術運算式,寫一個指令碼來統計一個檔案中的空白行數很容易。這兒就是一個幹這件事的例子:
BEGIN { x=0 } /^$/ { x=x+1 } END { print "I found " x " blank lines. :)" } |
在BEGIN塊中我們將整型變數x初始化為0。 然後,每當awk遇到空白行都將執行x=x+1語句,將x的值加1。在所有行都處理完成後,END塊內的語句執行,awk將輸出最後的總結,指出他找到的空白行數。
串型變數
關於awk變數的一個巧妙的事情是變數是“簡單和串型的(simple and stringy)“。我之所以說awk變數是串型的,是因為所有awk變數在內部都是按字串的形式儲存的。同時,awk變數也是簡單的,是因為只要變數含有數值串,你就可以執行算術運算,awk會自動進行字串到數值的轉換。看看下面這個例子,就明白我說的什麼意思了:
x="1.01" # We just set x to contain the *string* "1.01" x=x+1 # We just added one to a *string* print x # Incidentally, these are comments :) |
Awk將輸出:
有趣吧!儘管我們給變數x賦了一個字串值1.01,我們仍然能夠給x加1。在bash或python中就不能這麼幹。首先,bash不支援浮點算術,其次,儘管bash有字串型變數但他們不是"簡單"的;要執行任何算術操作,bash要求我們使用一個醜陋的$()結構把運算式括起來。如果我們使用python,我們還必須在進行算術運算前顯示的將字串”1.01“轉換為浮點數。有了awk,這一切都是自動進行的,這會讓我們的代碼看起來更清晰乾淨。如果我們想給每個輸入行的第一個欄位進行平方並加1,我們可以用下面的這個指令碼:
做一個小小的嘗試,你會發現在進行算術運算時,如果一個特定的變數並不包含有效數值,awk將認為該變數是數值0。
大量的運算子
awk的另一個好的地方是,他實現了完全的算術運算子。除了標準的加、減、乘、除,awk也給允許我們使用指數運算子"^"(上個例子已用過)、求模運算子"%"和一堆從C語言中借用過來的其他很方便的賦值運算子。
包括前置和後置的自增自減( i++, --foo ),加/減/乘/除賦值運算子( a+=3, b*=2, c/=2.2, d-=6.2 )。這還不是全部--我們也能得到方便的求模/指數賦值運算子(a^=2,b%=4)。
欄位分隔符號
Awk實現了一些自己特有的變數。一些特殊變數允許你微調awk的功能,另外一些能夠用來收集有關輸入資料的有價值的資訊。我們已經接觸過一個特殊變數:FS。如早前所說,這個變數允許你設定awk期望在欄位間找到的字元序列。當我們使用/etc/passwd作為輸入時,FS設定為":"。雖然這確實有效,不過FS還允許我們更加靈活的使用。
FS的值不是只限制為單個字元;他能使用Regex,指定任何長度的字元模式。如果你正在使用一個或多個tab作為欄位分隔符號,你可能想要設定FS為下面的形式:
上面,我們使用特殊的Regex符號”+“,他的意思是:”一個或多個前置字元“。
如果你的欄位是由空格分隔的(一個或多個空格或tab),你可以嘗試使用下面的Regex來設定FS:
儘管這個賦值是有效,不過他不是必須的。為什麼呢?因為FS的預設值是單個空白字元(awk解釋為一個或多個空格或tab)。在上面這個特殊的例子中,FS的預設值正好是你想要的。
複雜的Regex也沒有問題。比如你的記錄由單詞"foo"加上三個數字分隔,下面的Regex將幫你適當的解析:
欄位數量
下面我們將介紹的兩個變數,通常不是用來寫的,而是用來讀取以獲得輸入資料的有用資訊。第一個是NF變數,也叫做”欄位數量“變數。Awk將自動將這個變數設定為目前記錄的欄位數。你能使用這個變數來顯示特定的輸入行:
NF == 3 { print "this particular record has three fields: " $0 } |
當然,你也能在條件陳述式中使用NF變數,如下:
{ if ( NF > 2 ) { print $1 " " $2 ":" $3 } } |
記錄號
記錄號(NR)是另一個方便的變數。它總是包含目前記錄編號(awk處理第一個記錄的編號為1)。直到現在,我們 處理的輸入檔案中每行為一個記錄。在這種情況下,NR將告訴你當前的行號。然而,當我們後面開始處理多行時,將不再是這種情況,所以要小心!NR能像NF一樣湧來列印特定的行:
(NR < 10 ) || (NR > 100) { print "We are on record number 1-9 or 101+" } |
另外一個例子:
{ #skip header if ( NR > 10 ) { print "ok, now for the real information!" } } |
Awk提供的附加變數能用在多種情況下。後面的文章中我們將包含更多的這些變數。我們已經完成的awk初探。隨著這個系列的進行,我將樣本更多進階的awk功能,最後我將用一個真實的awk應用來結束這個系列。與此同時,如果你渴望學的更多,看看下面列出的這些資源吧。
Resources
- If you'd like a good old-fashioned book, O'Reilly's sed & awk, 2nd Edition is a wonderful choice.
- Be sure to check out the comp.lang.awkFAQ. It also contains lots of additional awk links.
- Patrick Hartigan's awk tutorial ispacked with handy awk scripts.
- Thompson's TAWK Compiler compiles awk scripts into fast binary executables. Versions are available for Windows, and DOS.
- The GNU Awk User's Guide is available for online reference.
第一部分結束!