見過最好的AWK手冊

原文: http://linuxfire.com.cn/~lily/awk.html

簡體中文版由bones7456 (http://li2z.cn)整理.

原文:應該是http://phi.sinica.edu.tw/aspac/reports/94/94011/但是原文很亂.

說明:之前也是對awk幾乎一無所知,無意中看到這篇文章,網上一搜,居然沒有像樣的簡體中文版.有的也是不怎麼完整,或者錯誤一大堆的.於是就順手整理了下這篇文章.通過整理這篇文章,自己也漸漸掌握了awk的種種用法.

原文可能比較老,有些目前已經不適用的命令有所改動,文中所有命令均在ubuntu7.04下調試通過,用的awkmawk.

由於本人能力有限,錯誤和不妥之處在所難免,歡迎多多指正.

last update: 2010-04-16

1. 前言

有關本手冊 :

這是一本awk學習指引, 其重點着重於 :

l        awk 適於解決哪些問題 ?

l        awk 常見的解題模式爲何 ?

爲使讀者快速掌握awk解題的模式及特性, 本手冊系由一些較具代表性的範例及其題解所構成; 各範例由淺入深, 彼此間相互連貫,範例中並對所使用的awk語法及指令輔以必要的說明. 有關awk的指令, 函數,...等條列式的說明則收錄於附錄中, 以利讀者往後撰寫程序時查閱. 如此編排, 可讓讀者在短時間內順暢地學會使用awk來解決問題. 建議讀者循着範例上機實習, 以加深學習效果.

 

讀者宜先具備下列背景 :

[a.] UNIX 環境下的簡單操作及基本概念.

例如 : 文件編輯, 文件複製 及 管道, 輸入/輸出重定向 等概念

[b.] C 語言的基本語法及流程控制指令.

(awk 指令並不多, 且其中之大部分與 C語言中之用法一致, 本手冊中對該類指令之語法及特性不再加以繁冗的說明, 讀者若欲深究,可自行翻閱相關的 C 語言書籍)

 

2. awk概述

爲什麼使用awk

awk 是一種程序語言. 它具有一般程序語言常見的功能.

因awk語言具有某些特點, 如 : 使用直譯器(Interpreter)不需先行編譯; 變量無類型之分(Typeless), 可使用文字當數組的下標(Associative Array)...等特色. 因此, 使用awk撰寫程序比起使用其它語言更簡潔便利且節省時間. awk還具有一些內建功能, 使得awk擅於處理具數據行(Record), 字段(Field)型態的資料; 此外, awk內建有pipe的功能, 可將處理中的數據傳送給外部的 Shell命令加以處理, 再將Shell命令處理後的數據傳回awk程序, 這個特點也使得awk程序很容易使用系統資源.

由於awk具有上述特色, 在問題處理的過程中, 可輕易使用awk來撰寫一些小工具; 這些小工具並非用來解決整個大問題,它們只扮演解決個別問題過程的某些角色, 可藉由Shell所提供的pipe將數據按需要傳送給不同的小工具進行處理, 以解決整個大問題. 這種解題方式, 使得這些小工具可因不同需求而被重複組合及重用(reuse); 也可藉此方式來先行測試大程序原型的可行性與正確性, 將來若需要較高的執行速度時再用C語言來改寫.這是awk最常被應用之處. 若能常常如此處理問題, 讀者可以以更高的角度來思考抽象的問題, 而不會被拘泥於細節的部份.

本手冊爲awk入門的學習指引, 其內容將先強調如何撰寫awk程序,未列入進一步解題方式的應用實例, 這部分將留待UNIX進階手冊中再行討論.

 

如何取得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

 

awk如何工作

爲便於解釋awk程序架構, 及有關術語(terminology), 先以一個員工薪資檔(emp.dat ), 來加以介紹.

A125 Jenny 100 210

A341 Dan 110 215

P158 Max 130 209

P148 John 125 220

A123 Linda 95 210

文件中各字段依次爲 員工ID, 姓名, 薪資率,及 實際工時. ID中的第一碼爲部門識別碼. "A","P"分別表示"組裝"及"包裝"部門.

本小節着重於說明awk程序的主要架構及工作原理, 並對一些重要的名詞輔以必要的解釋. 由這部分內容, 讀者可體會出awk語言的主要精神及awk與其它語程序言的差異處. 爲便於說明, 以條列方式說明於後.

名詞定義

l        數據行: awk從數據文件上讀取數據的基本單位.以上列文件emp.dat爲例, awk讀入的

第一筆數據行是 "A125 Jenny 100 210"

第二筆數據行是 "A341 Dan 110 215"

一般而言, 一個 數據行 就相當於數據文件上的一行資料. (參考 : 附錄 B 內建變量"RS" )

l        字段(Field) : 爲數據行上被分隔開的子字符串.

以數據行"A125 Jenny 100 210"爲例,

第一欄 第二欄 第三欄 第四欄 "A125" "Jenny" 100 210

一般是以空格符來分隔相鄰的字段. ( 參考 : 附錄 D 內建變量"FS" )

 

3. 如何執行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)這些內建的字段變量?

當 awk 從數據文件中讀取一個數據行時, awk 會使用內建變量$0 予以記錄.每當 $0 被改動時 (例如 : 讀入新的數據行 或 自行變更 $0,...) awk 會立刻重新分析 $0 的字段情況, 並將 $0 上各字段的數據用 $1, $2, ..予以記錄.

 

awk的內建變量(Built-in Variables)

awk 提供了許多內建變量, 使用者於程序中可使用這些變量來取得相關信息.常見的內建變量有 :

內建變量

含義

NF (Number of Fields)

爲一整數, 其值表$0上所存在的字段數目.

NR (Number of Records)

爲一整數, 其值表awk已讀入的數據行數目.

FILENAMEawk

正在處理的數據文件文件名.

 

例如 : awk 從資料文件 emp.dat 中讀入第一筆數據行

"A125 Jenny 100 210" 之後, 程序中:

$0 之值將是 "A125 Jenny 100 210"

$1 之值爲 "A125"

$2 之值爲 "Jenny"

$3 之值爲 100

$4 之值爲 210

$NF 之值爲 4

$NR 之值爲 1

$FILENAME 之值爲 "emp.dat"

 

awk的工作流程 :

執行awk時, 它會反覆進行下列四步驟.

 

  1. 自動從指定的數據文件中讀取一個數據行.
  2. 自動更新(Update)相關的內建變量之值. 如 : NF, NR, $0...
  3. 依次執行程序中 所有 的 Pattern { Actions } 指令.
  4. 當執行完程序中所有 Pattern { Actions } 時, 若數據文件中還有未讀取的數據, 則反覆執行步驟1到步驟4.

awk會自動重複進行上述4個步驟, 使用者不須於程序中編寫這個循環 (Loop).

 

打印文件中指定的字段數據並加以計算

awk 處理數據時, 它會自動從數據文件中一次讀取一筆記錄, 並會

將該數據切分成一個個的字段; 程序中可使用 $1, $2,... 直接取得

各個字段的內容. 這個特色讓使用者易於用 awk 編寫 reformatter 來改變量據格式.

[ 範例 :] 以文件 emp.dat 爲例, 計算每人應發工資並打印報表.

[ 分析 :] awk 會自行一次讀入一列數據, 故程序中僅需告訴

awk 如何處理所讀入的數據行.

執行如下命令 : ( $ 表UNIX命令行上的提示符 )

 

$ awk '{ print $2, $3 * $4 }' emp.dat

執行結果如下 :

屏幕出現 :

Jenny 21000

Dan 23650

Max 27170

John 27500

Linda 19950

 

[ 說明 :]

UNIX命令行上, 執行awk的語法爲:

$awk 'awk程序' 欲處理的資料文件文件名

本範例中的 程序部分 爲 {print $2, $3 * $4}.

把程序置於命令行時, 程序之前後須以 ' 括住.

emp.dat 爲指定給該程序處理的數據文件文件名.

 

本程序中使用 : Pattern { Actions } 語法.

Pattern 部分被省略, 表無任何限制條件. 故awk讀入每筆數據行後都將無條件執行這個 Actions.

print爲awk所提供的輸出指令, 會將數據輸出到stdout(屏幕).

print 的參數間彼此以 "," (逗號) 隔開, 印出數據時彼此間會以空白隔開. (參考 附錄 D 內建變量OFS)

將上述的 程序部分 儲存於文件 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 中說明)

awk中也提供與 C 語言中類似用法的 printf() 函數. 使用該函數可進一步控制數據的輸出格式.

編輯另一個awk程序如下, 並取名爲 pay2.awk

 

{ printf("%6s Work hours: %3d Pay: %5d\n", $2,$3, $3* $4) }

執行下列命令

 

$awk -f pay2.awk emp.dat

 

執行結果屏幕出現:

 Jenny Work hours: 100 Pay: 21000

   Dan Work hours: 110 Pay: 23650

   Max Work hours: 130 Pay: 27170

  John Work hours: 125 Pay: 27500

 Linda Work hours:  95 Pay: 19950

 

4. 選擇符合指定條件的記錄

Pattern { Action }爲awk中最主要的語法. 若某Pattern之值爲真則執行它後方的 Action. awk中常使用"關係表達式" (Relational Expression)來當成 Pattern.

awk 中除了>, <, ==, != ,...等關係運算符( Relational Operators )外,另外提供 ~(match),!~(Not Match) 二個關係運算符. 利用這兩個運算符, 可判斷某字符串是否包含能匹配所指定正則表達式的子字符串. 由於這些特性, 很容易使用awk來編寫需要字符串比對, 判斷的程序.

[ 範例 :] 承上例,

組裝部門員工調薪5%,(組裝部門員工之ID以"A"開頭)

所有員工最後之薪資率若仍低於100, 則以100計.

編寫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

結果如下 : 屏幕出現 :

A125    Jenny 105

A341      Dan 115

P158      Max 130

P148     John 125

A123    Linda 100

說 明 :

awk的工作程序是: 從數據文件中每次讀入一個數據行, 依序執行完程序中所有的 Pattern{ Action }指令:

$1~/^A.*/ { $3 *= 1.05 }

$3 < 100 { $3 = 100 }

{printf("%s %8s %d\n",$1,$2,$3)}

再從數據文件中讀進下一筆記錄繼續進行處理.

第一個 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 語言中用法相同的運算子或語法將不予贅述.

 

第二個 Pattern { Actions } 是: $3 <100 {$3 = 100 } 若第三欄的數據內容(表薪資率)小於100, 則調整爲100.

第三個 Pattern { Actions } 是: {printf("%s %8s %d\n",$1, $2, $3 )} 省略了Pattern(無條件執行Actions), 故所有數據行調整後的數據都將被印出.

 

5. awk 中數組

awk程序中允許使用字符串當做數組的下標(index). 利用這個特色十分有助於資料統計工作.(使用字符串當下標的數組稱爲Associative Array)

首先建立一個數據文件, 並取名爲 reg.dat. 此爲一學生註冊的資料文件; 第一欄爲學生姓名, 其後爲該生所修課程.

Mary O.S. Arch. Discrete

Steve D.S. Algorithm Arch.

Wang Discrete Graphics O.S.

Lisa Graphics A.I.

Lily Discrete Algorithm

awk中數組的特性

使用字符串當數組的下標(index).

使用數組前不須宣告數組名及其大小.

例如: 希望用數組來記錄 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."]++ .

如何取出數組中儲存的信息

以 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

執行結果如下 :

  Graphics 2

      O.S. 2

  Discrete 3

      A.I. 1

      D.S. 1

     Arch. 2

 Algorithm 2

[ 說 明 : ]

 

這程序包含二個Pattern { Actions }指令.

{ for( i=2; i <= NF; i++) Number[$i]++ }

END{for(course in Number) printf("%10s %d\n", course, Number[course] )}

第一個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]++ 之後

i=2時 $i="O.S." Number["O.S."]的值從默認的0,變成了1 ;

i=3時 $i="Arch." Number["Arch."]的值從默認的0,變成了1 ;

同理,i=4時 $i="Discrete" Number["Discrete"]的值從默認的0,變成了1 ;

 

第二個 Pattern { Actions }指令中END 爲awk之保留字, 爲 Pattern 的一種.

END 成立(其值爲true)的條件是: "awk處理完所有數據, 即將離開程序時. "

平常讀入數據行時, END並不成立, 故其後的Actions 並不被執行;

唯有當awk讀完所有數據時, 該Actions纔會被執行 ( 注意, 不管數據行有多少筆, END僅在最後才成立, 故該Actions僅被執行一次.)

BEGIN 與 END 有點類似, 是awk中另一個保留的Pattern.

唯一不同的是: "以 BEGIN 爲 Pattern 的 Actions 於程序一開始執行時, 被執行一次."

NF 爲awk的內建變量, 用以表示awk正處理的數據行中, 所包含的字段個數.

 

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

執行結果將會印出目前在線人數

[ 說 明 : ]

awk 程序並不一定要處理數據文件. 以本例而言, 僅輸入程序文件count.awk, 未輸入任何數據文件.

BEGIN 和 END 同爲awk中的一種 Pattern. 以 BEGIN 爲 Pattern的Actions ,只有在awk開始執行程序,尚未開啓任何輸入文件前, 被執行一次.(注意: 只被執行一次)

"|" 爲 awk 中表示管道的符號. awk 把 | 之前的字符串"who"當成Shell上的命令, 並將該命令送往Shell執行, 執行的結果(原先應於屏幕印出者)則藉由pipe送進awk程序中.

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. 文件中第一欄爲員工代號, 第二欄爲到達時間. 本範例中, 將使用該文件爲數據文件.

1034 7:26

1025 7:27

1101 7:32

1006 7:45

1012 7:46

1028 7:49

1051 7:51

1029 7:57

1042 7:59

1008 8:01

1052 8:05

1005 8:12

 

Ø重定向輸出到文件

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, 其內容如下 :

ID Number Arrival Time

============================

1034 7:26

1025 7:27

1101 7:32

1006 7:45

1012 7:46

1028 7:49

1051 7:51

1029 7:57

1042 7:59

1008 8:01

1052 8:05

1005 8:12

[ 說 明 :  ]

awk程序中, 文件名稱 today_rpt1 的前後須以" (雙引號)括住, 表示 today_rpt1 爲一字符串常量. 若未以"括住, 則 today_rpt1 將被awk解釋爲一個變量名稱.

在awk中任何變量使用之前, 並不須事先聲明. 其初始值爲空字符串(Null string) 或 0.因此程序中若未以 " 將 today_rpt1 括住, 則 today_rpt1 將是一變量, 其值將是空字符串, 這會在執行時造成錯誤(Unix 無法幫您開啓一個以空字符串爲文件名的文件).

因此在編輯awk程序時, 須格外留心. 因爲若敲錯變量名稱,awk在編譯程序時會認爲是一新的變量, 並不會察覺. 因此往往會造成運行時錯誤.

BEGIN 爲awk的保留字, 是 Pattern 的一種.

以 BEGIN 爲 Pattern 的 Actions 於awk程序剛被執行尚未讀取數據文件時被執行一次, 此後便不再被執行.

讀者或許覺得本程序中的I/O重定向符號應使用 " >>" (append)而非 " >".

本程序中若使用 ">" 將數據重導到 today_rpt1, awk 第一次執行該指令時會產生一個新檔 today_rpt1, 其後再執行該指令時則把數據追加到today_rpt1文件末, 並非每執行一次就重開一個新文件.

若採用">>"其差異僅在第一次執行該指令時, 若已存在today_rpt1則 awk 將直接把數據append在原文件之末尾. 這一點, 與UNIX中的用法不同.

Øawk 中如何利用系統資源

awk程序中很容易使用系統資源. 這包括在程序中途調用 Shell 命令來處理程序中的部分數據; 或在調用 Shell 命令後將其產生的結果交回 awk 程序(不需將結果暫存於某個文件). 這一過程是藉助 awk 所提供的管道 (雖然有些類似 Unix 中的管道, 但特性有些不同),及一個從 awk 中呼叫 Unix 的 Shell 命令的語法來達成的.

[例 :] 承上題, 將數據按員工ID排序後再輸出到文件 today_rpt2 , 並於表頭附加執行時的日期.

[ 分 析 : ]

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() 二個.

在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" 的執行次數是 "一次".

 

在 b 語法中, awk將先調用 Shell 命令. 其執行結果將通過 pipe 送入awk程序,以上例而言, awk先讓 Shell 執行 "ls",Shell 執行後將結果存於 pipe, awk指令 getline再從 pipe 中讀取數據.

使用本語法時應留心: 以上例而言,awk "立刻"調用 Shell 來執行 "ls", 執行次數是一次.

getline 則可能執行多次(若pipe中存在多行數據).

除上列 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 內容如下 :

 Today is  09月 21日

=========================

 ID Number Arrival Time

1005 8:12

1006 7:45

1008 8:01

1012 7:46

1025 7:27

1028 7:49

1029 7:57

1034 7:26

1042 7:59

1051 7:51

1052 8:05

1101 7:32

 

[ 說 明 : ]

awk程序由三個主要部分構成 :

[ i.] Pattern { Action} 指令

[ ii.] 函數主體. 例如 : function double( x ){ return 2*x } (參考第11節 Recursive Program )

[ iii.] Comment ( 以 # 開頭識別之 )

awk 的輸入指令 getline, 每次讀取一列數據. 若getline之後

未接任何變量, 則所讀入之資料將以$0 記錄, 否則以所指定的變量儲存之.

[ 以本例而言] :

執行 "date" | getline 後, $0 之值爲 "2007年 09月 21日 星期五 14:28:02 CST",當 $0 之值被更新時, awk將自動更新相關的內建變量, 如: $1,$2,..,NF.故 $2 之值將爲"09月", $3之值將爲"21日".

(有少數舊版的awk不允許即使用者自行更新(update)$0的值,或者更新$0時,它不會自動更新 $1,$2,..NF. 這情況下, 可改用gawk或nawk. 否則使用者也可自行以awk字符串函數split()來分隔$0上的數據)

本程序中 printf() 指令會被執行12次( 因爲有arr.dat中有12行數據), 但讀者不用擔心數據被重複sort了12次. 當awk結束該程序時纔會 close 這個 pipe , 此時纔將這12行數據一次送往系統,並呼叫 "sort -k 1 >> today_rpt2" 處理之.

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中.

 

Ø執行 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,

#!/bin/sh

 

# 注意以下的 awk 與 ' 之間須有空白隔開

awk '

{print}

' $*

# 注意以上的 ' 與 $* 之間須有空白隔開

執行 mydisplay 之前, 須先將它改成可執行的文件(此步驟往後不再贅述). 請執行如下命令:

$ chmod +x mydisplay

往後使用者就可直接把 mydisplay 當成指令, 來display任何文件.

例如 :

$ ./mydisplay today_rpt1 today_rpt2

[ 說 明 : ]

在script文件 mydisplay 中, 指令"awk"與第一個 '  之間須有空格(Shell中並無" awk' "指令).

第一個 ' 用以通知 Shell 其後爲awk程序.

第二個 ' 則表示 awk 程序結束.

故awk程序中一律以"括住字符串或字符, 而不使用 ' , 以免Shell混淆.

$* 爲 shell script中的用法, 它可用來代表命令行上 "mydisplay之後的所有參數".

例如執行 :

$ mydisplay today_rpt1 today_rpt2

事實上 Shell 已先把該指令轉換成 :

awk '

{ print}

' today_rpt1 today_rpt2

本例中, $* 用以代表 "today_rpt1 today_rpt2". 在Shell的語法中, 可用 $1 代表第一個參數, $2 代表第二個參數. 當不確定命令行上的參數個數時, 可使用 $* 表之.

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而產生 " 無法打開文件"的錯誤.

awk會將 Shell 命令行上awk程序(或 -f 程序文件名)之後的所有字符串, 視爲將輸入awk進行處理的數據文件文件名.

若執行awk的命令行上 "未指定任何數據文件文件名", 則將stdin視爲輸入之數據來源, 直到輸入end of file( Ctrl-D )爲止.

讀者可以用下列程序自行測試, 執行如下命令 :

$ awk -f mydump.awk  #(未接任何數據文件文件名)

$ ./mydisplay  #(未接任何數據文件文件名)

將會發現: 此後鍵入的任何數據將逐行復印一份於屏幕上. 這情況不是機器當機 ! 是因爲awk程序正處於執行中. 它正按程序指示, 將讀取數據並重新dump一次; 只因執行時未指定數據文件文件名, 故awk 便以stdin(鍵盤上的輸入)爲數據來源. 讀者可利用這個特點, 設計可與awk即時聊天的程序.

 

Ø改變 awk 切割字段的方式 & 自定義函數

awk不僅能自動分割字段, 也允許使用者改變其字段切割方式以適應各種格式之需要. 使用者也可自定義函數, 若有需要可將該函數單獨寫成一個文件,以供其它awk程序調用.

 

[ 範例 : ] 承接 6.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 )

Regular Expression 中使用中括號 [ ... ] 表示一個字符集合,用以表示任意一個位於兩中括號間的字符.故可用"[ \t:]"表示 一個 空白 , tab 或 ":"

Regular Expression中使用 "+" 形容其前方的字符可出現一次

或一次以上.

故 "[ \t:]+" 表示由一個或多個 "空白, tab 或 : " 所組成的字符串.

設定 FS ="[ \t:]+" 後, 數據行如: "1034 7:26" 將被分割成3個字段

第一欄 第二欄 第三欄

$1 $2 $3

1034 7 26

明顯地, awk程序中使用方法二比方法一更簡潔方便. 本例子中採用方法二,也藉此示範改變字段切割方式的用途.

編寫awk程序 reformat3, 如下 :

#!/bin/sh

 

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 的內容如下:

 Today is  09月 21日

=========================

 ID Number Arrival Time

 1005 8:12 *

 1006 7:45 

 1008 8:01 *

 1012 7:46 

 1025 7:27 

 1028 7:49 

 1029 7:57 

 1034 7:26 

 1042 7:59 

 1051 7:51 

 1052 8:05 *

 1101 7:32 

 Average arrival time : 7:49

[ 說 明 : ]

awk 中亦允許使用者自定函數. 函數定義方式請參考本程序, function 爲 awk 的保留字.

HM_to_M( ) 這函數負責將所傳入之小時及分鐘數轉換成以分鐘爲單位. 使用者自定函數時, 還有許多細節須留心, 如data scope,.. ( 請參考 第十節 Recursive Program)

 

awk中亦提供與 C 語言中相同的 Conditional Operator. 上式printf()中使用arrival >480 ? "*" : " " 即爲一例若 arrival 大於 480 則return "*" , 否則return " ".

 

% 爲awk的運算符(operator), 其作用與 C 語言中之 % 相同(取餘數).

NR(Number of Record) 爲awk的內建變量. 表示awk執行該程序後所讀入的記錄筆數.

 

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 同時打開一個文件進行輸出將會產生不可預期的結果.

讀者應留心上述兩點,纔可正確控制數據輸出到文件中的順序.

指令 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 代替該字符串.

Ø使用 getline 來讀取數據

[ 範 例 : ] 承上題,從文件中讀取當月遲到次數, 並根據當日出勤狀況更新遲到累計數.(按不同的月份累計於不同的文件)

[ 分 析 : ]

程序中自動抓取系統日期的月份名稱, 連接上"late.dat", 形成累計遲到次數的文件名稱(如 : 09月late.dat,...), 並以變量late_file記錄該文件名.

累計遲到次數的文件中的數據格式爲: 員工代號(ID) 遲到次數

例如, 執行本程序前文件 09月late.dat 的內容爲 :

1012 0

1006 1

1052 2

1034 0

1005 0

1029 2

1042 0

1051 0

1008 0

1101 0

1025 1

1028 0

 

編寫程序 reformat4 如下:

#!/bin/sh

 

awk '

BEGIN {

Sys_Sort = "sort -k 1 >> today_rpt4"

Result = "today_rpt4"

# 改變字段切割的方式

FS = "[ \t:]+"

# 令 Shell執行"date"; getline 讀取結果,並以$0記錄

"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 之內容如下 :

 Today is  09月 21日

=========================

 ID Number Arrival Time

1005  8:12     * 1 times

1006  7:45       1 times

1008  8: 1     * 1 times

1012  7:46      

1025  7:27       1 times

1028  7:49      

1029  7:57       2 times

1034  7:26      

1042  7:59      

1051  7:51      

1052  8: 5     * 3 times

1101  7:32      

 Average arrival time : 7:49

09月late.dat 文件被修改爲如下:

1005 1

1012 0

1006 1

1008 1

1101 0

1025 1

1034 0

1042 0

1028 0

1029 2

1051 0

1052 3

說 明 :

late_file 是一變量, 用以記錄遲到次數的文件的文件名.

late_file之值由兩部分構成, 前半部是當月月份名稱(由調用"date"取得)後半部固定爲"late.dat" 如: 09月late.dat.

指令 getline < late_file 表示從late_file所代表的文件中讀取一筆記錄, 並存放於$0.

若使用者可自行把數據放入$0, awk會自動對這新置入 $0 的數據進行字段分割. 之後程序中可用$1, $2,..來表示該筆資料的第一欄,第二欄,..,

(注: 有少數awk版本不容許使用者自行將數據置於 $0, 遇此情況可改用gawk或nawk)

執行getline指令時, 若成功讀取記錄,它會返回1. 若遇到文件結束, 它返回0; 無法打開文件則返回-1.

利用 while( getline < filename >0 ) {....}可讀入文件中的每一筆數據並予處理. 這是awk中用戶自行讀取數據文件的一個重要模式.

數組 cnt[ ] 以員工ID. 當下標(index), 其對應值表示其遲到的次數.

執行結束後, 利用 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 = "" 時:數個並鄰的空白行, awk僅視成一個單一的Record Saparator. (awk不會於兩個緊並的空白行之間讀取一筆空的Record)

awk會略過(skip)文件頭或文件尾的空白行. 故不會因爲這樣的空白行,造成awk多讀入了二筆空的數據.

請觀察下例,首先建立一個數據文件 week.rpt如下:

 

 

張長弓

GNUPLOT 入門

 

 

 

吳國強

Latex 簡介

VAST-2 使用手冊

mathematic 入門

 

李小華

awk Tutorial Guide

Regular Expression

 

 

該文件的開頭有數行空白行, 各筆Record之間使用一個或數個空白行隔開. 讀者請細心觀察,當 RS = "" 時, awk讀取該數據文件之方式.

編輯一個awk程序文件 make_report如下:

#!/bin/sh

 

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. GNUPLOT 入門

 

二. 報告人 : 吳國強 

 1. Latex 簡介

 2. VAST-2 使用手冊

 3. mathematic 入門

 

三. 報告人 : 李小華 

 1. awk Tutorial Guide

 2. Regular Expression

[ 說 明: ]

本程序同時也改變字段分隔字符( FS= "\n" ), 如此一筆數據中的每一行都是一個field. 例如: awk讀入的第一筆 Record 爲

張長弓

GNUPLOT 入門

其中 $1 指的是"張長弓", $2 指的是"GNUPLOT 入門"

上式中的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 :

#!/bin/sh

 

awk '

BEGIN {

for( i=0; i<ARGC ; i++)

print ARGV[i] # 依次印出awk所記錄的參數

}

' $*

執行如下命令 :

$ ./see_arg first-arg second-arg

結果屏幕出現 :

awk

first-arg

second-arg

[ 說明 : ]

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[ ] 中.

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 以進行處理. 但不會打開以apple,orange 爲檔名的文件(因爲 ARGC 被改成2). 但仍可通過ARGV[2], ARGV[3]取得命令行上的參數 apple, orange

 

也可以用下列命令來達成上例的效果.

$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"如下:

#!/bin/sh

 

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

屏幕將產生如下的畫面:

 Press "ctrl-d " to exit

 

蓮霧 的英文生字是:

若輸入 bellfruit

程序將產生

You are right !! Press Enter to Continue ---

 

[ 說 明 : ]

參數 test.dat (ARGV[1]) 表示儲存考題的數據文件文件名. awk 由該文件上取得考題資料後, 將 ARGV[1] 改成 "-".

"-" 表示由 stdin(鍵盤輸入) 數據. 鍵盤輸入數據的結束符號 (End of file)是 ctrl-d. 當 awk 讀到 ctrl-d 時就停止由 stdin 讀取數據.

awk的數學函數中提供兩個與隨機數有關的函數.

srand( ) :  以當前的系統時間作爲隨機數的種子

rand( ) : 返回介於 0與1之間的(近似)隨機數值.

 

11.         使用 awk 編寫遞歸程序

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)

} '

結果屏幕印出

Inside the function: arg1=36,x=35, y=46, z=55

Return to main : arg1= 0, x= 35, y= 46, z= 55

由上可知 :

函數內可任意使用主程序中的任何變量.函數內所啓用的任何變量(除參數外), 於該函數之外依然可以使用.此特性優劣參半, 最大的壞處是式中的變量不易被保護, 特別是遞歸調用本身, 執行子函數時會破壞父函數內的變量.

一個變通的方法是: 在函數的參數列中虛列一些參數. 函數執行中使用這些虛列的參數來記錄不想被破壞的數據,如此執行子函數時就不會破壞到這些數據. 此外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

#!/bin/sh

 

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 2 3

 1 3 2

 2 1 3

 2 3 1

 3 1 2

 3 2 1

 

共 6 種排列方式

 

[ 說 明 : ]

有些較舊版的awk,並不容許使用者指定$0之值. 此時可改用gawk, 或 nawk.否則也可自行使用 split() 函數來分割 main_lst.

爲避免執行子函數時破壞 new_main_lst, nf, i, j 故把這些變量也列於參數列上. 如此,new_main_lst, nf, i, j 將被當成局部變量,而不會受到子函數中同名的變量影響. 讀者聲明函數時,參數列上不妨將這些 "虛列的參數" 與真正用於傳遞信息的參數間以較長的空白隔開, 以便於區別.

awk 中欲將字符串concatenation(連接)時, 直接將兩字符串並置即可(Implicit Operator).

例如 :

awk '

BEGIN{

A = "This "

B = "is a "

C = A B "key." # 變量A與B之間應留空白,否則"AB"將代表另一新變量.

print C

} '

結果將印出

This is a key.

awk使用者所編寫的函數可再重用, 並不需要每個awk式中都重新編寫.

將函數部分單獨編寫於一文件中, 當需要用到該函數時再以下列方式include進來.

$ awk -f 函數文件名 -f awk主程序文件名 數據文件文件名

 

12.         附錄 A ──  Pattern

awk 通過判斷 Pattern 之值來決定是否執行其後所對應的Actions.這裏列出幾種常見的Pattern :

Ø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 並不會開啓任何數據文件進行處理.

ØEND

END 爲 awk 的保留字, 是另一種特殊的 Pattern.

END 成立(其值爲true)的時機與 BEGIN 恰好相反, 爲:"awk 處理完所有數據, 即將離開程序時"平常讀入數據行時, END並不成立, 故其對應的 Actions 並不被執行; 唯有當awk讀完所有數據時, 該 Actions 纔會被執行

注意 : 不管數據行有多少筆, 該 Actions 僅被執行一次.

Ø關係表達式

使用像 " 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 )

Ø正則表達式

直接使用正則表達式當成 Pattern; 此爲 $0 ~ 正則表達式 的簡寫.

該 Pattern 用以判斷 $0(數據行) 中是否含有匹配該正則表達式的子字符串; 若含有該成立(true) 則執行其對應的 Actions.

例如 :

/^[0-9]*$/ { print "This line is a integer !" }

與 $0 ~/^[0-9]*$/ { print "This line is a integer !" } 相同

Ø混合 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 )

Ø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 )

 

13.         附錄 B ── Actions

Actions 是由下列指令(statement)所組成 :

  • 表達式 ( function calls, assignments..)
  • print 表達式列表
  • printf( 格式化字符串, 表達式列表)
  • if( 表達式 ) 語句 [else 語句]
  • while( 表達式 ) 語句
  • do 語句 while( 表達式)
  • for( 表達式; 表達式; 表達式) 語句
  • for( variable in array) 語句
  • delete
  • break
  • continue
  • next
  • exit [表達式]
  • 語句

awk 中大部分指令與 C 語言中的用法一致, 此處僅介紹較爲常用或容易混淆的指令的用法.

Ø流程控制指令

l        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 以[ ] 前後括住表示該部分可視需要而予加入或省略.

l        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 將持續進行其後的語句.

l        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 指令會先執行statement而後再判斷是否應繼續執行. 所以, 無論如何其 statement 部分至少會執行一次.

l        for Statement 指令(一)

語法 :

for(variable in  array ) statement

範例 : 執行下列命令

awk '

BEGIN{

X[1]= 50; X[2]= 60; X["last"]= 70

for( any in X )

printf("X[%s] = %d\n", any, X[any] )

}'

結果輸出 :

X[last] = 70

X[1] = 50

X[2] = 60

(a)這個 for 指令, 專用以查找數組中所有的下標值, 並依次使用所指定的變量予以記錄. 以本例而言, 變量 any 將逐次代表 "last", 1 及2 .

(b)以這個 for 指令, 所查找出的下標之值彼此間並無任何次續關係.

(c)第5節中有該指令的使用範例, 及解說.

l        for Statement 指令(二)

語法 :

for(expression1; expression2; expression3) statement 

範例 :

for(i=1; i< =10; i++)  sum = sum + i

說明 :

(a)上列範例用以計算 1 加到 10 的總和.

(b)expression1  常用於設定該 for 循環的起始條件, 如上例中的 i=1

   expression2 用於設定該循環的停止條件, 如上例中的 i <= 10

   expression3 常用於改變 counter 之值, 如上例中的 i++

 

l        break 指令

break 指令用以強迫中斷(跳離) for, while, do-while 等循環.

範例 :

while(  getline < "datafile" > 0 )

{

    if( $1 == 0 )

        break

    else

        print $2 / $1

}

上例中, awk 不斷地從文件 datafile 中讀取資料, 當$1等於0時,就停止該執行循環.

l        continue 指令

循環中的 statement 進行到一半時, 執行 continue 指令來略過循環中尚未執行的statement.

範例 :

for( index in X_array)

{

    if( index !~ /[0-9]+/ )  continue

    print "There is a digital index", index

}

上例中若 index 不爲數字則執行 continue, 故將略過(不執行)其後的指令.

需留心 continue 與 break 的差異 : 執行 continue 只是掠過其後未執行的statement, 但並未跳離開該循環.

l        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 })執行起.

l        exit 指令

執行 exit 指令時, awk將立刻跳離(停止執行)該awk程序.

 

Øawk 中的 I/O 指令

l        printf 指令

該指令與 C 語言中的用法相同, 可藉由該指令控制資料輸出時的格式.

語法 :

printf("format", item1, item2,.. )

範 例 :

id = "BE-2647";  ave = 89

printf("ID# : %s   Ave Score : %d\n", id, ave)

(a)結果印出 :

ID# : BE-2647   Ave Score : 89

(b)format 部分是由 一般的字串(String Constant) 及 格式控制字符(Formatcontrol letter, 其前會加上一個%字符)所構成. 以上式爲例"ID# : " 及 "  Ave Score : " 爲一般字串. %s 及 %d 爲格式控制字符.

(c)打印時, 一般字串將被原封不動地打印出來. 遇到格式控制字符時,則依序把 format後方之 item 轉換成所指定的格式後進行打印.

(d)有關的細節, 讀者可從介紹 C 語言的書籍上得到較完整的介紹.

(e)print 及 printf 兩個指令, 其後可使用 > 或 >> 將輸出到stdout 的數據重定向到其它文件, 7.1 節中有完整的

l        print 指令

範 例 :

id = "BE-267";  ave = 89

print "ID# :", id, "Ave Score :"ave

(a)結果印出 :

ID# : BE-267 Ave Score :89

(b)print 之後可接上字串常數(Constant String)或變量. 它們彼此間可用"," 隔開.

(c)上式中, 字串 "ID# :" 與變量 id 之間使用","隔開, 打印時兩者之間會以自動 OFS(請參考 附錄D 內建變量 OFS) 隔開. OFS 之值一般內定爲 "一個空格"

(d)上式中, 字串 "Ave Score :" 與變量ave之間並未以","隔開, awk會將這兩者先當成字串concate在一起(變成"Ave Score :89")後,再予打印

l        getline 指令

語法

語法

由何處讀取數據

數據讀入後置於

getline var < file

所指定的 file

變量 var(var省略時,表示置於$0)

getline var

pipe 變量

變量 var(var省略時,表示置於$0)

getline var

見 注一

變量 var(var省略時,表示置於$0)

getline 一次讀取一行資料, 若讀取成功則return 1,若讀取失敗則return -1, 若遇到文件結束(EOF), 則return 0

l        close  指令

該指令用以關閉一個打開的文件, 或 pipe (見下例)

範 例 :

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. 以免造成因二個 processes 同時打開一個文件進行輸出(write)所產生的錯誤.

l        system 指令

該指令用以執行 Shell上的 command.

範 例 :

DataFile = "invent.rpt"

system( "rm " DataFile ) 

說明 :

(a)system("字符串")指令接受一個字符串當成Shell的命令. 上例中, 使用一個字串常數"rm " 連接(concate)一個變量 DataFile 形成要求 Shell 執行的命令.Shell 實際執行的命令爲 "rm invent.rpt". 

l        "|" 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 節,其中有完整的範例說明.

 

Øawk 釋放所佔用的記憶體的指令

awk 程式中常使用數組(Array)來記憶大量數據, delete 指令便是用來釋放數組中的元素所佔用的內存空間.

範 例 :

for( any in X_arr ) 

    delete X_arr[any]

讀者請留心, delete 指令一次只能釋放數組中的一個元素.

Øawk 中的數學運算符(Arithmetic Operators)

+(加), -(減), *(乘), /(除), %(求餘數), ^(指數) 與 C 語言中用法相同

Øawk 中的賦值運算符(Assignment Operators)

=, +=, -=, *= , /=, %=, ^=

x += 5 的意思爲 x = x + 5, 其餘類推.

Øawk 中的條件運算符(Conditional  Operator)

語 法 :

判斷條件 ? value1 : value2

若 判斷條件 成立(true) 則返回 value1, 否則返回 value2.

Øawk 中的邏輯運算符(Logical Operators)

&&( and ), ||(or), !(not)

Extended Regular Expression 中使用 "|" 表示 or 請勿混淆.

 

Øawk 中的關係運算符(Relational Operators)

>, >=, <, < =, ==, !=, ~, !~

Øawk 中其它的運算符

+(正號), -(負號),  ++(Increment Operator), - -(Decrement Operator)

Øawk 中各運算符的運算級

按優先高低排列:

    $       (欄位運算元, 例如 : i=3; $i表示第3欄)

    ^       (指數運算)

    + ,- ,! (正,負號,及邏輯上的 not)

    * ,/ ,% (乘,除,餘數)

    + ,-    (加,減) 

    >, >  =,< , < =, ==, != (關係運算符)

    ~, !~   (match, not match)

    &&      (邏輯上的 and)

    ||      (邏輯上的 or )

    ? :     (條件運算符)

    = , +=, -=,*=, /=, %=, ^= (賦值運算符)

 

14.         附錄C ── awk 的內建函數(Built-in Functions)

Ø(一). 字串函數

 

l        index( 原字串, 找尋的子字串 ):

若原字串中含有欲找尋的子字串,則返回該子字串在原字串中第一次出現的位置,若未曾出現該子字串則返回0.

例如執行 :

$ awk  'BEGIN{ print index("8-12-94","-") }'

結果印出

2

  

l        length( 字串 ) : 返回該字串的長度.

例如執行 :  

$ awk  'BEGIN { print length("John") '}

結果印出

4

l        match( 原字串, 用以找尋比對的正則表達式 ):

awk會在原字串中找尋合乎正則表達式的子字串. 若合乎條件的子字串有多個, 則以原字串中最左方的子字串爲準.

awk找到該字串後會依此字串爲依據進行下列動作:

設定awk內建變量 RSTART, RLENGTH :

          RSTART =  合條件的子字串在原字串中的位置.

                 =  0 ; 若未找到合條件的子字串.

          RLENGTH = 合條件的子字串長度.

                  = -1 ; 若未找到合條件的子字串.

返回 RSTART 之值.

例如執行 :

awk ' BEGIN {

    match( "banana", /(an)+/ )

    print RSTART, RLENGTH

} '       

       執行結果輸出

2 4

 

l        split( 原字串, 數組名稱, 分隔字符 ):

awk將依所指定的分隔字符(field separator)來分隔原字串成一個個的欄位(field),並以指定的數組記錄各個被分隔的欄位.

例如 :

ArgLst = "5P12p89"

split( ArgLst, Arr, /[Pp]/)

     執行後 : Arr[1]=5,  Arr[2]=12,  Arr[3]=89

l        sprintf(格式字符串, 項1, 項2, ...)

該函數的用法與awk或C的輸出函數printf()相同. 所不同的是sprintf()會將要求印出的結果當成一個字串返回. 一般最常使用sprintf()來改變資料格式. 如: x 爲一數值資料, 若欲將其變成一個含二位小數的資料,可執行如下指令:

x = 28

x = sprintf("%.2f",x)

執行後 x = "28.00"

l        sub( 比對用的正則表達式, 將替換的新字串, 原字串 )

sub( )將原字串中第一個(最左邊)合乎所指定的正則表達式的子字串改以新字串取代.

第二個參數"將替換的新字串"中可用"&"來代表"合於條件的子字串"

承上例,執行下列指令:

A = "a6b12anan212.45an6a"

sub( /(an)+[0-9]*/, "[&]", A)

print A

結果輸出

ab12[anan212].45an6a

sub()不僅可執行替換(replacement)的功用,當第二個參數爲空字串("")時,sub()所執行的是"去除指定字串"的功用.

通過 sub() 與 match() 的搭配使用,可逐次取出原字串中合乎指定條件的所有子字串.

例如執行下列程式:

awk '

BEGIN {

data = "p12-P34 P56-p61"

while( match( data ,/[0-9]+/) > 0) {

print substr(data, RSTART, RLENGTH )

sub(/[0-9]+/,"",data)

}

}'

結果輸出 :

12

34

56

61

sub( )中第三個參數(原字串)若未指定,則其預設值爲$0.

可用 sub( /[9-0]+/,"digital" ) 表示 sub(/[0-9]+/,"digital",$0 )

l        gsub( 比對用的正則表達式, 將替換的新字串, 原字串 )

這個函數與 sub()一樣,同樣是進行字串取代的函數. 唯一不同點是

     gsub()會取代所有合條件的子字串.

     gsub()會返回被取代的子字串個數.

 

請參考 sub().

l        substr( 字串,起始位置 [,長度] ):

返回從起始位置起,指定長度的子字串. 若未指定長度,則返回起始位置到字串末尾的子字串.

執行下例

$ awk 'BEGIN { print substr("User:Wei-Lin Liu", 6)}'

結果印出

Wei-Lin Liu

Ø(二). 數學函數

l        int(x) : 返回x的整數部分(去掉小數).

例如 :

int(7.8) 將返回 7

int(-7.8) 將返回 -7

l        sqrt(x) : 返回x的平方根.

例如 :

sqrt(9) 將返回 3

若 x 爲負數,則執行 sqrt(x)時將造成 Run Time Error [譯者注: 我這裏沒有發生錯誤,返回的是"nan"]

l        exp(x) : 將返回e的x次方.

例如 :

exp(1) 將返回 2.71828

l        log(x) : 將返回x以e爲底的對數值.

例如 :

log(exp(1))  將返回 1

若 x< 0 ,則執行 sqrt(x)時將造成 Run Time Error. [譯者注: 我這裏也沒有發生錯誤,返回的是"nan"]

l        sin(x) : x 須以弧度爲單位,sin(x)將返回x的sin函數值.

l        cos(x) : x 須以弧度爲單位,cos(x)將返回x的cos函數值

l        atan2(y,x) : 返回 y/x 的tan反函數之值,返回值系以弧度爲單位.

l        rand() : 返回介於 0與1之間的(近似)隨機數值; 0 < rand()<1.

除非使用者自行指定rand()函數起始的種子,否則每次執行awk程式時,  rand()函數都將使用同一個內定的種子,來產生隨機數.

l        srand([x]) : 指定以x爲rand( )函數起始的種子.

若省略了x,則awk會以執行時的日期與時間爲rand()函數起始的種子.

15.         附錄D ── awk 的內建變量 Built-in Variables

因內建變量的個數不多, 此處按其相關性分類說明, 並未按其字母順序排列.

 

l        ARGC

ARGC表示命令行上除了選項 -F, -v, -f 及其所對應的參數之外的所有參數的個數.若將"awk程式"直接寫於命令列上, 則 ARGC 亦不將該"程式部分"列入計算.

l        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. 

l        FILENAME

FILENAME用以表示目前正在處理的文件檔名.

l        FS

欄位分隔字符.

l        $0

表示目前awk所讀入的數據行.

l        $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"

l        NR

NR 表從 awk 開始執行該程序後所讀取的數據行數.

l        FNR

FNR 與 NR 功用類似. 不同的是awk每打開一個新的文件,FNR 便從 0 重新累計

l        NF

NF表目前的數據行所被切分的欄位數.

awk 每讀入一筆資料後, 在程序中可以 NF 來得知該行數據包含的欄位個數.在下一筆資料被讀入之前, NF 並不會改變. 但使用者若自行使用$0來記錄數據,例如: 使用 getline , 此時 NF 將代表新的 $0 上所記載的資料的欄位個數.

l        OFS

OFS輸出時的欄位分隔字符. 預設值 " "(一個空白), 詳見下面說明.

l        ORS

ORS輸出時數據行的分隔字符. 預設值 "\n"(跳行), 見下面說明.

l        OFMT

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 }'

輸出:

0.666667 1

0.67:1

程序中通過改變OFS和OFMT的值, 改變了指令 print 的輸出格式. 

l        RS

RS( Record Separator) : awk從文件上讀取資料時, 將根據 RS 的定義把資料切割成許多Records,而awk一次僅讀入一個Record,以進行處理.

RS 的預設值是 "\n". 所以一般 awk一次僅讀入一行資料.

有時一個Record含括了幾行資料(Multi-line Record). 這情況下不能再以"\n"

來分隔相鄰的Records, 可改用 空白行 來分隔.

在awk程式中,令 RS = "" 表示以 空白行 來分隔相鄰的Records.

l        RSTART

RSTART與使用字串函數 match( )有關的變量,詳見下面說明.

l        RLENGTH

RLENGTH與使用字串函數match( )有關之變量.

當使用者使用 match(...) 函數後, awk會將 match(...) 執行的結果以RSTART,RLENGTH 記錄.

請參考 附錄 C awk的內建函數 match().

l        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]

}

' $*

執行結果輸出:

78

78

78

78

16.         附錄E ── 正則表達式(Regular Expression) 簡介

l        爲什麼要使用正則表達式

UNIX 中提供了許多 指令 和 tools, 它們具有在文件中 查找(Search)字串或替換(Replace)字串 的功能. 像 grep, vi , sed, awk,...

不論是查找字串或替換字串, 都得先告訴這些指令所要查找(被替換)的字串爲何.若未能預先明確知道所要查找(被替換)的字串爲何, 只知該字串存在的範圍或特徵時,例如 :

  (一)找尋 "T0.c", "T1.c", "T2.c".... "T9.c" 當中的任一字串.

 (二)找尋至少存在一個 "A"的任意字串.

這情況下, 如何告知執行查找字串的指令所要查找的字串爲何.

例 (一) 中, 要查找任一在 "T" 與 ".c" 之間存在一個阿拉伯數字的字串;當然您可以列舉的方式, 一一把所要找尋的字串告訴執行命令的指令.但例 (二) 中合乎該條件的字串有無限種可能, 勢必無法一一列舉.此時,便需要另一種字串表示的方法(協定).

l        什麼是正則表達式

正則表達式(以下簡稱 Regexp)是一種字串表達的方式. 可用以指定具有某特徵的所有字串.

注: 爲區別於一般字串, 本附錄中代表 Regexp 的字串之前皆加 "Regexp". awk 程式中常以/..../括住 Regexp; 以區別於一般字串.

l        組成正則表達式的元素

普通字符 除了 . * [ ] + ? ( ) \  ^ $ 外之所有字符.

由普通字符所組成的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 次或多次, 或不出現(0次).

例如:

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"

l        match是什麼? 

討論 Regexp 時, 經常遇到 "某字串匹配( match )某 Regexp"的字眼. 其意思爲 :  "這個 Regexp 可被解釋成該字串".

[ 例如] :

字串 "the" 匹配(match) Regexp /[Tt]he/.

因爲 Regexp /[Tt]he/ 可解釋成字串 "the" 或 "The", 故字串 "the" 或 "The"都匹配(match) Regexp /[Th]he/.

l        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 歸爲一類.

l        應用 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

}

' $*

結果印出

path : /usr/local/bin   name : xdvi

例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

}

' $*

結果輸出

$123,456,789.00

例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

}

' $*



http://www.blogjava.net/jasmine214--love/archive/2011/01/25/343478.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章