【譯】 AWK教程指南


前面的話:

  這幾天寫了一個程序,在同一個目錄里生成了很多文件,需要統計其中部分文件的總大小,發現經常用到的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時,它會反復進行下列四步驟。 

    1. 自動從指定的數據文件中讀取一個數據行。
    2. 自動更新(Update)相關的內置變量的值。如:NF, NR, $0...
    3. 依次執行程序中 所有 的 Pattern { Actions } 指令。
    4. 當執行完程序中所有 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
    
  Pattern 部分被省略,表示無任何限制條件。故awk讀入每行數據后都將無條件執行這個 Actions。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM