AWK

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

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

## AWK程序結構

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

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

```awk
pattern { action }
```

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

除了一般的，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`)可能類似於某些語言中的變量（例如PHP和perl），但在AWK中，它們指代的是當前記錄的域。另外，`$0`是指整個記錄。事實上，命令`print`和`print $0`的效果是相同的。

`print`命令也可以顯示變量、計算、函數調用的結果：

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

其輸出可以重定向到File:

```awk
print "expression" > "file name"
```

或重定向到管道：

```awk
 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`開頭，後面跟函數名稱，參數列表和函數體。

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

上面的函數可以如此調用：

```awk
print add_three(36)     # 输出'''39'''
```

函數可以擁有其私有變量。其私有變量可以寫在參數列表之後，因為這些值會在調用函數時被忽略。通常可以在參數列表中參數和私有變量之間加入一些空格，用以區別「真正的」參數和私有變量。

函數聲明中，函數名和括號間可以有任意空格，但在調用時二者必須緊鄰。

## 樣例程序

### Hello World

AWK的hello world程序為：

```awk
BEGIN { print "Hello, world!" }
```

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

### 輸出長度大於80的行

輸出長度大於80字符的行。注意模式的默認行為是輸出當前行。

```awk
length($0) > 80
```

### 輸出單詞計數

對輸入中的單詞進行計數，然後輸出行數，單詞數和字符數（類似wc）。

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

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

### 計算最後一個單詞的和

```awk
{ 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`作為輸出，而不是一個空行。

### 匹配輸入行中的範圍

```console
$ 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`，則該範圍的結束即為輸入的結束。命令

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

會輸出從符合正則表達式`^--cut here--$`開始的輸入行，也即從只包含`--cut here--`的行開始，直到輸入的結束。

### 計算詞頻

使用關聯數組計算詞頻：

```awk
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`塊中，我們輸出單詞及其出現的次數。代碼

```awk
for(i in words)
```

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

### 從命令匹配模式

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

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

awk命令中的`$pattern`並沒有為引號所保護。在這裡，模式可以檢查輸入行（`$0`）是否與之匹配。`FILENAME`變量則包含了當前的文件名。awk沒有顯式的字串連接操作符；與bash相似，只需簡單的將字串並列即可。`$0`則會輸出原始的輸入行。

也有另外的方法來完成同樣的任務。下面的腳本直接在awk中訪問環境變量。

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

這個腳本用到了陣列`ENVIRON`，一個One True awk中引入的量。其作用類似與POSIX標準中的C進程控制函數。這個腳本先建立了一個名為`pattern`的環境變量，其值為腳本的第一個參數，然後讓awk在其餘的參數所代表的文件內尋找該模式。

`~`是用於檢查其兩個操作數是否匹配的運算符；其逆則為`!~`。注意正則表達式也屬於普通的字串，可以儲存於變量中。

下面的方法則採用了在命令行對變量賦值的方法，即在awk的參數中寫入一個變量的值：

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

最後，這種方法是純awk的，無需shell的幫助，也無需知道太多關於awk腳本實現的細節（而在命令行對變量賦值的方法可能與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`）。也即

```console
$ awk 'prog'
```

也可以工作，因為程序中已經將`ARGC`置為了`2`；若該值為`1`，則awk會認為沒有文件需要讀取而直接退出。同時，若需從標準輸入讀取數據，需要將文件名顯式的指定為`-`。

## 自包含的AWK腳本

與許多其他的程序語言相似，可以利用「shebang」語法構建自包含的awk腳本。

例如，一個名為`hello.awk`，可以輸出「Hello, world!」的UNIX命令可以通過建立內容如下，名為`hello.awk`的文件來完成：

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

`-f`參數告訴awk將該文件作為awk的程序文件，然後即可運行該程序。

## 外部連結

- gawk主頁
- aaa - the Amazing Awk Assembler by Henry Spencer
- Awk教學

## 基本資料

- screenshot_caption：在shell中使用AWK來檢查兩個文件中匹配欄位
- 編程范型：腳本、過程式、數據驅動
- 發行時間：1977年
- 軟體版本週期： IEEE Std 1003.1-2008 (POSIX) / SUSv4
- 設計者：阿爾佛雷德·艾侯、彼得·溫伯格以及布萊恩·柯林漢
- 類型系統：無；支持字串，整數和浮點數，以及正則表達式
- 軟體實現：awk, GNU Awk, mawk, nawk, MKS AWK, Thompson AWK（編譯器）, Awka（編譯器）
- dialects：「old awk」 oawk 1977, 「new awk」 nawk 1985, 「GNU Awk」 gawk
- 受影響於：C, Sed, SNOBOL
- 施影響於：Tcl, AMPL, Perl
- 作業系統：跨平台
- 網站： cm.bell-labs.com/cm/cs/awkbook/index.html