前面的話:
這幾天寫了一個程序,在同一個目錄里生成了很多文件,需要統計其中部分文件的總大小,發現經常用到的ls、du等命令都無濟於事,我甚至都想到了最笨的方法,寫一個腳本:mkdir一個新目錄,把要統計總大小的文件mv過去,然后du或者ls -lh新目錄。誠然,這個辦法又笨又不精確,於是求助萬能的網絡,找到的都是同一篇用了3個很長的循環來統計的腳本,還是自己先苦讀“經書”吧。鳥哥的書第十二章就有現成的示例,就用到了馬上要出場的awk工具,用法如下(統計目錄下所有tmp*文件的總大小,以KB為單位輸出):
ls -l tmp* | awk 'BEGIN{total=0} {total+=$5} END{printf "%.2f KB\n", total/1024}'
鳥哥的書第十二章后面推薦了一篇awk的高級文獻,我下載來看發現頭疼的是,全篇都是繁體字,雖然是80年代的文獻,既然鳥哥的書里面推薦了,說明還是很有參考價值的。於是我萌發了把全篇“翻譯”過來的念頭,網上也有很多“譯文”了,但是想要好好學習,還是自己再全部“推敲”一遍吧,而且能保證有始有終。關於原文,鳥哥的網站有備份:
http://linux.vbird.org/linux_basic/0330regularex/awk.pdf
另外在網上搜索的過程中,也找到了一些不錯的教程和筆記,這里貼一個鏈接mark一下。
http://man.lupaworld.com/content/manage/ringkee/awk.htm
下面進入正題,有些貼圖是本人在機器上執行過之后截取貼上來的,也希望大家能自己動手,切實掌握AWK的知識。
1 前言
- 有關本文
這是一本AWK學習指南,其重點在於:
AWK適用於解決哪些問題?
AWK常見的解題模式是什么?
為使讀者快速掌握awk解題的模式及特性,本手冊系由一些較具代表性的范例及其題解所構成;各范例由淺入深,彼此間相互連貫,范例中並對所使用的awk語法及指令輔以必要的說明。有關awk的指令、函數、...等條列式的說明則收錄於附錄中,以利讀者往后撰寫程序時查閱。 如此編排,可讓讀者在短時間內順利地學會使用awk來解決問題。建議讀者循着范例上機實習,以加深學習效果。
- 讀者宜先具備下列背景知識
a. UNIX 環境下的簡單操作及基本概念。
例如:文件編輯, 文件復制 及 管道, 輸入/輸出重定向 等概念。
b. C 語言的基本語法及流程控制指令。
例如:printf(), while() ...
(注:awk 指令並不多,且其中的大部分與 C語言中的用法一致,本手冊中對該類指令的語法及特性不再加以繁冗的說明,讀者若欲深究,可自行翻閱相關的 C 語言書籍)
- 參考書
本文以學習指引為主要編排方式,讀者若需要有關AWK介紹詳盡的參考書,可以參考下列兩本書:
— Alfred V. Aho, Brian W. Kernighan and Peter J. Weinberger, “The AWK Programming Language", Addison-Wesley Publishing Company
— Dale Dougherty, "sed & awk", O`Reilly & Associates, Inc
2 AWK概述
2.1 為什么用AWK
由於awk具有上述特色,在問題處理的過程中,可輕易使用awk來撰寫一些小工具;這些小工具並非用來解決整個大問題,它們只扮演解決個別問題過程的某些角色,可通過Shell所提供的pipe將數據按需要傳送給不同的小工具進行處理,以解決整個大問題。這種解題方式,使得這些小工具可因不同需求而被重復組合及使用(reuse);也可通過這種方式來先行測試大程序原型的可行性與正確性,將來若需要較高的執行速度時再用C語言來改寫。這是awk最常被應用之處。若能常常如此處理問題,讀者可以以更高的角度來思考抽象的問題,而不會被拘泥於細節的部分。本手冊作為awk入門的學習指引,其內容將先強調如何撰寫awk程序,未列入進一步解題方式的應用實例,這部分將留待UNIX進階手冊中再行討論。
2.2 如何取得awk
一般的UNIX操作系統,本身即帶有awk。不同的UNIX操作系統所帶的awk其版本亦不盡相同。若讀者所使用的系統上未帶有awk,可通過anonymous ftp到下列地方取得:
phi.sinica.edu.tw:/pub/gnu
ftp.edu.tw:/UNIX/gnu
prep.ai.mit.edu:/pub/gnu
2.3 awk如何工作
為便於解釋awk程序架構,及有關術語(terminology),先以一個員工薪資數據文件(emp.dat),來加以介紹。
數據文件中各字段依次為 員工ID、姓名、時薪 及 實際工時。ID中的第一個字母為部門識別碼,"A"、"P"分別表示"組裝"及"包裝"部門。
本小節着重於說明awk程序的主要架構及工作原理,並對一些重要的名詞加以必要的解釋。通過學習這部分內容,讀者可體會出awk語言的主要精神及awk與其它語程序言的差別。為便於說明,之后以條列方式說明。
- 名詞定義
1. 記錄(Record):awk從數據文件上讀取數據的基本單位。以上列數據文件emp.dat為例,awk讀入的
第一條記錄是 "A125 Jenny 100 210"
第二條記錄是 "A341 Dan 110 215"
一般而言, 一條 記錄 就相當於數據文件上的一行資料。 (參考 : 附錄 B 內建變量"RS")
2. 字段(Field):為記錄中被分隔開的子字符串。以數據行"A125 Jenny 100 210"為例,
第一個 | 第二個 | 第三個 | 第四個 |
“A125" | "Jenny" | 100 | 210 |
一般是以空格符來分隔相鄰的字段。( 參考:附錄 D 內建變量"FS" )
- 如何執行AWK
在UNIX的命令行上輸入下列格式的指令:("$"表示Shell命令行上的提示符號)
$ awk 'awk程序' 數據文件名
則awk會先編譯該程序,然后執行該程序來處理所指定的數據文件。(上述方式直接把程序寫在UNIX的命令行上)
- awk程序的主要結構:
awk程序中主要語法是 Pattern { Actions },故常見的awk程序其形式如下:
Pattern1 { Actions1 }
Pattern2 { Actions2 }
......
Pattern3 { Actions3 }
- Pattern 是什么 ?
awk 可接受許多不同形式的 Pattern。一般常使用 "關系表達式"(Relational expression)來當作 Pattern。
例如:
x > 34 是一個Pattern,判斷變量 x 與 34 是否存在大於的關系。
x == y 是一個Pattern,判斷變量 x 與變量 y 是否存在等於的關系。
上式中 x >34 、 x == y 便是典型的Pattern。
awk 提供 C 語言中常見的關系運算符(Relational Operators) 如 >, <, >=, <=, ==, !=。此外,awk 還提供 ~ (match) 及 !~(not match) 二個關系運算符(注一)。
其用法與涵義如下:
若 A 為一字符串,B 為一正則表達式(Regular Expression)
A ~ B 判斷 字符串A 中是否 包含 能匹配(match)B表達式的子字符串。
A !~ B 判斷 字符串A 中是否 不包含 能匹配(match)B表達式的子字符串。
例如 :
"banana" ~ /an/ 整個是一個Pattern。
因為"banana"中含有可以匹配 /an/ 的子字符串,故此關系式成立(true),整個Pattern的值也是true。
相關細節請參考 附錄 A Patterns, 附錄 E Regular Expression
(注一:) 有少數awk文獻,把 ~, !~ 當成另一類的 Operator,並不視為一種 Relational Operator。本手冊中將這兩個運算符當成一種 Relational Operator。
- Actions 是什么?
Actions 是由許多awk指令構成。而awk的指令與 C 語言中的指令十分類似。
例如:
awk的 I/O指令:print, printf( ), getline, ...
awk的 流程控制指令:if(...){..} else{..}, while(...){...}, ...
(請參考 附錄 B --- "Actions" )
- awk 如何處理 Pattern { Actions } ?
awk 會先判斷(Evaluate) 該 Pattern 的值,若 Pattern 判斷后的值為true (或不為0的數字,或不是空的字符串),則awk將執行該 Pattern 所對應的 Actions。反之,若 Pattern 的值不為 true,則awk將不執行該 Pattern所對應的 Actions。
例如:若awk程序中有下列兩指令
50 > 23 {print "Hello! The word!!" }
"banana" ~ /123/ {print "Good morning !" }
awk會先判斷 50 >23 是否成立。因為該式成立,所以awk將打印出"Hello! The word!!"。而另一 Pattern 為"banana"~/123/,因為"banana" 內未含有任何子字符串可 match /123/,該 Pattern 的值為false,故awk將不會打印出 "Good morning !"
- awk 如何處理{ Actions } 的語法?(缺少Pattern部分)
有時語法 Pattern { Actions }中,Pattern 部分被省略,只剩 {Actions}。這種情形表示 "無條件執行這個 Actions"。
- awk 的字段變量
awk 所內建的字段變量及其涵意如下 :
字段變量 |
含義 |
$0 |
一字符串,其內容為目前 awk 所讀入的整行數據。 |
$1 |
$0 上第一個字段的數據。 |
$2 |
$0 上第二個字段的數據。 |
... |
其余類推 |
- 讀入數據行時,awk如何更新(update)這些內置的字段變量?
1. 當 awk 從數據文件中讀取一行數據時,awk 會使用內置變量$0 予以記錄。
2. 每當 $0 被改動時 (例如:讀入新的數據行 或 自行變更 $0) awk 會立刻重新分析 $0 的字段情況,並將 $0 上各字段的數據用 $1、$2、...等予以記錄。
- awk的內置變量(Built-in Variables)
awk 提供了許多內置變量,使用者在程序中可使用這些變量來取得相關信息(不用加$)。常見的內置變量有:
內置變量 |
含義 |
NF (Number of Fields) |
為一整數,其值表示$0上所存在的字段總數。 |
NR (Number of Records) |
為一整數,其值表示awk已讀入的數據行數目。 |
FILENAME |
awk正在處理的數據文件名。 |
例如 : awk 從數據文件 emp.dat 中讀入第一行記錄"A125 Jenny 100 210" 之后,程序中:
$0 的值將是 "A125 Jenny 100 210"
$1 的值為 "A125" $2 的值為 "Jenny"
$3 的值為 100 $4 的值為 210
NF 的值為 4 $NF 的值為 210 (筆者注:$NF即為$4)
NR 的值為 1 FILENAME 的值為 "emp.dat"
- awk的工作流程 :
執行awk時,它會反復進行下列四步驟。
-
- 自動從指定的數據文件中讀取一個數據行。
- 自動更新(Update)相關的內置變量的值。如:NF, NR, $0...
- 依次執行程序中 所有 的 Pattern { Actions } 指令。
- 當執行完程序中所有 Pattern { Actions } 時,若數據文件中還有未讀取的數據,則反復執行步驟1到步驟4。
awk會自動重復進行上述4個步驟,使用者不須在程序中編寫這個循環 (Loop)。
3 怎樣計算並打印文件中指定的字段數據
awk 處理數據時,它會自動從數據文件中一次讀取一條記錄,並會將該記錄切分成一個個的字段;程序中可使用 $1, $2,... 直接取得各個字段的內容。這個特色讓使用者易於用 awk 編寫 reformatter 來改變數據格式。
范例:以數據文件 emp.dat 為例,計算每人應發工資並打印報表。
分析:awk 會自行一次讀入一條記錄,故程序中僅需告訴 awk 如何處理所讀入的數據行。
執行如下命令:($ 表UNIX命令行上的提示符)
$ awk '{ print $2, $3 * $4 }' emp.dat
執行結果如下:
屏幕出現:
說明:
1. UNIX命令行上,執行awk的語法為:
$ awk 'awk程序' 要處理的數據文件名
本范例中的 程序部分為 {print $2, $3 * $4}。把程序置於命令行時,程序之前后必須以 ' (單引號)括住。
2. emp.dat 為指定給該程序處理的數據文件名。
3. 本程序中使用:Pattern { Actions } 語法。
Pattern | Actions |
print $2, $3 * $4 |
4. print為 awk 所提供的輸出指令,會將數據輸出到stdout(屏幕)。print 的參數間彼此以 "," (逗號) 隔開,打印出數據時彼此間會以空白隔開。(參考 附錄 D 內置變量OFS)
5. 將上述的 程序部分 儲存於文件 pay1.awk 中,執行命令時再指定 awk程序文件 的文件名。這是執行awk的另一種方式,特別適用於程序較大的情況,其語法如下:
$ awk -f awk程序文件名 數據文件名
故執行下列兩命令,將產生同樣的結果。
$ awk -f pay1.awk emp.dat $ awk '{ print $2, $3 * $4 }' emp.dat
讀者可使用 "-f" 參數,讓awk主程序使用“其它 僅含 awk函數 的程序文件中的函數 ”
其語法如下:
$ awk -f awk主程序文件名 -f awk函數文件名 數據文件名
(有關 awk 中函數的聲明與使用於 7.4 中說明)
6. awk中也提供與 C 語言中類似用法的 printf() 函數,使用該函數可進一步控制數據的輸出格式。
編輯另一個awk程序如下,並取名為 pay2.awk
{ printf("%6s Work hours: %3d Pay: %5d\n", $2, $3, $3 * $4) }
執行下列命令
$ awk -f pay2.awk emp.dat
執行結果屏幕出現:
4 通過文本內容和對比選擇指定的記錄
Pattern { Action }為awk中最主要的語法。若某Pattern的值為真則執行它后面的 Action。 awk中常使用"關系表達式" (Relational Expression)來當成 Pattern。
awk 中除了>, <, ==, != ,...等關系運算符( Relational Operators )外,另外提供 ~(match),!~(Not Match) 二個關系運算符。利用這兩個運算符,可判斷某字符串是否包含能匹配所指定正則表達式的子字符串。由於這些特性,很容易使用awk來編寫需要字符串比對、判斷的程序。
范例:接上例,
1. 組裝部門員工調薪5%,(組裝部門員工的ID以"A"開頭)
2. 所有員工最后的薪資率若仍低於100,則以100計。
3. 編寫 awk 程序打印新的員工薪資率報表。
分析:這個程序須先判斷所讀入的數據行是否滿足指定條件,再進行某些動作。awk中 Pattern { Actions } 的語法已涵蓋這種 " if ( 條件) { 動作} "的架構。
編寫如下的程序, 並取名 adjust1.awk
$1 ~ /^A.*/ { $3 *= 1.05 } $3 < 100 { $3 = 100 } { printf("%s %8s %d\n", $1, $2, $3)}
執行下列命令:
$ awk -f adjust1.awk emp.dat
結果如下:屏幕出現:
說 明:
1. awk的工作流程是:從數據文件中每次讀入一行數據,依序執行完程序中所有的 Pattern{ Action }指令
Pattern | Actions |
$1~/^A.*/ | { $3 *= 1.05 } |
$3 < 100 | { $3 = 100 } |
{printf("%s %8s %d\n",$1,$2,$3)} |
再從數據文件中讀進下一條記錄繼續進行處理。
2. 第一個 Pattern { Action }是:
$1 ~ /^A.*/ { $3 *= 1.05 }
$1 ~ /^A.*/ 是一個Pattern,用來判斷該行數據的第一個字段是否包含以"A"開頭的子字符串。其中 /^A.*/ 是一個Regular Expression,用以表示任何以"A"開頭的字符串。(有關 Regular Expression 的用法 參考 附錄 E )。
Actions 部分為 $3 *= 1.05。$3 *= 1.05 與 $3 = $3 * 1.05 意義相同,運算符"*=" 的用法則與 C 語言中一樣。此后與 C 語言中用法相同的運算符或語法將不予贅述。
3. 第二個 Pattern { Actions } 是:
$3 < 100 { $3 = 100 }
若第三個字段內容(即時薪)小於100,則調整為100。
4. 第三個 Pattern { Actions } 是:
{printf("%s %8s %d\n",$1, $2, $3)}
省略了Pattern(無條件執行Actions),故所有數據行調整后的數據都將被打印。
5 AWK中的數組
awk程序中允許使用字符串當做數組的下標(index)。利用這個特色十分有助於資料統計工作。(使用字符串當下標的數組稱為Associative Array)
首先建立一個數據文件,並取名為 reg.dat。此為一學生注冊的資料文件;第一欄為學生姓名,其后為該生所修課程。
awk中數組的特性
1. 使用字符串當數組的下標(index)。
2. 使用數組前不須聲明數組名及其大小。
例如:希望用數組來記錄 reg.dat 中各門課程的修課人數。這情況,有兩項信息必須儲存:
(a) 課程名稱,如: "O.S.","Arch.".. ,共有哪些課程事先並不明確。
(b) 各課程的修課人數。 如:有幾個人修"O.S."
在awk中只要用一個數組就可同時記錄上列信息。其方法如下:
使用一個數組 Number[ ]:
* 以課程名稱當 Number[ ] 的下標。
* 以 Number[ ] 中不同下標所對映的元素代表修課人數。
例如:
有2個學生修 "O.S.",則以 Number["O.S."] = 2 表示。
若修"O.S."的人數增加一人,則 Number["O.S."] = Number["O.S."] + 1
或 Number["O.S."]++ 。
3. 如何取出數組中儲存的信息
以 C 語言為例,聲明 int Arr[100];之后,若想得知 Arr[ ]中所儲存的數據,只須用一個循環,如:
for(i=0; i<100; i++)
printf("%d\n", Arr[i]);
即可。上式中:
數組 Arr[ ] 的下標: 0, 1, 2,..., 99
數組 Arr[ ] 中各下標所對應的值: Arr[0], Arr[1],...Arr[99]
但 awk 中使用數組並不須事先聲明。以剛才使用的 Number[ ] 而言,程序執行前,並不知將來有哪些課程名稱可能被當成Number[ ]的下標。
awk 提供了一個指令,通過該指令awk會自動查找數組中使用過的所有下標。以 Number[ ] 為例,awk將會找到 "O.S.","Arch.",...
使用該指令時,須指定所要查找的數組,及一個變量。awk會使用該變量來記錄從數組中找到的每一個下標。例如
for(course in Number){
...
}
指定用 course 來記錄 awk 從Number[ ] 中所找到的下標。awk每找到一個下標時,就用course記錄該下標的值且執行{....}中的指令。通過這個方式便可取出數組中儲存的信息。(詳見下例)
范例:統計各科修課人數,並印出結果。
建立如下程序,並取名為 course.awk:
{ for( i=2; i <= NF; i++) Number[$i]++ } END{
for(course in Number)
printf("%10s %d\n", course, Number[course] ) }
執行下列命令:
$ awk -f course.awk reg.dat
執行結果如下:
說 明:
1. 這程序包含兩個Pattern { Actions }指令。
Pattern | Actions |
{ for( i=2; i <= NF; i++) Number[$i]++ } | |
END | { for(course in Number) printf("%10s %d\n", course, Number[course] )} |
2. 第一個Pattern { Actions }指令中省略了Pattern 部分。故隨着每行數據的讀入其Actions部分將逐次無條件被執行。以awk讀入第一條記錄 " Mary O.S. Arch. Discrete" 為例,因為該筆數據 NF = 4(有4個字段),故該 Action 的for Loop中i = 2,3,4。
i | $i | 最初 Number[$i] | Number[$i]++ 之后 |
2 | "O.S." | AWK default Number["O.S."] = 0 | 1 |
3 | "Arch." | AWK default Number["Arch."] = 0 | 1 |
4 | "Discrete" | AWK default Number["Discrete"] = 0 | 1 |
3. 第二個 Pattern { Actions }指令中
* END 為awk的保留字,為 Pattern 的一種。
* END 成立(其值為true)的條件是:"awk處理完所有數據,即將離開程序時。"
平常讀入數據行時,END並不成立,故其后的Actions 並不被執行;唯有當awk讀完所有數據時,該Actions才會被執行(注意,不管有多少行數據,END僅在最后才成立,故該Actions僅被執行一次。)
BEGIN 與 END 有點類似,是awk中另一個保留的Pattern。唯一不同的是:
"以 BEGIN 為 Pattern 的 Actions 於程序一開始執行時,被執行一次。"
4. NF 為awk的內置變量,用以表示awk正處理的數據行中,所包含的字段個數。
5. awk程序中若含有以 $ 開頭的自定變量,都將以如下方式解釋:
以 i= 2 為例,$i = $2 表第二個字段數據。 (實際上,$ 在 awk 中為一運算符(Operator),用以取得字段數據。)
6 在AWK程序中使用Shell命令
awk程序中允許調用Shell指令,並提供管道解決awk與系統間數據傳遞的問題。所以awk很容易使用系統資源,讀者可利用這個特點來編寫某些適用的系統工具。
范例:寫一個awk程序來打印出線上人數。
將下列程序建文件,命名為 count.awk
BEGIN { while ( "who" | getline ) n++ print n }
並執行下列命令:
$ awk -f count.awk
執行結果將會打印出目前在線人數。
說 明:
1. awk 程序並不一定要處理數據文件,以本例而言,僅輸入程序文件count.awk,未輸入任何數據文件。
2. BEGIN 和 END 同為awk中的一種 Pattern。以 BEGIN 為 Pattern的Actions,只有在awk開始執行程序、尚未打開任何輸入文件前, 被執行一次。(注意:只被執行一次)
3. "|" 為 awk 中表示管道的符號。awk 把 管道 之前的字符串"who"當成Shell上的命令,並將該命令送往Shell執行,執行的結果(原先應打印在屏幕上的)則通過pipe送進awk程序中。
4. getline為awk所提供的輸入指令。
其語法如下:
語法 |
由何處讀取數據 |
數據讀入后置於 |
getline var < file |
所指定的 file |
變量 var(var省略時,表示置於$0) |
| getline var |
pipe |
變量 var(var省略時,表示置於$0) |
getline var |
見 注一 |
變量 var(var省略時,表示置於$0) |
注一:當 Pattern 為 BEGIN 或 END 時,getline 將由 stdin 讀取數據,否則由awk正處理的數據文件上讀取數據。
getline 一次讀取一行數據,若讀取成功則return 1;
若讀取失敗則return -1;
若遇到文件結束(EOF),則return 0。
本程序使用 getline 所 return 的數據來做為 while 判斷循環停止的條件,某些awk版本較舊,並不容許使用者改變 $0 的值。這種版的 awk 執行本程序時會產生 Error,讀者可於 getline 之后置上一個變量 (如此,getline 讀進來的數據便不會被置於 $0 ),或直接改用gawk便可解決。
7 AWK應用實例
本節將示范一個統計上班到達時間及遲到次數的程序。
這程序每日被執行時將讀入兩個數據文件:
* 員工當日到班時間的數據文件 ( 如下列的 arr.dat )
* 存放員工當月遲到累計次數的文件
當程序執行執完畢后將更新第二個數據文件的數據(遲到次數),並打印當日的報表。這程序將分成下列數小節逐步完成,其大綱如下:
7.1 在到班資料文件 arr.dat 之前增加一行抬頭"ID Number Arrvial Time",並產生報表輸出到文件today_rpt1 中。
<在awk中如何將數據輸出到文件>
7.2 將 today_rpt1 上的數據按員工代號排序,並加注執行當日日期;產生文件 today_rpt2
<awk中如何運用系統資源及awk中Pipe的特性>
7.3 將awk程序包含在一個shell script文件中
7.4 於 today_rpt2 每日報表上,遲到者之前加上"*",並加注當日平均到班時間;產生文件 today_rpt3
7.5 從文件中讀取當月遲到次數,並根據當日出勤狀況更新遲到累計數。
<使用者在awk中如何讀取文件數據>
某公司其員工到勤時間文件內容如下,取名為 arr.dat。文件中第一欄為員工代號,第二欄為到達時間。本范例中,將使用該文件為數據文件。
7.1 重定向輸出到文件
awk中並未提供如 C 語言中的fopen() 指令,也沒有fprintf() 文件輸出這樣的指令。但awk中任何輸出函數之后皆可借助使用與UNIX 中類似的 I/O 重定向符,將輸出的數據重定向到指定的文件;其符號仍為 > (輸出到一個新產生的文件) 或 >> ( 添加輸出的數據到文件末尾 )。
例:在到班數據文件 arr.dat 之前增加一行抬頭如下:"ID Number Arrival Time",並產生報表輸出到文件 today_rpt1中。
建立如下文件並取名為reformat1.awk
BEGIN { print " ID Number Arrival Time" > "today_rpt1" print "===========================" > "today_rpt1" } { printf(" %s %s\n", $1,$2 ) > "today_rpt1" }
執行:
$ awk -f reformat1.awk arr.dat
執行后將產生文件 today_rpt1,其內容如下:
說 明:
1. awk程序中,文件名稱 today_rpt1 的前后須以" (雙引號)括住,表示 today_rpt1 為一字符串常量。若未以"括住,則 today_rpt1 將被awk解釋為一個變量名稱。
在awk中任何變量使用之前,並不須事先聲明。其初始值為空字符串(Null string) 或 0。因此程序中若未以 " 將 today_rpt1 括住,則 today_rpt1 將是一變量,其值將是空字符串,這會在執行時造成錯誤(Unix 無法幫您開啟一個以空字符串為文件名的文件)。
因此在編輯awk程序時,須格外留心。因為若敲錯變量名稱,awk在編譯程序時會認為是一新的變量,並不會察覺。因此往往會造成運行時錯誤。
2. BEGIN 為awk的保留字,是 Pattern 的一種。
以 BEGIN 為 Pattern 的 Actions 於awk程序剛被執行尚未讀取數據文件時被執行一次,此后便不再被執行。
3. 讀者或許覺得本程序中的I/O重定向符號應使用 " >>" (append)而非 " >"。
本程序中若使用 ">" 將數據重定向到 today_rpt1,awk 第一次執行該指令時會產生一個新文件 today_rpt1,其后再執行該指令時則把數據追加到today_rpt1文件末,並非每執行一次就重開一個新文件。
若采用">>"其差異僅在第一次執行該指令時,若已存在today_rpt1則 awk 將直接把數據append在原文件的末尾。
這一點,與UNIX中的用法不同。
7.2 使用系統資源
awk程序中很容易使用系統資源。這包括在程序中途調用 Shell 命令來處理程序中的部分數據;或在調用 Shell 命令后將其產生的結果交回 awk 程序(不需將結果暫存於某個文件)。這一過程是借助 awk 所提供的管道 (雖然有些類似 Unix 中的管道,但特性有些不同),及一個從 awk 中調用 Unix 的 Shell 命令的語法來達成的。
例: 承上題,將數據按員工ID排序后再輸出到文件 today_rpt2,並於表頭附加執行時的日期。
分 析:
1. awk 提供與 UNIX 用法近似的 pipe,其記號亦為 "|"。其用法及含意如下:
awk程序中可接受下列兩種語法:
a.語法
awk output 指令 | "Shell 接受的命令"
(如: print $1,$2 | "sort -k 1")
b.語法
"Shell 接受的命令" | awk input 指令
(如: "ls " | getline)
注: awk input 指令只有 getline 一個。
awk output 指令有 print, printf() 兩個。
2. 在a 語法中,awk所輸出的數據將轉送往 Shell,由 Shell 的命令進行處理。以上例而言,print 所輸出的數據將經由 Shell 命令 "sort -k 1" 排序后再送往屏幕(stdout)。
上列awk程序中,"print$1, $2" 可能反復執行很多次,其輸出的結果將先暫存於 pipe 中,等到該程序結束時,才會一並進行 "sort -k 1"。
須注意兩點:不論 print $1, $2 被執行幾次,
"sort -k 1" 的執行時間是 "awk程序結束時",
"sort -k 1" 的執行次數是 "一次"。
3. 在 b 語法中,awk將先調用 Shell 命令。其執行結果將通過 pipe 送入awk程序,以上例而言,awk先讓 Shell 執行 "ls",Shell 執行后將結果存於 pipe,awk指令 getline 再從 pipe 中讀取數據。
使用本語法時應留心:
以上例而言,awk "立刻"調用 Shell 來執行 "ls",執行次數是一次。
getline 則可能執行多次(若pipe中存在多行數據)。
4. 除上列a、b二種語法外,awk程序中其它地方如出現像 "date", "cls", "ls"... 這樣的字符串,awk只把它當成一般字符串處理。
建立如下文件並取名為 reformat2.awk
# 程序 reformat2.awk # 這程序用以練習awk中的pipe BEGIN { "date" | getline #Shell 執行 "date",getline 取得結果並以$0記錄 print " Today is " , $2, $3 > "today_rpt2" print "=========================" > "today_rpt2" print " ID Number Arrival Time" > "today_rpt2" close( "today_rpt2" ) } { printf( "%s %s\n", $1 ,$2 ) | "sort -k 1 >> today_rpt2" }
執行如下命令:
$ awk -f reformat2.awk arr.dat
執行后,系統會自動將 sort 后的數據追加( Append; 因為使用 " >>") 到文件 today_rpt2末端。today_rpt2 內容如下:
說 明:
1. awk程序由三個主要部分構成:
i. Pattern { Action} 指令
ii. 函數主體。 例如: function double( x ){ return 2*x } (參考第11節 Recursive Program )
iii. Comment ( 以 # 開頭識別之 )
2. awk 的輸入指令 getline,每次讀取一行數據。若getline之后未接任何變量,則所讀入的內容將以$0 記錄;否則以所指定的變量儲存之。
以本例而言:
執行 "date" | getline 后,
$0 的值為 "Tue Nov 19 00:15:31 CST 2013" (筆者注:該時間為筆者本機上程序的執行時間)
當 $0 的值被更新時,awk將自動更新相關的內置變量,如: $1,$2,..,NF。故 $2 的值將為"Nov",$3的值將為"19"。
(有少數舊版的awk不允許即使用者自行更新(update)$0的值,或者更新$0時,它不會自動更新 $1,$2,..NF。這情況下,可改用gawk或nawk。否則使用者也可自行以awk字符串函數split()來分隔$0上的數據)
3. 本程序中 printf() 指令會被執行12次( 因為有arr.dat中有12行數據),但讀者不用擔心數據被重復sort了12次。當awk結束該程序時才會 close 這個 pipe,此時才將這12行數據一次送往系統,並調用 "sort -k 1 >> today_rpt2" 處理之。
4. awk提供另一個調用Shell命令的方法,即使用awk函數
system("shell命令")
例如:
awk ' BEGIN{ system("date > date.dat") getline < "date.dat" print "Today is ", $2, $3 } '
但使用 system( "shell 命令" ) 時,awk無法直接將執行中的部分數據輸出給Shell 命令,且 Shell 命令執行的結果也無法直接輸入到awk中。
7.3 執行AWK程序
本小節中描述如何將awk程序直接寫在 shell script 之中。此后使用者執行 awk 程序時,就不需要每次都鍵入 " awk -f program datafile"。script 中還可包含其它 Shell 命令,如此更可增加執行過程的自動化。
建立一個簡單的 awk程序 mydump.awk,如下:
{print}
這個程序執行時會把數據文件的內容 print 到屏幕上( 與cat功用類似 )。print 之后未接任何參數時,表示 "print $0"。
若欲執行該awk程序,來打印出文件 today_rpt1 及 today_rpt2 的內容時,必須於 UNIX 的命令行上執行下列命令:
方式一
awk -f mydump.awk today_rpt1 today_rpt2
方式二
awk '{print}' today_rpt1 today_rpt2
第二種方式系將awk 程序直接寫在 Shell 的命令行上,這種方式僅適合較短的awk程序。
方式三 建立如下的 shell script,並取名為 mydisplay,
awk ' # 注意以下的 awk 與 ' 之間須有空白隔開 {print} ' $* # 注意以上的 ' 與 $* 之間須有空白隔開
執行 mydisplay 之前,須先將它改成可執行的文件(此步驟往后不再贅述)。
請執行如下命令:
$ chmod +x mydisplay
往后使用者就可直接把 mydisplay 當成指令,來display任何文件。
例如:
$ ./mydisplay today_rpt1 today_rpt2
說 明:
1. 在script文件 mydisplay 中,指令"awk"與第一個 ' 之間須有空格(Shell中並無" awk' "指令)。
第一個 ' 用以通知 Shell 其后為awk程序。
第二個 ' 則表示 awk 程序結束。
故awk程序中一律以"括住字符串或字符,而不使用 ' ,以免Shell混淆。
2. $* 為 shell script中的用法,它可用來代表命令行上 "mydisplay之后的所有參數"。
例如執行:
$ mydisplay today_rpt1 today_rpt2
事實上 Shell 已先把該指令轉換成:
awk ' { print} ' today_rpt1 today_rpt2
本例中,$* 用以代表 "today_rpt1 today_rpt2"。在Shell的語法中,可用 $1 代表第一個參數,$2 代表第二個參數。當不確定命令行上的參數個數時,可使用 $* 表示。
3. awk命令行上可同時指定多個數據文件。
以 $ awk -f dump.awk today_rpt1 today_rpt2hf 為例,
awk會先處理today_rpt1,再處理 today_rpt2。此時若文件無法打開,將造成錯誤。
例如:不存在文件"file_no_exist",則執行:
$ awk -f dump.awk file_no_exit
將產生運行時錯誤(無法打開文件)。
但某些awk程序 "僅" 包含以 BEGIN 為Pattern的指令。執行這種awk程序時,awk並不須開啟任何數據文件。此時命令行上若指定一個不存在的數據文件,並不會產生 "無法打開文件"的錯誤。(事實上awk並未打開該文件)
例如執行:
$ awk 'BEGIN {print "Hello,World!!"} ' file_no_exist
該程序中僅包含以 BEGIN 為 Pattern 的 Pattern {actions},awk 執行時並不會打開任何數據文件;所以不會因不存在文件file_no_exit而產生 " 無法打開文件"的錯誤。
4. awk會將 Shell 命令行上awk程序(或 -f 程序文件名)之后的所有字符串,視為將輸入awk進行處理的數據文件文件名。若執行awk的命令行上 "未指定任何數據文件文件名",則將stdin視為輸入的數據來源,直到輸入end of file( Ctrl-D )為止。
讀者可以用下列程序自行測試, 執行如下命令:
$ awk -f mydump.awk #(未接任何數據文件文件名)
或
$ ./mydisplay #(未接任何數據文件文件名)
將會發現:此后鍵入的任何數據將逐行復印一份於屏幕上。這情況不是機器當機!是因為awk程序正處於執行中。它正按程序指示,將讀取數據並重新dump一次;只因執行時未指定數據文件文件名,故awk 便以stdin(鍵盤上的輸入)為數據來源。讀者可利用這個特點,設計可與awk即時聊天的程序。
7.4 改變字段的分隔符 & 用戶自定義函數
awk不僅能自動分割字段,也允許使用者改變其字段切割方式以適應各種格式的需要。使用者也可自定義函數,若有需要可將該函數單獨寫成一個文件,以供其它awk程序調用。
范例:承接 6.2 的例子,若八點為上班時間,請加注 "*"於遲到記錄之前,並計算平均上班時間。
分析:
1. 因八點整到達者不為遲到,故僅以到達的小時數做判斷是不夠的;仍應參考到達時的分鍾數。若 "將到達時間轉換成以分鍾為單位",不僅易於判斷是否遲到,同時也易於計算到達平均時間。
2. 到達時間($2)的格式為 dd:dd 或 d:dd;數字當中含有一個 ":"。但文本數字交雜的數據awk無法直接做數學運算。(注:awk中字符串"26"與數字26 並無差異,可直接做字符串或數學運算,這是awk重要特色之一。但awk對文本數字交雜的字符串無法正確進行數學運算)。
解決的方法:
方法一
對到達時間($2) d:dd 或 dd:dd 進行字符串運算,分別取出到達的小時數及分鍾數。
首先判斷到達小時數為一位或兩位字符,再調用函數分別截取分鍾數及小時數。此解法需使用下列awk字符串函數:
length( 字符串 ):返回該字符串的長度。
substr( 字符串,起始位置,長度):返回從起始位置起,指定長度的子字符串。若未指定長度,則返回從起始位置到字符串末尾的子字符串。
所以:
小時數 = substr( $2, 1, length($2) - 3 )
分鍾數 = substr( $2, length($2) - 2 )
方法二
改變輸入列字段的切割方式,使awk切割字段后分別將小時數及分鍾數隔開於二個不同的字段。
字段分隔字符 FS (field seperator) 是awk的內置變量,其默認值是空白及tab。awk每次切割字段時都會先參考FS 的內容。若把":"也當成分隔字符,則awk 便能自動把小時數及分鍾數分隔成不同的字段。
故令
FS = "[ \t:]+" (注:[ \t:]+ 為一Regular Expression )
1. Regular Expression 中使用中括號 [ ... ] 表示一個字符集合,用以表示任意一個位於中括號內的字符。故可用"[ \t:]"表示 一個 空白,tab 或 ":"
2. Regular Expression中使用 "+" 形容其前方的字符可出現一次或一次以上。
故 "[ \t:]+" 表示由一個或多個 "空白,tab 或 : " 所組成的字符串。
設定 FS = "[ \t:]+" 后,數據行如: "1034 7:26" 將被分割成3個字段
字段一 | 字段二 | 字段三 |
$1 | $2 | $3 |
1034 | 7 | 26 |
明顯地,awk程序中使用方法二比方法一更簡潔方便。本例子中采用方法二,也借此示范改變字段切割方式的用途。
編寫awk程序 reformat3,如下:
awk ' BEGIN { FS= "[ \t:]+" #改變字段切割的方式 "date" | getline #Shell 執行 "date". getline 取得結果以$0記錄 print " Today is " ,$2, $3 > "today_rpt3" print "=========================">"today_rpt3" print " ID Number Arrival Time" > "today_rpt3" close( "today_rpt3" ) } { #已更改字段切割方式, $2表到達小時數, $3表分鍾數 arrival = HM_to_M($2, $3) printf(" %s %s:%s %s\n", $1, $2, $3, arrival > 480 ? "*": " ")|"sort -k 1 >> today_rpt3" total += arrival } END{ close("today_rpt3") close("sort -k 1 >> today_rpt3") printf(" Average arrival time : %d:%d\n",total/NR/60, (total/NR)%60 ) >> "today_rpt3" } function HM_to_M( hour, min ){ return hour*60 + min } ' $*
並執行如下指令:
$ ./reformat3 arr.dat
執行后,文件 today_rpt3 的內容如下:
說 明:
1. awk 中也允許使用者自定義函數。函數定義方式請參考本程序,function 為 awk 的保留字。HM_to_M( ) 這函數負責將所傳入的小時及分鍾數轉換成以分鍾為單位。使用者自定函數時,還有許多細節須留心,如data scope,... ( 請參考 第十節 Recursive Program)
2. awk中亦提供與 C 語言中相同的 Conditional Operator。上式printf()中使用arrival >480 ? "*" : " " 即為一例。若 arrival 大於 480 則return "*" ,否則return " "。
3. % 為awk的運算符(operator),其作用與 C 語言中的 % 相同(取余數)。
4. NR(Number of Record) 為awk的內置變量。表示awk執行該程序后所讀入的記錄條數。
5. awk 中提供的 close( )指令,語法如下(有兩種):
① close( filename )
② close( 置於pipe之前的command )
為何本程序使用了兩個 close( ) 指令:
- 指令 close( "sort -k 1 >> today_rpt3" ),其意思為 close 程序中置於 "sort -k 1 >> today_rpt3 " 之前的 Pipe,並立刻調用 Shell 來執行"sort -k 1 >> today_rpt3"。(若未執行這指令,awk必須於結束該程序時才會進行上述動作;則這12個sort后的數據將被 append 到文件 today_rpt3 中"Average arrival time : ..." 的后方)
- 因為 Shell 排序后的數據也要寫到 today_rpt3,所以awk必須先關閉使用中的today_rpt3 以使 Shell 正確將排序后的數據追加到today_rpt3,否則2個不同的 process 同時打開一個文件進行輸出將會產生不可預期的結果。
讀者應留心上述兩點,才可正確控制數據輸出到文件中的順序。
6. 指令 close("sort -k 1 >> today_rpt3")中字符串 "sort -k 1 >> today_rpt3" 必須與 pipe | 后面的 Shell Command 名稱一字不差,否則awk將視為二個不同的 pipe。
讀者可於BEGIN{}中先令變量 Sys_call = "sort -k 1 >> today_rpt3",程序中再一律以 Sys_call 代替該字符串。
7.5 使用getline來讀取文件數據
范例:承上題,從文件中讀取當月遲到次數,並根據當日出勤狀況更新遲到累計數。(按不同的月份累計於不同的文件)
分析:
1. 程序中自動抓取系統日期的月份名稱,連接上"late.dat",形成累計遲到次數的文件名稱(如:Jullate.dat,...),並以變量late_file記錄該文件名。
2. 累計遲到次數的文件中的數據格式為:
員工代號(ID) 遲到次數
例如,執行本程序前文件 Novlate.dat 的內容為:
編寫程序 reformat4 如下:
awk ' BEGIN { Sys_Sort = "sort -k 1 >> today_rpt4" Result = "today_rpt4" # 改變字段切割的方式 # 令 Shell執行"date"; getline 讀取結果,並以$0記錄 FS = "[ \t:]+"
"date" | getline print " Today is " , $2, $3 > Result print "=========================" > Result print " ID Number Arrival Time" > Result close( Result ) # 從文件按中讀取遲到數據, 並用數組cnt[ ]記錄. 數組cnt[ ]中以 # 員工代號為下標, 所對應的值為該員工的遲到次數. late_file = $2"late.dat"
while( getline < late_file >0 )
cnt[$1] = $2 close( late_file ) } { # 已更改字段切割方式, $2表小時數,$3表分鍾數 arrival = HM_to_M($2, $3) if( arrival > 480 ){ mark = "*" # 若當天遲到,應再增加其遲到次數, 且令mark 為"*". cnt[$1]++
} else mark = " " # message 用以顯示該員工的遲到累計數, 若未曾遲到message為空字符串 message = cnt[$1] ? cnt[$1] " times" : "" printf("%s %2d:%2d %5s %s\n", $1, $2, $3, mark, message ) | Sys_Sort total += arrival } END { close( Result ) close( Sys_Sort ) printf(" Average arrival time : %d:%d\n", total/NR/60, (total/NR)%60 ) >> Result #將數組cnt[ ]中新的遲到數據寫回文件中 for( any in cnt ) print any, cnt[any] > late_file } function HM_to_M( hour, min ){ return hour*60 + min } ' $*
執行后,today_rpt4 的內容如下:
說 明:
1. late_file 是一變量,用以記錄遲到次數的文件的文件名。late_file的值由兩部分構成,前半部是當月月份名稱(由調用"date"取得),后半部固定為"late.dat",如: Junlate.dat。
2. 指令 getline < late_file 表示從late_file所代表的文件中讀取一條記錄,並存放於$0。若使用者可自行把數據放入$0,awk會自動對這新置入 $0 的數據進行字段分割。之后程序中可用$1, $2,..來表示該筆資料的字段一,字段二,...
(注:有少數awk版本不容許使用者自行將數據置於 $0,遇此情況可改用gawk或nawk)
執行getline指令時,若成功讀取記錄,它會返回1;若遇到文件結束,它返回0;無法打開文件則返回-1。
3. 利用 while( getline < filename >0 ) {....}可讀入文件中的每一筆數據並予處理。這是awk中用戶自行讀取數據文件的一個重要模式。
4. 數組 cnt[ ] 以員工ID 當下標(index),其對應值表示其遲到的次數。
5. 執行結束后,利用 for(Variable in array ){...}的語法 for( any in cnt ) print any, cnt[any] > late_file
將更新過的數據重新寫回到記錄遲到次數的文件。該語法在前面曾有說明。
8 處理多行數據
awk 每次從數據文件中只讀取一行數據進行處理。awk是依照其內置變量 RS(Record Separator) 的定義將文件中的數據分隔成一行一行的Record。RS 的默認值是 "\n"(換行符),故平常awk中一行數據就是一條 Record。
但有些文件中一條Record涵蓋了多行數據,這種情況下不能再以 "\n" 來分隔Records。最常使用的方法是相鄰的Records之間改以 一個空白行 來隔開。
在awk程序中,令 RS = ""(空字符串)后,awk把會空白行當成來文件中Record的分隔符。顯然awk對 RS = "" 另有解釋方式,簡略描述如下,
當 RS = "" 時:
1. 數個相鄰的空白行,awk僅視成一個單一的Record Saparator。(awk不會於兩個相鄰的空白行之間讀取一條空的Record)
2. awk會略過(skip)文件頭或文件尾的空白行。故不會因為這樣的空白行,造成awk多讀入了兩條空的記錄。
請觀察下例,首先建立一個數據文件 week.rpt 如下:
張長弓 GNUPLOT 入門 吳國強 Latex 簡介 VAST-2 使用手冊 mathematic 入門 李小華 awk Tutorial Guide Regular Expression
該文件的開頭有數行空白行,各條記錄之間使用一個或數個空白行隔開。讀者請細心觀察,當 RS = "" 時,awk讀取該數據文件的方式。
編輯一個awk程序文件 make_report 如下:
awk ' BEGIN { FS = "\n" RS = ""
split( "一. 二. 三. 四. 五. 六. 七. 八. 九.", C_Number, " " ) } { printf("\n%s 報告人 : %s \n",C_Number[NR],$1) for( i=2; i <= NF; i++)
printf(" %d. %s\n", i-1, $i) }
' $*
執行
$ ./make_report week.rpt
屏幕產生結果如下:
說 明:
1. 本程序同時也改變字段分隔字符( FS= "\n" ),如此一條記錄中的每一行都是一個字段。
例如: awk讀入的第一條記錄為
張長弓 GNUPLOT 入門
其中 $1 指的是"張長弓",$2 指的是"GNUPLOT 入門"
2. 上式中的C_Number[ ]是一個數組(array),用以記錄中文數字。
例如:C_Number[1] = "一.", C_Number[2] = "二."
這過程使用awk字符串函數 split( ) 來把中文數字放進數組 C_Number[ ]中。
函數 split( )用法如下:
split( 原字符串, 數組名, 分隔字符(field separator) ):
awk將依所指定的分隔字符(field separator)分隔原字符串成一個個的字段(field),並以指定的 數組 記錄各個被分隔的字段。
9 如何讀取命令行上的參數
大部分的應用程序都允許使用者在命令之后增加一些選擇性的參數。執行awk時這些參數大部分用於指定數據文件文件名,有時希望在程序中能從命令行上得到一些其它用途的數據。本小節中將敘述如何在awk程序中取用這些參數。
建立文件如下,命名為 see_arg:
awk ' BEGIN { for( i=0; i<ARGC ; i++) print ARGV[i] # 依次印出awk所記錄的參數 } ' $*
執行如下命令:
$ ./see_arg first-arg second-arg
結果屏幕出現:
說明:
1. ARGC,ARGV[ ] 為awk所提供的內置變量。
- ARGC:為一整數。代表命令行上,除了選項-v,-f 及其對應的參數之外所有參數的數目。
- ARGV[ ]:為一字符串數組。ARGV[0],ARGV[1],...,ARGV[ARGC-1] 分別代表命令行上相對應的參數。
例如,當命令行為:
$ awk -vx=36 -f program1 data1 data2
或
$ awk '{ print $1 ,$2 }' data1 data2
其 ARGC 的值為 3
ARGV[0] 的值為 "awk"
ARGV[1] 的值為 "data1"
ARGV[2] 的值為 "data2"
命令行上的 "-f program1"," -vx=36",或程序部分 '{ print $1, $2}' 都不會列入 ARGC 及 ARGV[ ] 中。
2. awk 利用 ARGC 來判斷應打開的數據文件個數。
但使用者可強行改變 ARGC;當 ARGC 的值被使用者設為 1 時,awk將被蒙騙,誤以為命令行上並無數據文件文件名,故不會以 ARGV[1],ARGV[2],...為文件名來打開文件讀取數據;但在程序中仍可通過 ARGV[1],ARGV[2],...來取得命令行上的數據。
某一程序 test1.awk 如下:
BEGIN{ number = ARGC #先用number 記住實際的參數個數. ARGC = 2 # 自行更改 ARGC=2, awk將以為只有一個資料文件 # 仍可藉由ARGV[ ]取得命令行上的資料. for( i=2; i<number; i++)
data[i] = ARGV[i] } ........
於命令行上鍵入
$ awk -f test1.awk data_file apple orange
執行時 awk 會打開數據文件 data_file 以進行處理,但不會打開以appleo、range 為文件名的文件(因為 ARGC 被改成2)。但仍可通過ARGV[2]、ARGV[3]取得命令行上的參數 apple、orange。
3. 也可以用下列命令來達成上例的效果。
$ awk -f test2.awk -v data[2]="apple" -v data[3]="orange" data_file
10 編寫可與用戶交互的AWK程序
執行awk程序時,awk會自動從文件中讀取數據來進行處理,直到文件結束。只要將awk讀取數據的來源改成鍵盤輸入,便可設計與awk 交互的程序。本節將提供一個該類程序的范例。
范例:本節將編寫一個英語生字測驗的程序,它將印出中文字意,再由使用者回答其英語生字。
首先編輯一個數據文件 test.dat (內容不限,格式如下)
apple 蘋果
orange 柳橙
banana 香蕉
pear 梨子
starfruit 楊桃
bellfruit 蓮霧
kiwi 奇異果
pineapple 菠蘿
watermelon 西瓜
編輯awk程序"c2e"如下:
awk ' BEGIN { while( getline < ARGV[1] ){ #由指定的文件中讀取測驗數據 English[++n] = $1 # 最后, n 將表示題目的題數 Chinese[n] = $2 } ARGV[1] = "-" # "-"表示由stdin(鍵盤輸入) srand() # 以系統時間為隨機數啟始的種子 question() #產生考題 } {# awk自動讀入由鍵盤上輸入的數據(使用者回答的答案) if( $1 != English[ind] ) print "Try again!"
else{ print "\nYou are right !! Press Enter to Continue --- " getline question() #產生考題 } } function question(){ ind = int(rand()* n) + 1 #以隨機數選取考題 system("clear") print " Press \"ctrl-d\" to exit" printf("\n%s ", Chinese[ind] " 的英文生字是: ") } ' $*
執行時輸入如下指令:
$./c2e test.dat
屏幕將產生如下的畫面:
若輸入 starfruit
程序將產生
說明:
1. 參數 test.dat (ARGV[1]) 表示儲存考題的數據文件文件名。awk 由該文件上取得考題資料后,將 ARGV[1] 改成 "-"。
"-" 表示由 stdin(鍵盤輸入) 數據。鍵盤輸入數據的結束符號 (End of file)是 ctrl-d。當 awk 讀到 ctrl-d 時就停止由 stdin 讀取數據。
2. awk的數學函數中提供兩個與隨機數有關的函數。
rand( ): 返回介於 0與1之間的(近似)隨機數值。 0 < rand() < 1.
除非使用者自行制定rand()函數起始的seed,否則每次執行awk程序時,rand()都將以同一個內定的seed為起始。
srand(x):制定以x作為rand()函數起始的種子。若省略了x,則awk會以執行時的日期與時間為rand()函數起始的seed。(參考 附錄 C AWK的Built-in Functions)
11 遞歸程序
awk 中除了函數的參數列表(Argument List)上的參數(Arguments)外,所有變量不管於何處出現,全被視為全局變量。其生命持續至程序結束——該變量不論在function外或 function內皆可使用,只要變量名稱相同所使用的就是同一個變量,直到程序結束。因遞歸函數內部的變量,會因它調用子函數(本身)而重復使用,故編寫該類函數時應特別留心。
例如:執行
awk ' BEGIN { x = 35 y = 45 test_variable( x ) printf("Return to main : arg1= %d, x= %d, y= %d, z= %d\n", arg1, x, y, z) } function test_variable( arg1 ) { arg1++ # arg1 為參數列上的參數, 是local variable. 離開此函數后將消失. y++ # 會改變主式中的變量 y z = 55 # z 為該函數中新使用的變量, 主程序中變量 z 仍可被使用. printf("Inside the function: arg1=%d, x=%d, y=%d, z=%d\n", arg1, x, y, z) } '
結果屏幕打印出
由上可知:
- 函數內可任意使用主程序中的任何變量。
- 函數內所啟用的任何變量(除參數外),於該函數之外依然可以使用。
此特性優劣參半,最大的壞處是程序中的變量不易被保護,特別是遞歸調用本身,執行子函數時會破壞父函數內的變量。
一個變通的方法是:在函數的參數列中虛列一些參數。函數執行中使用這些虛列的參數來記錄不想被破壞的數據,如此執行子函數時就不會破壞到這些數據。此外awk 並不會檢查調用函數時所傳遞的參數個數是否一致。
例如:定義遞歸函數如下:
function demo( arg1 ) { # 最常見的錯誤例子 ........ for(i=1; i< 20 ; i++){ demo(x) # 又調用本身. 因為 i 是 global variable, 故執行完該子函數后 # 原函數中的 i 已經被壞, 故本函數無法正確執行. ....... } .......... }
可將上列函數中的 i 虛列在該函數的參數列上,如此 i 便是一個局部變量,不會因執行子函數而被破壞。
將上列函數修改如下:
function demo( arg1, i ) { ...... for(i=1; i< 20; i++) { demo(x) #awk不會檢查呼叫函數時, 所傳遞的參數個數是否一致 ..... } }
$0, $1,.., NF, NR,..也都是 global variable,讀者於遞歸函數中若有使用這些內置變量,也應另外設立一些局部變量來保存,以免被破壞。
范例:以下是一個常見的遞歸調用范例。它要求使用者輸入一串元素(各元素間用空白隔開) 然后打印出這些元素所有可能的排列。
編輯如下的awk程序,取名為 permu
awk ' BEGIN { print "請輸入排列的元素,各元素間請用空白隔開" getline permutation($0, "") printf("\n共 %d 種排列方式\n", counter) } function permutation( main_lst, buffer, new_main_lst, nf, i, j ) { $0 = main_lst # 把main_lst指定給$0之后awk將自動進行字段分割. nf = NF # 故可用 NF 表示 main_lst 上存在的元素個數. # BASE CASE : 當main_lst只有一個元素時. if( nf == 1){ print buffer main_lst #buffer的內容再加上main_lst就是完成一次排列的結果 counter++ return } # General Case : 每次從 main_lst 中取出一個元素放到buffer中 # 再用 main_lst 中剩下的元素 (new_main_lst) 往下進行排列 else for( i=1; i<=nf ;i++) { $0 = main_lst # $0為全局變量已被破壞, 故重新把main_lst賦給$0,令awk再做一次字段分割 new_main_lst = ""
for(j=1; j<=nf; j++) # 連接 new_main_lst if( j != i )
new_main_lst = new_main_lst " " $j permutation( new_main_lst, buffer " " $i ) } } ' $*
執行
$ ./permu
屏幕上出現提示信息,若輸入 1 2 3 回車,結果打印出:
說明:
1. 有些較舊版的awk,並不容許使用者指定$0的值。此時可改用gawk 或 nawk。否則也可自行使用 split() 函數來分割 main_lst。
2. 為避免執行子函數時破壞 new_main_lst, nf, i, j 故把這些變量也列於參數列上。如此,new_main_lst, nf, i, j 將被當成局部變量,而不會受到子函數中同名的變量影響。讀者聲明函數時,參數列上不妨將這些 "虛列的參數" 與真正用於傳遞信息的參數間以較長的空白隔開,以便於區別。
3. awk 中欲將字符串concatenation(連接)時,直接將兩字符串並置即可(Implicit Operator)。
例如:
awk ' BEGIN{ A = "This " B = "is a " C = A B "key." # 變量A與B之間應留空白,否則"AB"將代表另一新變量. print C } '
結果將印出
4. awk使用者所編寫的函數可再重用,並不需要每個awk式中都重新編寫。
將函數部分單獨編寫於一文件中,當需要用到該函數時再以下列方式include進來。
$ awk -f 函數文件名 -f awk主程序文件名 數據文件文件名
附錄 A ── Patterns
awk 通過判斷 Pattern 的值來決定是否執行其后所對應的Actions。這里列出幾種常見的Pattern:
A.1 BEGIN
BEGIN 為 awk 的保留字,是一種特殊的 Pattern。
BEGIN 成立(其值為true)的時機是:
"awk 程序一開始執行,尚未讀取任何數據之前。"
所以在 BEGIN { Actions } 語法中,其 Actions 部份僅於程序一開始執行時被執行一次。當 awk 從數據文件讀入數據行后, BEGIN 便不再成立,故不論有多少數據行,該 Actions 部份僅被執行一次。
一般常把 "與數據文件內容無關" 與 "只需執行一次" 的部分置於該Actions(以 BEGIN 為 Pattern)中。
例如:
BEGIN { FS = "[ \t:]" # 於程序一開始時, 改變awk切割字段的方式 RS = "" # 於程序一開始時, 改變awk分隔數據行的方式 count = 100 # 設定變量 count 的起始值 print " This is a title line " # 印出一行 title } .......
# 其它 Pattern { Actions }
.....
有些awk程序甚至"不需要讀入任何數據行"。遇到這情況可把整個程序置於以 BEGIN 為 Pattern的 Actions 中。
例如:
BEGIN { print " Hello ! the Word ! " }
注意:執行該類僅含 BEGIN { Actions } 的程序時,awk 並不會開啟任何數據文件進行處理。
A.2 END
END 為 awk 的保留字,是另一種特殊的 Pattern。
END 成立(其值為true)的時機與 BEGIN 恰好相反,為:
"awk 處理完所有數據,即將離開程序時"
平常讀入數據行時,END並不成立,故其對應的 Actions 並不被執行;唯有當awk讀完所有數據時,該 Actions 才會被執行。
注意:不管數據有多少行,該 Actions 僅被執行一次。
A.3 關系表達式
使用像 " A 關系運算符 B" 的表達式當成 Pattern。
當 A 與 B 存在所指定的關系(Relation)時,該 Pattern 就算成立(true)。
例如:
length($0) <= 80 { print $0 }
上式中 length($0) <= 80 是一個 Pattern,當 $0(數據行)的長度小於等於80時該 Pattern 的值為true,將執行其后的 Action (打印該行數據)。
awk 中提供下列 關系運算符(Relation Operator)
運算符 | 含意 |
> | 大於 |
< | 小於 |
>= | 大於或等於 |
<= | 小於或等於 |
== | 等於 |
!= | 不等於 |
~ | match |
!~ | not match |
上列關系運算符除~(match)與!~(not match)外,與 C 語言中的含意一致。
~(match) 與!~(match) 在 awk 的含意簡述如下:
若 A 為一字符串,B 為一正則表達式:
- A ~ B 判斷 字符串A 中是否 包含 能匹配(match)B式樣的子字符串。
- A !~ B 判斷 字符串A 中是否 未包含 能匹配(match)B式樣的子字符串。
例如:
$0 ~ /program[0-9]+\.c/ { print $0 }
$0 ~ /program[0-9]+\.c/ 整個是一個 Pattern,用來判斷$0(數據行)中是否含有可 match /program[0-9]+\.c/ 的子字符串,若$0 中含有該類字符串,則執行 print (打印該行數據)。
Pattern 中被用來比對的字符串為$0 時(如本例),可僅以正則表達式部分表示整個Pattern。故本例的 Pattern 部分$0 ~/program[0-9]+\.c/ 可僅用/program[0-9]+\.c/表之(有關匹配及正則表達式請參考 附錄 E )
A.4 正則表達式
直接使用正則表達式當成 Pattern,此為 $0 ~ 正則表達式 的簡寫。
該 Pattern 用以判斷 $0(數據行) 中是否含有匹配該正則表達式的子字符串,若含有,該式成立(true),則執行其對應的 Actions。
例如:
/^[0-9]*$/ { print "This line is an integer !" }
與
$0 ~ /^[0-9]*$/ { print "This line is an integer !" }
相同。
A.5 混合Pattern
之前所介紹的各種 Patterns,其計算后結果為一邏輯值(True or False)。awk 中邏輯值彼此間可通過&&(and)、||(or)、!(not) 結合成一個新的邏輯值。故不同 Patterns 彼此可通過上述結合符號來結合成一個新的 Pattern。如此可進行復雜的條件判斷。
例如:
FNR >= 23 && FNR <= 28 { print " " $0 }
上式利用&& (and) 將兩個 Pattern 求值的結果合並成一個邏輯值。該式將數據文件中 第23行 到 28行 向右移5格(先輸出5個空白字符)后輸出。( FNR 為awk的內置變量, 請參考 附錄 D )
A.6 Pattern1, Pattern2
遇到這種 Pattern(筆者注:逗號表達式),awk 會幫您設立一個 switch(或flag)。
- 當awk讀入的數據行使得 Pattern1 成立時,awk 會打開(turn on)這個 switch
- 當awk讀入的數據行使得 Pattern2 成立時,awk 會關上(turn off)這個 switch
該 Pattern 成立的條件是:
當這個 switch 被打開(turn on)時 (包括 Pattern1 或 Pattern2 成立的情況)
例 如:
FNR >= 23 && FNR <= 28 { print " " $0 }
可改寫為
FNR == 23 , FNR == 28 { print " " $0 }
說 明:
當 FNR >= 23 時,awk 就 turn on 這個 switch;因為隨着數據行的讀入,awk不停的累加 FNR。當 FNR = 28 時,Pattern2 (FNR == 28) 便成立,這時 awk 會關上這個 switch。
當 switch 打開的期間,awk 會執行 print " " $0
( FNR 為awk的內置變量, 請參考 附錄 D )
附錄 B ── Actions
Actions 是由下列指令(statement)所組成:
1 表達式 ( 函數調用,賦值...) 2 print 表達式列表 3 printf( 格式化字符串, 表達式列表) 4 if( 表達式 ) 語句 [else 語句] 5 while( 表達式 ) 語句 6 do 語句 while( 表達式) 7 for( 表達式; 表達式; 表達式) 語句 8 for( variable in array) 語句 9 delete 10 break 11 continue 12 next 13 exit [表達式] 14 語句
awk 中大部分指令與 C 語言中的用法一致,此處僅介紹較為常用或容易混淆的指令的用法。
B.1 程序控制流
- if 指令
語法:
if(表達式) 語句1 [else 語句2 ]
范例:
if( $1 > 25 ) print "The 1st field is larger than 25"
else
print "The 1st field is not larger than 25"
(a)與 C 語言中相同,若 表達式 計算(evaluate)后的值不為 0 或 空字符串,則執行 語句1;否則執行 語句2。
(b)進行邏輯判斷的表達式所返回的值有兩種,若最后的邏輯值為true,則返回1;否則返回0。
(c)語法中else 語句2 以[ ] 前后括住表示該部分可視需要而予加入或省略。
- while 指令
語法:
while( 表達式 ) 語句
范例:
while( match(buffer,/[0-9]+\.c/ ) )
{ print "Find :" substr( buffer,RSTART, RLENGTH) buff = substr( buffer, RSTART + RLENGTH) }
上列范例找出 buffer 中所有能匹配 /[0-9]+.c/(數字之后接上 ".c"的所有子字符串)。范例中 while 以函數 match( )所返回的值做為判斷條件。若buffer 中還含有匹配指定條件的子字符串(match成功),則 match()函數返回1,while 將持續進行其后的語句。
- do-while 指令
語法:
do 語句 while(表達式)
范例:
do{ print "Enter y or n ! " getline data } while( data !~ /^[YyNn]$/)
(a)上例要求用戶從鍵盤上輸入一個字符,若該字符不是Y, y, N, 或 n則會不停執行該循環,直到讀取正確字符為止。
(b)do-while 指令與 while 指令 最大的差異是:do-while 指令會先執行 語句 而后再判斷是否應繼續執行。所以,無論如何其 語句 部分至少會執行一次。
- for 語句指令(一)
語法:
for(variable in array ) 語句
范例:執行下列命令
awk ' BEGIN{ X[1]= 50; X[2]= 60; X["last"]= 70
for( any in X ) printf("X[%s] = %d\n", any, X[any] ) }'
結果輸出:
(a)這個 for 指令,專用以查找數組中所有的下標值,並依次使用所指定的變量予以記錄。以本例而言,變量 any 將逐次代表 "last"、1及2。
(b)以這個 for 指令,所查找出的下標的值彼此間並無任何次序關系。
(c)第5節中有該指令的使用范例及解說。
- for 語句指令(二)
語法:
for(表達式1; 表達式2; 表達式3) 語句
范例:
for(i=1; i< =10; i++)
sum = sum + i
說明:
(a)上列范例用以計算 1 加到 10 的總和。
(b)表達式1 常用於設定該 for 循環的起始條件,如上例中的 i=1
表達式2 常用於設定該循環的停止條件,如上例中的 i <= 10
表達式3 常用於改變 counter 的值,如上例中的 i++
- break 指令
break 指令用以強迫中斷(跳出) for, while, do-while 等循環。
范例:
while( getline < "datafile" > 0 ) { if( $1 == 0 ) break else print $2 / $1 }
上例中,awk 不斷地從文件 datafile 中讀取資料,當$1等於0時就停止該循環。
- continue 指令
循環中的 語句 進行到一半時,執行 continue 指令來略過循環中尚未執行的 語句。
范例:
for( index in X_array ) { if( index !~ /[0-9]+/ )
continue print "There is a digital index", index }
上例中若 index 不為數字則執行 continue,故將略過(不執行)其后的指令。
需留心 continue 與 break 的差異:執行 continue 只是跳過其后未執行的statement,但並未跳出該循環。
- next 指令
執行 next 指令時,awk 將跳過位於該指令(next)之后的所有指令(包括其后的所有Pattern { Actions }),接著讀取下一行數據,繼續從第一個 Pattern {Actions} 執行起。
范例:
/^[ \t]*$/ { print "This is a blank line! Do nothing here !" next } $2 != 0 { print $1, $1/$2 }
上例中,當 awk 讀入的數據行為空白行時( match /^[ \]*$/ ),除打印消息外,只執行 next,故 awk 將跳過其后的指令,繼續讀取下一行數據,從頭(第一個 Pattern { Actions })執行起。
- exit 指令
執行 exit 指令時,awk將立刻跳出(停止執行)該awk程序。
B.2 AWK中的I/O指令
- printf 指令
該指令與 C 語言中的用法相同,可通過該指令控制數據輸出時的格式。
語法:
printf("format", item1, item2,.. )
范例:
id = "BE-2647"; ave = 89 printf("ID# : %s Ave Score : %d\n", id, ave)
(a)結果印出:
(b)format 部分是由 一般的字串(String Constant) 及 格式控制字符(Formatcontrol letter, 其前會加上一個%字符)所構成。以上式為例,"ID# : " 及 " Ave Score : " 為一般字串,%s 及 %d 為格式控制字符。
(c)打印時,一般字串將被原封不動地打印出來。遇到格式控制字符時,則依序把 format后方的 item 轉換成所指定的格式后進行打印。
(d)有關的細節,讀者可從介紹 C 語言的書籍上得到較完整的介紹。
(e)print 及 printf 兩個指令,其后可使用 > 或 >> 將輸出到stdout 的數據重定向到其它文件,7.1 節中有完整的范例說明。
- print 指令
范例:
id = "BE-267"; ave = 89 print "ID# :", id, "Ave Score :"ave
(a)結果印出:
(b)print 之后可接上字串常數(Constant String)或變量。它們彼此間可用"," 隔開。
(c)上式中,字串 "ID# :" 與變量 id 之間使用","隔開,打印時兩者之間會以自動 OFS(請參考 附錄D 內建變量 OFS) 隔開。OFS 的值一般內定為 "一個空格"
(d)上式中,字串 "Ave Score :" 與變量ave之間並未以","隔開,awk會將這兩者先當成字串concate在一起(變成"Ave Score :89")后,再予打印
- getline 指令
語法:
語法 |
由何處讀取數據 |
數據讀入后置於 |
getline var < file |
所指定的 file |
變量 var(var省略時表示置於$0) |
| getline var |
pipe 變量 |
變量 var(var省略時表示置於$0) |
getline var |
見 注一 |
變量 var(var省略時表示置於$0) |
注一:當Pattern為BEGIN或END時,getline將由stdin讀取數據,否則由awk正處理的文件上讀取數據。
getline 一次讀取一行數據,若讀取成功則return 1;若讀取失敗則return -1;若遇到文件結束(EOF)則return 0。
- close 指令
該指令用以關閉一個打開的 文件 或 pipe (見下例)
范例:
awk '
BEGIN { print "ID # Salary" > "data.rpt" }
{ print $1 , $2 * $3 | "sort -k 1 > data.rpt" }
END { close( "data.rpt" ) close( "sort -k 1 > data.rpt" ) print " There are", NR, "records processed." } '
說明:
(a)上例中, 一開始執行 print "ID # Salary" > "data.rpt" 指令來輸出一行抬頭。它使用 I/O Redirection ( > )將數據轉輸出到data.rpt,此時文件 data.rpt 是處於 Open 狀態。
(b)指令 print $1, $2 * $3 不停的將輸出的數據送往 pipe(|),awk在程序將結束時才會調用 shell 使用指令 "sort -k 1 > data.rpt" 來處理 pipe 中的數據;並未立即執行,這點與 Unix 中pipe的用法不盡相同。
(c)最后希望在文件 data.rpt 的末尾處加上一行 "There are....."。但此時,Shell尚未執行 "sort -k 1 > data.rpt",故各行數據排序后的 ID 及 Salary 等數據尚未寫入data.rpt。所以得命令 awk 提前先通知 Shell 執行命令 "sort -k 1 > data.rpt" 來處理 pipe 中的數據。awk中這個動作稱為 close pipe,通過執行 close ( "shell command" )來完成。需留心 close( )指令中的 shell command 需與"|"后方的 shell command 完全相同(一字不差),較佳的方法是先為該字串定義一個簡短的變量,程序中再以此變量代替該shell command。
(d)為什么執行 close("data.rpt")?因為 sort 完后的資料也將寫到data.rpt,而該文件正為awk所打開使用(write)中,故awk程序中應先關閉data.rpt,以免造成因兩個 進程 同時打開一個文件進行輸出(write)所產生的錯誤。
- system 指令
該指令用以執行 Shell上的 command。
范例:
DataFile = "invent.rpt" system( "rm " DataFile )
說明:
(a)system("字符串")指令接受一個字符串當成Shell的命令。上例中,使用一個字串常數"rm " 連接(concate)一個變量 DataFile 形成要求 Shell 執行的命令。Shell 實際執行的命令為 "rm invent.rpt"。
- "|" pipe指令
"|" 配合 awk 輸出指令,可把 output 到 stdout 的數據繼續轉送給Shell 上的某一命令當成input的數據。"|" 配合 awk getline 指令, 可調用 Shell 執行某一命令,再以 awk 的 getline 指令將該命令的所產生的數據讀進 awk 程序中。
范例:
{ print $1, $2 * $3 | "sort -k 1 > result" } "date" | getline Date_data
讀者請參考7.2 節,其中有完整的范例說明。
B.3 awk釋放所占內存的指令
awk 程序中常使用數組(Array)來保存大量數據,delete 指令便是用來釋放數組中的元素所占用的內存空間。
范例:
for( any in X_arr ) delete X_arr[any]
讀者請留心,delete 指令一次只能釋放數組中的一個元素。
B.4 awk 中的數學運算符(Arithmetic Operators)
+(加)、 -(減)、 *(乘)、 /(除)、 %(求余數)、 ^(指數) 與 C 語言中用法相同。
B.5 awk 中的賦值運算符(Assignment Operators)
=、 +=、 -=、 *=、 /=、 %=、 ^=
x += 5 的意思為 x = x + 5,其余類推。
B.6 awk 中的條件運算符(Conditional Operator)
語法:
判斷條件 ? value1 : value2
若 判斷條件 成立(true) 則返回 value1,否則返回 value2。
B.7 awk 中的邏輯運算符(Logical Operators)
&&( and )、 ||(or)、 !(not)
Extended Regular Expression 中使用 "|" 表示 or 請勿混淆。
B.8 awk 中的關系運算符(Relational Operators)
>、 >=、 <、 <=、 ==、 !=、 ~、 !~
B.9 awk 中其它的運算符
+(正號)、 -(負號)、 ++(Increment Operator)、 - -(Decrement Operator)
B.10 awk 中各運算符的運算級
按優先級從高到低排列:
$ | 字段運算元,例如: i=3; $i表示第3個字段 |
^ | 指數運算 |
+, -, ! | 正、負號,及邏輯上的 非 |
* ,/ ,% | 乘,除,余數 |
+ ,- | 加,減 |
>, > =,< , < =, ==, != | 關系運算符 |
~, !~ | match, not match |
&& | 邏輯上的 and |
|| | 邏輯上的 or |
? : | 條件運算符 |
= , +=, -=,*=, /=, %=, ^= | 賦值運算符 |
附錄C ── awk 的內建函數(Built-in Functions)
C.1 字串函數
- index( 原字串, 查找的子字串 )
若原字串中含有欲尋找的子字串,則返回該子字串在原字串中第一次出現的位置,若未曾出現該子字串則返回0。
例如:
$ awk 'BEGIN{ print index("8-12-94","-") }'
結果打印 2
- length( 字串 ):返回該字串的長度
例如:
$ awk 'BEGIN { print length("John") }'
結果打印 4
- match( 原字串, 用以查找比對的正則表達式 )
awk會在原字串中查找合乎正則表達式的子字串,若合乎條件的子字串有多個,則以原字串中最左方的子字串為准。awk找到該字串后會依此字串為依據進行下列動作:
1. 設定awk內建變量 RSTART、RLENGTH:
RSTART = 合條件的子字串在原字串中的位置。
= 0 ;若未找到合條件的子字串。
RLENGTH = 合條件的子字串長度。
= -1 ;若未找到合條件的子字串。
2. 返回 RSTART 的值.
例如:
awk ' BEGIN { match( "banana", /(an)+/ ) print RSTART, RLENGTH } '
結果打印 2 4
- split( 原字串, 數組名稱, 分隔字符 ):
awk將依所指定的分隔字符(field separator)來分隔原字串成一個個的字段(field),並以指定的數組記錄各個被分隔的字段。
例如:
ArgLst = "5P12p89"
split( ArgLst, Arr, /[Pp]/)
執行后: Arr[1]=5, Arr[2]=12, Arr[3]=89
- sprintf(格式字符串, 項1, 項2, ...)
該函數的用法與 awk 或 C 的輸出函數printf()相同。所不同的是sprintf()會將要求印出的結果當成一個字串返回。一般最常使用sprintf()來改變數據格式。如:x 為一數值數據,若欲將其變成一個含二位小數的數據,可執行如下指令:
x = 28 x = sprintf("%.2f",x)
執行后: x = "28.00"
- sub( 用於比對的正則表達式, 新字串, 原字串 )
sub( )將原字串中第一個(最左邊)合乎所指定的正則表達式的子字串改以新字串取代。
1. 第二個參數"新字串"中可用"&"來代表"合乎條件的子字串"。承上例,執行下列指令:
A = "a6b12anan212.45an6a" sub( /(an)+[0-9]*/, "[&]", A) print A
結果打印 ab12[anan212].45an6a
2. sub()不僅可執行替換(replacement)的功用,當第二個參數為空字串("")時,sub()所執行的是"去除指定字串"的功用。
3. 通過 sub() 與 match() 的搭配使用,可逐次取出原字串中合乎指定條件的所有子字串。
例如執行下列程序:
awk ' BEGIN { data = "p12-P34 P56-p61"
while( match( data ,/[0-9]+/) > 0) { print substr(data, RSTART, RLENGTH ) sub(/[0-9]+/,"",data) } }'
結果打印:
4. sub( )中第三個參數(原字串)若未指定,則其缺省值為$0。
可用 sub( /[9-0]+/,"digital" ) 表示 sub(/[0-9]+/,"digital",$0 )
- gsub( 用於比對的正則表達式, 將替換的新字串, 原字串 )
這個函數與 sub()一樣,同樣是進行字串取代的函數。唯一不同點是
1. gsub()會取代所有合條件的子字串。
2. gsub()會返回被取代的子字串個數。
請參考 sub()。
- substr( 字串, 起始位置 [,長度] )
返回從起始位置起,指定長度的子字串。若未指定長度,則返回起始位置到字串末尾的子字串。
例如:
$ awk 'BEGIN { print substr("User:Wei-Lin Liu", 6)}'
結果打印 Wei-Lin Liu
C.2 數學函數
- int(x):返回x的整數部分(去掉小數)
例如:
int(7.8) 將返回 7
int(-7.8) 將返回 -7
- sqrt(x):返回x的平方根
例如:
sqrt(9) 將返回 3
若 x 為負數,則執行 sqrt(x) 時將造成 Run Time Error (筆者注:本機上提示的是"-nan",如下圖)
- exp(x):將返回e的x次方
例如:
exp(1) 將返回 2.71828
- log(x):將返回x以e為底的對數值
例如:
log(exp(1)) 將返回 1 (筆者注:本機上log(e)打印出來是-inf,所以用exp(1)代替e)
若 x< 0,則執行 sqrt(x)時將造成 Run Time Error(筆者注:本機上提示的是"nan",同上)
- sin(x):x 須以弧度為單位,sin(x)將返回x的sin函數值
- cos(x):x 須以弧度為單位,cos(x)將返回x的cos函數值
- atan2(y,x):返回 y/x 的tan反函數的值,返回值以弧度為單位
- rand():返回介於 0與1之間的(近似)隨機數值;0 < rand()<1
除非使用者自行指定rand()函數起始的種子,否則每次執行awk程式時,rand()函數都將使用同一個缺省的種子來產生隨機數。
- srand(x):指定以x為rand( )函數起始的種子
若省略了x,則awk會以執行時的日期與時間為rand()函數起始的種子。
附錄D ── awk 的內置變量 Built-in Variables
因內置變量的個數不多,此處按其相關性分類說明,並未按其字母順序排列。
- ARGC
ARGC表示命令行上除了選項 -F, -v, -f 及其所對應的參數之外的所有參數的個數。若將"awk程序"直接寫在命令列上,則 ARGC 亦不將該"程序部分"列入計算。
- ARGV
ARGV數組用以記錄命令列上的參數。
例:執行下列命令
$ awk -F\t -v a=8 -f prg.awk file1.dat file2.dat
或
$ awk -F\t -v a=8 '{ print $1 * a }' file1.dat file2.dat
執行上述任一程序后
ARGC = 3
ARGV[0] = "awk"
ARGV[1] = "file1.dat"
ARGV[2] = "file2.dat"
讀者請留心:當 ARGC = 3 時,命令行上僅指定了 2 個文件。
注:
-F\t 表示以 tab 為字段分隔字符 FS(field seporator)。
-v a=8 用以初始化程序中的變量 a。
- FILENAME
FILENAME用以表示目前正在處理的文件的文件名。
- FS
字段分隔字符。
- $0
表示目前awk所讀入的數據行。
- $1,$2..
分別表示所讀入的數據行的第一個字段,第二個字段,...(參考下列說明)
當awk讀入一行數據 "A123 8:15" 時,會先以$0 記錄,故 $0 = "A123 8:15"。若程序中進一步使用了 $1, $2.. 或 NF 等內置變量時,awk才會自動分割 $0以便取得字段相關的數據,切割后各個字段的數據會分別以$1, $2, $3...記錄。
awk缺省(default)的 字段分隔字符(FS) 為 空白字符(空格及tab)。以本例而言,讀者若未改變 FS,則分割后:
第一個字段($1)="A123", 第二個字段($2)="8:15"。
使用者可用正則表達式自行定義 FS。awk每次需要分割數據行時,都會參考目前FS的值。
例如:
令 FS = "[ :]+" 表示任何由 空白" " 或 冒號":" 所組成的字串都可當成分隔字符,則分割后:
第一個字段($1) = "A123",第二個字段($2) = "8",第三個字段($3) = "15"
- NR
NR 表示從 awk 開始執行該程序后所讀取的數據行數。
- FNR
FNR 與 NR 功用類似,不同的是awk每打開一個新的文件,FNR 便從 0 重新累計。
- NF
NF表示目前的數據行所被切分的字段數。awk 每讀入一行數據后,在程序中可用 NF 來得知該行數據包含的字段個數。在下一行數據被讀入之前,NF 並不會改變。但使用者若自行使用$0來記錄數據,例如:使用 getline,此時 NF 將代表新的 $0 上所記載的數據的字段個數。
- OFS
輸出時的字段分隔字符。缺省為 " "(一個空白),詳見下面說明。
- ORS
輸出時數據行的分隔字符。缺省為 "\n"(換行),見下面說明。
- OFMT
數值數據的輸出格式。缺省為 "%.6g"(若須要時最多打印6位小數)
當使用 print 指令一次打印多項數據時,
例如:print $1, $2
輸出時,awk會自動在 $1 與 $2 之間補上一個 OFS 的值(缺省為一個空白)。
每次使用 print 輸出后,awk會自動補上 ORS 的值(缺省為換行符)。
使用 print 輸出數值數據時,awk將采用 OFMT 的值為輸出格式。
例如:
$ awk 'BEGIN { print 2/3,1; OFS=":"; OFMT="%.2g"; print 2/3,1 }'
輸出:
程序中通過改變OFS和OFMT的值,改變了指令 print 的輸出格式。
- RS
RS( Record Separator):awk從文件上讀取數據時,將根據 RS 的定義把數據切割成許多記錄,而awk一次僅讀入一條記錄進行處理。
RS 的缺省值是 "\n",所以一般 awk一次僅讀入一行數據。有時一個Record含括了幾行數據(Multi-line Record),這情況下不能再以"\n"
來分隔相鄰的記錄,可改用 空白行 來分隔。
在awk程序中,令 RS = "" 表示以 空白行 來分隔相鄰的記錄。
- RSTART
與使用字串函數 match( )有關的變量,詳見下面說明。
- RLENGTH
與使用字串函數match( )有關的變量。
當使用者使用 match(...) 函數后,awk會將 match(...) 執行的結果以RSTART、RLENGTH 記錄。
請參考 附錄 C awk的內置函數 match()。
- SUBSEP
SUBSEP(Subscript Separator) 數組下標的分隔字符,缺省值為"\034"。
實際上,awk中的 數組 只接受 字串 當它的下標,如: Arr["John"]。但使用者在 awk 中仍可使用 數字 當陣列的下標,甚至可使用多維的數組(Multi-dimenisional Array) 如:Arr[2,79]。事實上,awk在接受 Arr[2,79] 之前,就已先把其下標轉換成字串"2\03479",之后便以Arr["2\03479"] 代替 Arr[2,79]。
可參考下例:
awk 'BEGIN {
Arr[2,79] = 78 print Arr[2,79] print Arr[ 2 , 79 ] print Arr["2\03479"] idx = 2 SUBSEP 79 print Arr[idx] } ' $*
執行結果:
附錄E ── 正則表達式(Regular Expression) 簡介
- 為什么要使用正則表達式
UNIX 中提供了許多 指令 和 tools,它們具有在文件中 查找(Search)字串或替換(Replace)字串 的功能。像 grep, vi , sed, awk,...
不論是查找字串或替換字串,都得先告訴這些指令所要查找(被替換)的字串為何。若未能預先明確知道所要查找(被替換)的字串為何,只知該字串存在的范圍或特征時,例如:
(一)查找 "T0.c", "T1.c", "T2.c".... "T9.c" 當中的任一字串。
(二)查找至少存在一個 "A"的任意字串。
這情況下,如何告知執行查找字串的指令所要查找的字串為何。
例 (一) 中,要查找任一在 "T" 與 ".c" 之間存在一個阿拉伯數字的字串,當然您可以列舉的方式,一一把所要查找的字串告訴執行命令的指令。但例 (二) 中合乎該條件的字串有無限種可能,勢必無法一一列舉。此時,便需要另一種字串表示的方法(協定)。
- 什么是正則表達式
正則表達式(以下簡稱 Regexp)是一種字串表達的方式。可用以指定具有某特征的所有字串。
注:為區別於一般字串,本附錄中代表 Regexp 的字串之前皆加 "Regexp"。
注:awk 程序中常以 /..../ 括住 Regexp,以區別於一般字串。
- 組成正則表達式的元素
普通字符:除了 . * [ ] + ? ( ) \ ^ $ 外的所有字符。
由普通字符所組成的Regexp其意義與原字串字面意義相同。
例如:Regexp "the" 與一般字串的 "the" 代表相同的意義。
. (Meta character):用以代表任意一字符。
須留心 UNIX Shell 中使用 "*"表示 Wild card(通配符),可用以代表任意長度的字串。而 Regexp 中使用 "." 來代表一個任意字符(注意:並非任意長度的字串)。Regexp 中 "*" 另有其它涵意,並不代表任意長度的字串。
^ 表示該字串必須出現於行首。
$ 表示該字串必須出現於行末。
例如:
Regexp /^The/ 用以表示所有 "The"出現於行首 的字串 。
Regexp /The$/ 用以表示所有 "The"出現於行末 的字串。
\ 將特殊字符還原成字面意義的字符(Escape character)。
Regexp 中特殊字符將被解釋成特定的意義,若要表示特殊字符的字面(literal meaning)意義時,在特殊字符之前加上"\"即可。
例如:
使用Regexp來表示字串 "a.out"時,不可寫成 /a.out/。因為 "."是特殊字符,表示任一字符。可符合 Regexp / a.out/ 的字串將不只 "a.out" 一個;字串 "a2out"、"a3out"、"aaout" ...都符合 Regexp /a.out/ 。正確的用法為:/ a\.out/
[...] 字符集合,用以表示兩中括號間所有的字符當中的任一個。
例如:
Regexp /[Tt]/ 可用以表示字符 "T" 或 "t"。故 Regexp /[Tt]he/ 表示 字串 "The" 或 "the"。字符集合 [...] 內不可隨意留空白。
例如:
Regexp /[ Tt ]/ 其中括號內有空白字符,除表示"T"、"t" 中任一個字符,也可代表一個 " "(空白字符)。
- 字符集合中可使用 "-" 來指定字符的區間。
例如:
Regexp /[0-9]/ 等於 /[0123456789]/ ,用以表示任意一個阿拉伯數字。
同理 Regexp /[A-Z]/ 用以表示任意一個大寫英文字母。
但應留心:
Regexp /[0-9a-z]/ 並不等於 /[0-9][a-z]/ ;前者表示一個字符,后者表示兩個字符。
Regexp /[-9]/ 或 /[9-]/ 只代表字符 "9"或 "-"。
[^...] 使用[^..] 產生字符集合[..]的補集(complement set)。
例如:
要指定 "T" 或 "t" 之外的任一個字符,可用 /[^Tt]/ 表示。
同理 Regexp /[^a-zA-Z]/ 表示英文字母之外的任一個字符。
須留心:
"^" 的位置:"^"必須緊接於"["之后,才代表字符集合的補集。
例如:
Regexp /[0-9\^]/ 只是用以表示一個阿拉伯數字或字符"^"。
* 形容字符重復次數的特殊字符。"*" 形容它前方的字符可以不出現,也可以出現 1 次或多次。
例如:
Regexp /T[0-9]*\.c/ 中 * 形容其前 [0-9] (一個阿拉伯數字)出現的次數可為 0次或 多次,故Regexp /T[0-9]*\.c/ 可用以表示"T.c"、"T0.c"、"T1.c"、...、"T19.c"。
+ 形容其前的字符出現一次或一次以上。
例如:
Regexp /[0-9]+/ 用以表示一位或一位以上的數字。
? 形容其前的字符可出現一次或不出現。
例如:
Regexp /[+-]?[0-9]+/ 表示數字(一位以上)之前可出現正負號或不出現正負號。
(...) 用以括住一群字符,且將之視成一個group(見下面說明)。
例如:
Regexp /12+/ 表示字串 "12", "122", "1222", "12222",...
Regexp /(12)+/ 表示字串 "12", "1212", "121212", "12121212"....
上式中 12 以( )括住,故 "+" 所形容的是 12,重復出現的也是 12。
| 表示邏輯上的"或"(or)
例如:
Regexp / Oranges? | apples? | water/ 可用以表示:字串 "Orange", "Oranges" 或 "apple", "apples" 或 "water"
- match是什么?
討論 Regexp 時,經常遇到 "某字串匹配( match )某 Regexp"的字眼。其意思為:"這個 Regexp 可被解釋成該字串"。
例如:
字串 "the" 匹配(match) Regexp /[Tt]he/。
因為 Regexp /[Tt]he/ 可解釋成字串 "the" 或 "The",故字串 "the" 或 "The"都匹配(match) Regexp /[Th]he/。
- awk 中提供二個關系運算符(Relational Operator,見注一) ~ !~
它們也稱之為 match、not match。但函義與一般常稱的 match 略有不同。
其定義如下:
A 表示一字串,B 表示一 Regular Expression
只要 A 字串中存在有子字串可 match( 一般定義的 match) Regexp B,則 A ~ B 就算成立,其值為 true,反之則為 false。
! ~ 的定義與 ~ 恰好相反。
例如:
"another" 中含有子字串 "the" 可 match Regexp /[Tt]he/ ,所以 "another" ~ /[Tt]he/ 的值為 true。
注一:有些論著不把這兩個運算符( ~, !~)與 Relational Operators 歸為一類。
- 應用 Regular Expression 解題的簡例
下面列出一些應用 Regular Expression 的簡例,部分范例中會更改$0 的值,若您使用的 awk不允許用戶更改 $0時 請改用 gawk。
例1:
將文件中所有的字串 "Regular Expression" 或 "Regular expression" 換成 "Regexp"
awk ' {
gsub( /Regular[ \t]+[Ee]xpression/, "Regexp") print } ' $*
例2:
去除文件中的空白行(或僅含空白字符或tab的行)
awk '
$0 !~ /^[ \t]*$/ { print }
' $*
例3:
在文件中具有 ddd-dddd (電話號碼型態,d 表示digital)的字串前加上"TEL : "
awk ' {
gsub( /[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/, "TEL : &" ) print } ' $*
例4:
從文件的 Fullname 中分離出 路徑 與 文件名
awk ' BEGIN{ Fullname = "/usr/local/bin/xdvi" match( Fullname, /.*\//)
path = substr(Fullname, 1, RLENGTH-1) name = substr(Fullname, RLENGTH+1) print "path :", path," name :",name } ' $*
結果打印:
例5:
將某一數值改以現金表示法表示(整數部分每三位加一撇,且含二位小數)
awk ' BEGIN { Number = 123456789 Number = sprintf("$%.2f",Number) while( match(Number,/[0-9][0-9][0-9][0-9]/ ) ) sub(/[0-9][0-9][0-9][.,]/, ",&", Number) print Number } ' $*
結果輸出
例6:
把文件中所有具 "program數字.f"形態的字串改為"[Ref : program數字.c]"
awk ' { while( match( $0, /program[0-9]+\.f/ ) ){ Replace = "[Ref : " substr( $0, RSTART, RLENGTH-2) ".c]" sub( /program[0-9]+\.f/, Replace) } print } ' $*