AWK

程式語言

AWK是一種優良的文字處理工具,LinuxUnix環境中現有的功能最強大的資料處理引擎之一。這種編程及資料操作語言(其名稱得自於它的創始人阿爾佛雷德·艾侯彼得·溫伯格布萊恩·柯林漢姓氏的首個字母)的最大功能取決於一個人所擁有的知識。AWK提供了極其強大的功能:可以進行正規表示式的匹配,樣式裝入、流控制、數學運算子、行程控制語句甚至於內建的變數和函式。它具備了一個完整的語言所應具有的幾乎所有精美特性。實際上AWK的確擁有自己的語言:AWK程式設計語言,三位建立者已將它正式定義為「樣式掃描和處理語言」。它允許建立簡短的程式,這些程式讀取輸入檔案、為資料排序、處理資料、對輸入執行計算以及生成報表,還有無數其他的功能。gawk是AWK的GNU版本。

AWK
編程範型指令碼程序式資料驅動[1][2]
設計者阿爾佛雷德·艾侯彼得·溫伯格以及布萊恩·柯林漢
面市時間1977年
目前版本
  • IEEE Std 1003.1-2008
編輯維基數據鏈結
型態系統無;支援字串,整數和浮點數,以及正規表示式
作業系統跨平台
網站cm.bell-labs.com/cm/cs/awkbook/index.html
主要實作產品
awk, GNU Awk, mawk, nawk, MKS AWK, Thompson AWK(編譯器), Awka(編譯器)
衍生副語言
old awk oawk 1977, new awk nawk 1985, GNU Awk gawk
啟發語言
C, Sed, SNOBOL[1][2]
影響語言
Tcl, AMPL, Perl

最簡單地說,AWK是一種用於處理文字的程式語言工具。AWK在很多方面類似於Unix shell程式語言,儘管AWK具有完全屬於其本身的語法。它的設計思想來源於SNOBOL4sed、Marc Rochkind設計的有效性語言、語言工具yacclex,當然還從C語言中取得了一些優秀的思想。在最初創造AWK時,其目的是用於文字處理,並且這種語言的基礎是,只要在輸入資料中有模式匹配,就執行一系列指令。該實用工具掃描檔案中的每一行,尋找與命令列中所給定內容相匹配的模式。如果發現匹配內容,則進行下一個編程步驟。如果找不到匹配內容,則繼續處理下一行。

AWK程式結構

AWK是一種處理文字檔案的語言。它將檔案作為記錄序列處理。在一般情況下,檔案內容的每行都是一個記錄。每行內容都會被分割成一系列的域,因此,我們可以認為一行的第一個詞為第一個域,第二個詞為第二個,以此類推。AWK程式是由一些處理特定模式的語句塊構成的。AWK一次可以讀取一個輸入行。對每個輸入行,AWK直譯器會判斷它是否符合程式中出現的各個模式,並執行符合的模式所對應的動作。

——阿爾佛雷德·艾侯,The A-Z of Programming Languages: AWK

AWK程式是由一系列模式--動作對組成的,寫做

pattern { action }

其中pattern表示AWK在資料中尋找的內容,而action是在找到匹配內容時所執行的一系列命令。輸入行被分成了一些記錄:記錄預設由換行符分割,因此輸入會按照行進行分割。程式使用給定的條件一個個的測試每條記錄,並執行測試通過的條件所對應的actionpatternaction都可以省略不寫。無pattern預設匹配全部的記錄;而無action則是列印原始記錄。簡單的AWK表達式之外,pattern可以是BEGINEND;這兩種條件對應的action分別是讀取所有的記錄之前和之後。同時,如pattern1, pattern2的條件表示符合條件pattern1pattern2的記錄及其之間的部分。

除了一般的,C語言風格的算術和邏輯運算子外,AWK允許運算子~,用來測試正規表示式是否可以與一字串匹配。作為語法糖,沒有~運算子的正規表示式會被用來對當前記錄進行測試,相當於/regexp/ ~ $0

AWK命令

AWK命令即為前文例子中以action指代的語句。AWK命令可以包括函式呼叫,變數賦值,計算,及/或各項的組合。標準AWK提供了許多內建函式;其部分實現則可能提供了更多的內建函式。同時,AWK的部分實現支援動態連結庫,使得其可以支援更多的函式。 便利起見,下述例子中可能省略大括號({ })。

print命令

print 命令用於輸出文字。其輸出的文字總是以"輸出記錄分隔符"(Output record separator, ORS)分割的,其預設值為換行符。該命令的最簡形式為:

print
會輸出當前記錄的內容。在AWK中,記錄會被分割成「域」,它們可以被分別顯示或使用:
print $1
顯示當前記錄的第1個域
print $1, $3
顯示當前記錄的第1和第3個域,並以預定義的輸出域分隔符(Output field separator, OFS)分隔,其預設值為一個空格符

雖然域的符號($X )可能類似於某些語言中的變數(例如PHPperl),但在AWK中,它們指代的是當前記錄的域。另外,$0是指整個記錄。事實上,命令printprint $0的效果是相同的。 print命令也可以顯示變數、計算、函式呼叫的結果:

print 3+2
print foobar(3)
print foobar(variable)
print sin(3-2)

其輸出可以重新導向到File:

print "expression" > "file name"

或重新導向到管道

print "expression" | "command"

內建變數

AWK的內建變數包括域變數,例如$1, $2, $3,以及$0。這些變數給出了記錄中域的內容。 內建變數也包括一些其他變數:

  • NR:已輸入記錄的條數。
  • NF:當前記錄中域的個數。記錄中最後一個域可以以$NF的方式參照。
  • FILENAME:當前輸入檔案的檔名。
  • FS:「域分隔符」,用於將輸入記錄分割成域。其預設值為「空白字元」,即空格和制表符。FS可以替換為其它字元,從而改變域分隔符。
  • RS:當前的「記錄分隔符」。預設狀態下,輸入的每行都被作為一個記錄,因此預設記錄分隔符是換行符
  • OFS:「輸出域分隔符」,即分隔print命令的參數的符號。其預設值為空格。
  • ORS:「輸出記錄分隔符」,即每個print命令之間的符號。其預設值為換行符。
  • OFMT:「輸出數字格式」(Format for numeric output),其預設值為"%.6g"。

變數和語法

變數名可以是語言關鍵字外的,只包含大小寫拉丁字母,數字和底線(「_」)的任意字。而運算子「+ - * /」則分別代表加,減,乘,除。簡單的將兩個變數(或字串常數)放在一起,則會將二者串接為一個字串。若二者間至少有一個是常數,則中間可以不加空格;但若二者均為變數,中間必須包括空格。字串常數是以雙引號(「"」)分隔的。語句無需以分號結尾。另外,注釋是以「#」開頭的。

使用者定義函式

函式是以與C語言類似的方式定義的,以關鍵字function開頭,後面跟函式名稱,參數列和函式體。

# 示例函数
function add_three (number) {
  return number + 3
}

上面的函式可以如此呼叫:

print add_three(36)     # 输出39

函式可以擁有其私有變數。其私有變數可以寫在參數列之後,因為這些值會在呼叫函式時被忽略。通常可以在參數列中參數和私有變數之間加入一些空格,用以區別「真正的」參數和私有變數。 函式聲明中,函式名和括號間可以有任意空格,但在呼叫時二者必須緊鄰。

樣例程式

Hello World

AWK的hello world程式為:

BEGIN { print "Hello, world!" }

注意此處無需寫出exit語句,因為唯一的模式是BEGIN

輸出長度大於80的行

輸出長度大於80字元的行。注意模式的預設行為是輸出當前行。

length($0) > 80

輸出單詞計數

對輸入中的單詞進行計數,然後輸出行數,單詞數和字元數(類似wc)。

{
    w += NF
    c += length + 1
}
END { print NR, w, c }

由於沒有提供模式,輸入的全部行都可以匹配該模式,因此對每行都會執行預定操作。注意w+=NF的含義等同於w = w + NF

計算最後一個單詞的和

{ s += $NF }
END { print s + 0 }

s是數值$NF的累加,$NF是每條記錄中的最後一個域,NF(沒有$)是當前行中域的數量。例如一個域數為4的行中$NF相當於$4。事實上,$是一個具有最高優先級一元運算子。(若一行沒有域,則有NF為0,而$NF相當於$0,是整行,在這種情況下,要麼是空字串,要麼只有空白符,因此其數值為0。)

檔案結束時,END模式得到了匹配,因此可以輸出s。然而,在沒有輸入行的情況下,s會沒有值,從而導致沒有輸出。因此,對其加0可以使AWK在這種情況下對其賦值,從而得到一個數值。這種方法是將字串強制轉化為數值的慣用法(反之,與空字串連接則是將數值強制轉換為字串的方法,例如s "")。如此處理之後,若程式輸入為空檔案,可以得到「0」作為輸出,而不是一個空行。

匹配輸入行中的範圍

$ yes Wikipedia | awk 'NR % 4 == 1, NR % 4 == 3 { printf "%6d  %s\n", NR, $0 }' | sed 7q
     1  Wikipedia
     2  Wikipedia
     3  Wikipedia
     5  Wikipedia
     6  Wikipedia
     7  Wikipedia
     9  Wikipedia
$

yes命令重複輸入其參數(預設則是輸出「y」)。在這裡,我們讓該命令輸出「Wikipedia」。動作塊則輸出帶行號的內容。printf函式可以類比標準C中的printf函式,其效果與前述的print函式類似。而符合模式的行是這樣產生的:NR是記錄的編號,也就是AWK正在處理行的行號(從1開始)。「%」是取餘數運算子。因此,NR % 4 == 1對第1,5,9等行為真。類似的,NR % 4 == 3對3,7,11等行為真。範圍模式在其第一部分匹配(例如對第1行)之前為假,並在第二部分匹配(例如第3行)之前為真。然後,再在第二次匹配上其第一部分(例如第5行)前為假。sed命令則是用於截取其前7行輸出,防止yes命令一直執行下去。若head命令可用的話,這行命令的效果和head -n7相同。 若範圍模式的第一部分永遠為真,例如設定為「1」,可以用來使該範圍從輸入的最開始開始。類似的,若第二部分總是為假,例如「0」,則該範圍的結束即為輸入的結束。 命令

/^--cut here--$/, 0

會輸出從符合正規表示式「^--cut here--$」開始的輸入行,也即從只包含「--cut here--」的行開始,直到輸入的結束。

計算詞頻

使用關聯陣列計算詞頻:

BEGIN {
    FS="[^a-zA-Z]+"
}
{
     for(i=1; i<=NF; ++i)
          words[tolower($i)]++
}
END {
    for(i in words)
         print i, words[i]
}

BEGIN塊設定域分隔符為任意非字母字元。值得注意的是,分隔符不僅可以是字串,也可以是正規表示式。然後,程式對每個輸入行執行相同的操作。在此,對每個域,我們累加其小寫形式出現的次數。最後,在END塊中,我們輸出單詞及其出現的次數。代碼

for(i in words)

建立了一個遍歷關聯陣列中元素的迴圈,其中,i會被設為對應的鍵。這一點和多數語言不同,而和Objective-C 2.0中的for...in語法相似。這樣的語法允許以簡單的方式遍歷陣列,從而輸出這些單詞。另外,tolower函式是One True awk(見下文)的附加函式。

從命令匹配模式

這個程式可以以多種不同形式出現。第一個使用Bourne shell指令碼來完成大部分工作。這也是最短的一個方法:

$ cat grepinawk
pattern=$1
shift
awk '/'$pattern'/ { print FILENAME ":" $0 }' $*
$

awk命令中的$pattern並沒有為引號所保護。在這裡,模式可以檢查輸入行($0)是否與之匹配。FILENAME變數則包含了當前的檔名。awk沒有顯式的字串連接運算子;與BASH相似,只需簡單的將字串並列即可。$0則會輸出原始的輸入行。 也有另外的方法來完成同樣的任務。下面的指令碼直接在awk中訪問環境變數

$ cat grepinawk
pattern=$1
shift
awk '$0 ~ ENVIRON["pattern"] { print FILENAME ":" $0 }' $*
$

這個指令碼用到了陣列ENVIRON,一個One True awk中引入的量。其作用類似與POSIX標準中的getenv (3)函式。這個指令碼先建立了一個名為pattern的環境變數,其值為指令碼的第一個參數,然後讓awk在其餘的參數所代表的檔案內尋找該模式。 ~是用於檢查其兩個運算元是否匹配的運算子;其逆則為!~。注意正規表示式也屬於普通的字串,可以儲存於變數中。 下面的方法則採用了在命令列對變數賦值的方法,即在awk的參數中寫入一個變數的值:

$ cat grepinawk
pattern=$1
shift
awk '$0 ~ pattern { print FILENAME ":" $0 }' "pattern=$pattern" $*
$

最後,這種方法是純awk的,無需shell的幫助,也無需知道太多關於awk指令碼實現的細節(而在命令列對變數賦值的方法可能與awk的實現相關);但這種方法的指令碼有點長:

BEGIN {
    pattern = ARGV[1]
    for (i = 1; i < ARGC; i++) # 去除第一个参数
        ARGV[i] = ARGV[i + 1]
    ARGC—if (ARGC == 1) { # 模式是唯一参数,因此强制从标准输入读取
        ARGC = 2
        ARGV[1] = "-"
    }
}
$0 ~ pattern { print FILENAME ":" $0 }

BEGIN塊的作用不僅僅是提取出第一個參數,也防止第一個參數在BEGIN塊結束後直接被解釋為輸入檔案。ARGC,輸入參數的數量永遠是不小於1的,因為ARGV[0]是執行指令碼的命令名,通常是"awk"。另外,ARGV[ARGC]永遠是空字串。對於其中的if塊,它表明若沒有指定輸入檔案,awk會直接讀取標準輸入流stdin)。也即

awk 'prog'

也可以工作,因為程式中已經將ARGC置為了2;若該值為1,則awk會認為沒有檔案需要讀取而直接退出。同時,若需從標準輸入讀取資料,需要將檔名顯式的指定為-

自包含的AWK指令碼

與許多其他的程式語言相似,可以利用「shebang」語法構建自包含的awk指令碼。 例如,一個名為hello.awk,可以輸出「Hello, world!」的UNIX命令可以通過建立內容如下,名為hello.awk的檔案來完成:

#!/usr/bin/awk -f
BEGIN { print "Hello, world!" }

-f參數告訴awk將該檔案作為awk的程式檔案,然後即可執行該程式。

參見


參考文獻

  1. ^ 1.0 1.1 Andreas J. Pilavakis. UNIX Workshop. Macmillan International Higher Education. 1989: 196. 
  2. ^ 2.0 2.1 Arnold Robbins. Effective Awk Programming: Universal Text Processing and Pattern Matching 4th. O'Reilly Media. 2015: 560. 

外部連結