AWK
- 所有 UNIX 系統裡都至少有一套 awk
- awk 是 POSIX 的一部分
- 把輸入流看成一連串紀錄(record)的集合,每筆紀錄可以更進一步細分成字段(field)
- 如何構成一筆紀錄和一個字段,是可控制的,甚至可以在處理期間修改
- 替使用者妥善處理每個文件的開啟、讀取與關閉
- 強大的功能大多是具備對正則表達式的支援
命令行
1 2 |
awk [ -F fs ] [ -v var=value ... ] 'program' [ -- ] [ var=value ... ] [ file(s) ] |
- 慣例是將
-F
當作第一個選項,或者也可以設定變量FS
來重新定義分隔符12awk -F ':' '{ ... }' files FS="[\f\v]" files以上面例子來看,
-F
應用到第一個文件組,而FS
應用到第二個文件組 -
--
是特殊選項,指出 awk 本身已經沒有更進一步的命令行選項 - 初始化的
-v
必須放在 program 之前,在文件處理之前生效 - program 使用單引號,保護內容不被 Shell 解釋,不過要特別注意程序是否本身包含單引號
程序元素
- 提供標量(scalar)、陣列(array)兩種資料結構
- 提供語句類型:賦值、註釋、條件、函數、輸入、循環、輸出
字串表達式
- 以雙引號定界
- 長度沒有任何限制,視內存而定
- 賦值後舊字串的內存空間會自動回收
- 字串的比較視字串長短而定,如 “A” < “AA” 返回 1
- awk 無字串連接的運算符,多個連續的字串,會自動連接在一起
- 數字轉字串,可透過連接空字串,如 s = “” + 123
正則匹配語句
- 提供兩個運算符:
~
、!~
分別代表匹配和不匹配 - 正則常量可以用雙引號或斜槓
/
定界,斜槓形式比較常見,因為可以用來強調括起來的就是正則表達式 - 字面意義的引號、斜槓、反斜槓在正則裡都應該被保護,兩種形式的保護方式可能不同,例如
"\\\\TeX"
與/\\Tex/
都是表示正則的\Tex
數值表達式
- 所有 awk 裡的數字都以雙精確度的浮點值(對應於 C 的 double)表示
- awk 在 IEEE 754 廣泛可用前就以開發,無法完整支持 Infinity 與 NaN
- 浮點數可以包含一個末端以字母 e(或 E)所表示的十次方指數,例如 0.03125、3.125e-2、0.003125E1
- 字串轉數字,可透過連接0,如 n = 0 + “123”
IEEE 754
- 64 位雙精度的值包含 1 位正負號、11 位偏移指數,以及 53 位顯著位,表示最多可存 16 位的十進制數字 (2^53)
- 值太大無法表示,會顯示 overflow 並回傳 Infinity
- 值未正確定義,如 Infinity – Infinity 或 0 / 0 則結果是 NaN
- 相同正負號的 Infinity 比較時是相等的,NaN 比較時則不等於它自己(假設 x 為 NaN,則 x != x)
數值運算符
- 指數運算與賦值運算具有右結合性,如 a^b^c^d 意即 a^(b^(c^d))
**
與**=
運算符並不在 POSIX 標準內,請用^
與^=
取代- 支援三元條件式,如 a = (u>w)?x^3:y^7
標量變量(scalar)
- 所有 awk 變量在建立時的初始值為空字串,但是當需要數值時,它會被視為 0
- 命名規則:局部變量為小寫、全域變量第一個字母為大寫、內建量全為大寫
數組變量(array)
- 以方括號將任意數字或字串作為索引,稱為關聯數組(key-value pair)
- 儲存空間是稀疏的(sparse),聲明的元素才會被配置
- 以
delete array[index]
或delete array
回收空間,但數組名稱不會被刪除 - 一個變量不能同時用作標量變量和數組變量
- 不支持多維數組如
array[index1][index2]
,但支援多個索引如array[index1, index2]
- 針對逗號分隔的多個索引,awk 將其視為一個字串,以內建變量 SUBSEP 取代逗點,例如
array[index1, index2]
的索引會轉換成index1 SEBSEP index2
,預設的 SUBSEP 是\034
- 為了避免 SUBSEP 被修改,每個程序應該只在 BEGIN 操作裡設置一次
命令行參數
awk 透過內建變量 ARGC(參數計數) 與 ARGV(參數值),讓命令行參數變得可用,以下方範例說明:
1 2 3 4 5 6 7 |
# showargs.awk BEGIN { print "ARGC=", ARGC for (k=0; k < ARGC; k++) print "ARGV[" k "] = [" ARGV[k] "]" } |
1 2 3 4 5 6 7 8 9 |
$ awk -v One=1 -v Two=2 -f showargs.awk Three=3 file1 Four=4 file2 file3 ARGC = 6 ARGV[0] = [awk] ARGV[1] = [Three=3] ARGV[2] = [file1] ARGV[3] = [Four=4] ARGV[4] = [file1] ARGV[5] = [file2] |
須注意的是,program、與 -f 或 -v 選項結合的參數是不可使用的
環境變量
awk 提供內建數組 ENVIRON 來新增、修改及刪除環境變量,但此數組的變更無法傳遞給子進程,因此應該只做讀取使用(readonly)
分隔符
- awk 以內建變量 RS 分隔紀錄(在 POSIX 中為單一字串,預設為換行),再以內建變量 FS (正則表達式,默認為空格)分隔字段
- gawk、mawk 支援 RS 可以是正則表達式
- FS 只有在超過一個字符時,才會被視為正則表達式,如不會視
FS = "."
為正則 - 使用預設 FS 分隔字段時,連續的空格(如 tab)、及行的開頭及結尾的空白會被自動忽略,除非明確指定只匹配一個空格
FS = "[ ]"
123$ echo ' a b c ' | awk -F'[ ]' '{ print NF ":" $0 }'5
字段
- 字段可以透過特殊名稱 $1、$2、$NF 使用,此名稱會轉換成整數,如 $3.14 及 $(27/9) 都會引用到第三個字段
- 不帶$號的 NF 變量代表一筆紀錄的字段數
- 名稱 $0 引用到當前紀錄
- 引用大於 NF 範圍以外的名稱會回傳空字串,引用小於 0 的負值字段編號則會噴錯
模式與操作
- 可以使用逗號隔開的兩個表達式來選取範圍(range expression)
- 在 BEGIN 區塊裡,引用 FILENAME、FNR、NF、NR 會回傳 NULL 或 0,因為初始值尚未定義
- 在 END 區塊裡,FILENAME 是最後一個要處理的輸入文件,FNR、NF、NR 則會保留最後一筆輸入紀錄而來的值
以下這些模式/操作組合的結果是一樣的:
改變輸出的分隔符,至少須指定一個字段(即使沒有做任何變更):
1 2 3 4 5 |
$ echo 'one two three' | awk '{ OFS = ":"; print $0 }' one two three $ echo 'one two three' | awk '{ OFS = ":"; $1 = $1; print $0 }' one:two:three |
常見單行操作
語句
- 連續語句的執行,以單行程序的方式通常用分號隔開,以文件提供的方式通常是一句一行
- 浮點數通常不精確,應避免在 for 語句表達式裡計算非整數的值
- awk 迴圈不支援 shell 風格的
break n
及continue n
幾個基本的複合語句:
1 2 3 4 5 6 7 |
if (expression1) statement1 else if (expression2) statement2 else statement3 |
1 2 3 |
while (expression) statement |
1 2 3 4 |
do statement while (expression) |
1 2 3 |
for (expr1; expr2; expr3) statement |
- expr1: 循環開始之前計算
- expr2: 每次循環開始時計算,若非零則循環繼續
- expr3: 每次循環結束時計算
註:三個表達式皆可以為空
1 2 3 |
for (key in array) statement |
腳本範例:整數因數分解
用戶控制的輸入 getline
getline var
: 從當前輸入讀取下一條紀錄,並存入 var,並更新 NR / FNRgetline < file
: 從 file 中讀取下一條紀錄,存入 $0,並更新 NFgetline var < file
: 從 file 中讀取下一條紀錄,存入 var
範例:讀取並檢查輸入
1 2 3 4 |
print "What is the square root of 625?" getline answer print "Your apply, ", answer ", is", (answer == 25) ? "right." : "wrong." |
如果要確保輸入來自終端而非標準輸入,可改用 getline answer < “dev/tty”
範例:讀取並存入陣列
1 2 3 4 5 |
nwords = 1 while((getline words[nwords] < "/usr/share/dict/words") > 0) nwords++ print "There are ", nwords " words in system dictionary." |
當輸入讀取成功時,返回 +1;當讀取為文件結尾,返回 0;返回 -1 表示錯誤
範例:配合管道
1 2 3 4 |
"date" | getline now close("date") print "The current time is ", now |
在 awk 裡,文件一旦打開後,便會一直保持在打開狀態,而大部分系統會限制打開文件的個數,因此當使用的管道通過時,應透過 close 函數關閉文件。
由於輸入是來自管道,因此關閉輸出管道的操作是在完成時立刻執行。如果需要在相同程序中讀取輸出,可以透過臨時性的文件,例如:
1 2 3 4 5 6 7 8 9 10 |
tmpfile = "/tmp/output.tmp" command = "sort > " tmpfile for (name in telephone) print name "\n" telephone[name] | command close(command) while((getline < tmpfile) > 0) print close(tmpfile) |
調用外部程序
使用 system 命令
- 先清除所有緩衝區輸出,然後啟動一個 /bin/sh 實例執行命令
- 每次調用都會啟動一個新的 Shell
- 傳遞數據至 Shell 只能透過中間文件(中間文件必須在調用 system 之前關閉,確保任何緩衝區輸出皆已正確寫入)
- 不能指定 Shell
1 2 3 4 5 6 |
tmpfile = "/tmp/output.tmp" for (name in telephone) print name "\t" telephone[name] > tmpfile close(tmpfile) system("sort < " tmpfile) |
另一種解法,使用管道傳輸到 Shell,能指定 Shell
1 2 3 4 5 6 |
Shell = "/usr/local/bin/ksh" print "export INPUTFILE=/var/tmp/myfile.in" | Shell print "export OUTPUTFILE=/var/tmp/myfile.out" | Shell print "env | grep PUTFILE" | Shell close(Shell) |
自訂函數
- 慣例是將所有函數放在成對的模式/操作碼之後,並按照字母排序
- 函數中使用但未列在參數列表中的變量,會被視為全域 (global) 的
- 「額外的參數」會被視為局部(local)的,初始值是空字串,慣例是將他們同樣列在參數列表中,並在字首前保留一些空白
範例:數組查找
1 2 3 4 5 6 7 8 |
function find_key(array, value, key) { for (key in array) if (array[key] == value) return key return "" } |
範例:遞迴(resursion)
1 2 3 4 5 6 7 8 9 10 |
# fib.awk function fib(n) { if (n <= 2) return 1 return fib(n-1) + fib(n-2) } { result = fib($1); print "fib(" $1 ") = ", result } |
其他常用函數
substr(string, stasrt, len)
: 提取子字串tolower(string)
、toupper(string)
:大小寫轉換index(string, find)
:尋找第一個出現的子字串match(string, regexp)
:字串匹配split(string, array, regexp)
:分割字串(注意 “” 與 “
[ ]
” 的差異)