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 程式是由一系列模式 -- 動作對組成的，寫做

```
_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_ 命令也可以顯示變數、計算、函式呼叫的結果：

```
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 的程式檔案，然後即可執行該程式。

## 參見

* sed
* Unix shell
* Perl
* PowerShell

 

## 參考文獻

## 外部連結

* gawk 首頁（頁面存檔備份，存於網際網路檔案館）
* aaa - the Amazing Awk Assembler by Henry Spencer
* Awk 教學（頁面存檔備份，存於網際網路檔案館）