什麼是Awk
Awk是一種小巧的程式語言及命令列工具。(其名稱得自於它的創始人Alfred Aho、Peter Weinberger 和 Brian Kernighan姓氏的首個字母)。它非常適合伺服器上的日誌處理,主要是因為Awk可以對檔案進行操作,通常以可讀文字構建行。
我說它適用於伺服器是因為日誌檔案,轉儲檔案(dump files),或者任意文字格式的伺服器終止轉儲到磁碟都會變得很大,並且在每個伺服器你都會擁有大量的這類檔案。如果你經歷過這樣的情境——在沒有像Splunk或者其他等價的工具情況下不得不在50個不同的伺服器裡分析幾G的檔案,你會覺得去獲取和下載所有的這些檔案並分析他們是一件很糟糕的事。
我親身經歷過這種情境。當一些Erlang節點將要死掉並留下一個700MB到4GB的崩潰轉儲檔案(crash dump)時,或者當我需要在一個小的個人伺服器(叫做VPS)上快速瀏覽日誌,查詢一個常規模式時。
在任何情況下,Awk都不僅僅只是用來查詢資料的(否則,grep或者ack已經足夠使用了)——它同樣使你能夠處理資料並轉換資料。
程式碼結構
Awk指令碼的程式碼結構很簡單,就是一系列的模式(pattern)和行為(action):
1 2 3 4 5 6 7 8 9 10 11 |
# comment Pattern1 { ACTIONS; } # comment Pattern2 { ACTIONS; } # comment Pattern3 { ACTIONS; } # comment Pattern4 { ACTIONS; } |
掃描文件的每一行時都必須與每一個模式進行匹配比較,而且一次只匹配一個模式。那麼,如果我給出一個包含以下內容的檔案:
this is line 1
this is line 2
this is line 1 這行就會與Pattern1進行匹配。如果匹配成功,就會執行ACTIONS。然後this is line 1 會和Pattern2進行匹配。如果匹配失敗,它就會跳到Pattern3進行匹配,以此類推。
一旦所有的模式都匹配過了,this is line 2 就會以同樣的步驟進行匹配。其他的行也一樣,直到讀取完整個檔案。
簡而言之,這就是Awk的執行模式
資料型別
Awk僅有兩個主要的資料型別:字串和數字。即便如此,Awk的字串和數字還可以相互轉換。字串能夠被解釋為數字並把它的值轉換為數字值。如果字串不包含數字,它就被轉換為0.
它們都可以在你程式碼裡的ACTIONS部分使用 = 操作符給變數賦值。我們可以在任意時刻、任意地方宣告和使用變數,也可以使用未初始化的變數,此時他們的預設值是空字串:“”。
最後,Awk有陣列型別,並且它們是動態的一維關聯陣列。它們的語法是這樣的:var[key] = value 。Awk可以模擬多維陣列,但無論怎樣,這是一個大的技巧(big hack)。
模式
可以使用的模式分為三大類:正規表示式、布林表示式和特殊模式。
正規表示式和布林表示式
你使用的Awk正規表示式比較輕量。它們不是Awk下的PCRE(但是gawk可以支援該庫——這依賴於具體的實現!請使用 awk
–version檢視),然而,對於大部分的使用需求已經足夠了:
1 2 3 4 5 |
/admin/ { ... } # any line that contains 'admin' /^admin/ { ... } # lines that begin with 'admin' /admin$/ { ... } # lines that end with 'admin' /^[0-9.]+ / { ... } # lines beginning with series of numbers and periods /(POST|PUT|DELETE)/ # lines that contain specific HTTP verbs |
注意,模式不能捕獲特定的組(groups)使它們在程式碼的ACTIONS部分執行。模式是專門匹配內容的。
布林表示式與PHP或者Javascript中的布林表示式類似。特別的是,在awk中可以使用&&(“與”)、||(“或”)、!(“非”)操作符。你幾乎可以在所有類C語言中找到它們的蹤跡。它們可以對常規資料進行操作。
與PHP和Javascript更相似的特性是比較操作符,==,它會進行模糊匹配(fuzzy matching)。因此“23”字串等於23,”23″ == 23 表示式返回true。!= 操作符同樣在awk裡使用,並且別忘了其他常見的操作符:>,<,>=,和<=。
你同樣可以混合使用它們:布林表示式可以和常規表示式一起使用。 /admin/ || debug == true 這種用法是合法的,並且在遇到包含“admin”單詞的行或者debug變數等於true時該表示式就會匹配成功。
注意,如果你有一個特定的字串或者變數要與正規表示式進行匹配,~ 和!~ 就是你想要的操作符。 這樣使用它們:string ~ /regex/ 和 string !~ /regex/。
同樣要注意的是,所有的模式都只是可選的。一個包含以下內容的Awk指令碼:
{ ACTIONS }
對輸入的每一行都將會簡單地執行ACTIONS。
特殊的模式
在Awk裡有一些特殊的模式,但不是很多。
第一個是BEGIN,它僅在所有的行都輸入到檔案之前進行匹配。這是你可以初始化你的指令碼變數和所有種類的狀態的主要地方。
另外一個就是END。就像你可能已經猜到的,它會在所有的輸入都被處理完後進行匹配。這使你可以在退出前進行清除工作和一些最後的輸出。
最後一類模式,要把它進行歸類有點困難。它處於變數和特殊值之間,我們通常稱它們為域(Field)。而且名副其實。
域
使用直觀的例子能更好地解釋域:
1 2 3 4 5 6 7 8 9 10 11 |
# According to the following line # # $1 $2 $3 # 00:34:23 GET /foo/bar.html # _____________ _____________/ # $0 # Hack attempt? /admin.html$/ && $2 == "DELETE" { print "Hacker Alert!"; } |
域(預設地)由空格分隔。$0 域代表了一整行的字串。 $1 域是第一塊字串(在任何空格之前), $2 域是後一塊,以此類推。
一個有趣的事實(並且是在大多是情況下我們要避免的事情),你可以通過給相應的域賦值來修改相應的行。例如,如果你在一個塊裡執行 $0 = “HAHA THE LINE IS GONE”,那麼現在下一個模式將會對修改後的行進行操作而不是操作原始的行。其他的域變數都類似。
行為
這裡有一堆可用的行為(possible actions),但是最常用和最有用的行為(以我的經驗來說)是:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ print $0; } # prints $0. In this case, equivalent to 'print' alone { exit; } # ends the program { next; } # skips to the next line of input { a=$1; b=$0 } # variable assignment { c[$1] = $2 } # variable assignment (array) { if (BOOLEAN) { ACTION } else if (BOOLEAN) { ACTION } else { ACTION } } { for (i=1; i<x; i++) { ACTION } } { for (item in c) { ACTION } } |
這些內容將會成為你的Awk工具箱的主要工具,在你處理日誌之類的檔案時你可以隨意地使用它們。
Awk裡的變數都是全域性變數。無論你在給定的塊裡定義什麼變數,它對其他的塊都是可見的,甚至是對每一行都是可見的。這嚴重限制了你的Awk指令碼大小,不然他們會造成不可維護的可怕結果。請編寫儘可能小的指令碼。
函式
可以使用下面的語法來呼叫函式:
{ somecall($2) }
這裡有一些有限的內建函式可以使用,所以我可以給出這些函式的通用文件(regular documentation)。
使用者定義的函式同樣很簡單:
1 2 3 4 5 6 7 8 9 |
# function arguments are call-by-value function name(parameter-list) { ACTIONS; # same actions as usual } # return is a valid keyword function add1(val) { return val+1; } |
特殊變數
除了常規變數(全域性的,可以在任意地方使用),這裡還有一系列特殊的變數,它們的的作用有點像配置條目(configuration entries):
1 2 3 4 5 6 7 8 9 10 11 |
BEGIN { # Can be modified by the user FS = ","; # Field Separator RS = "n"; # Record Separator (lines) OFS = " "; # Output Field Separator ORS = "n"; # Output Record Separator (lines) } { # Can't be modified by the user NF # Number of Fields in the current Record (line) NR # Number of Records seen so far ARGV / ARGC # Script Arguments } |
我把可修改的變數放在BEGIN裡,因為我更喜歡在那重寫它們。但是這些變數的重寫可以放在指令碼的任意地方然後在後面的行裡生效。
示例
以上的就是Awk語言的核心內容。我這裡沒有大量的例子,因為我趨向於使用Awk來完成快速的一次性任務。
不過我依然有一些隨身攜帶的指令碼檔案,用來處理一些事情和測試。我最喜歡的一個指令碼是用來處理Erlang的崩潰轉儲檔案,形如下面的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
=erl_crash_dump:0.3 Tue Nov 18 02:52:44 2014 Slogan: init terminating in do_boot () System version: Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] Compiled: Fri Sep 19 03:23:19 2014 Taints: Atoms: 12167 =memory total: 19012936 processes: 4327912 processes_used: 4319928 system: 14685024 atom: 339441 atom_used: 331087 binary: 1367680 code: 8384804 ets: 382552 =hash_table:atom_tab size: 9643 used: 6949 ... =allocator:instr option m: false option s: false option t: false =proc:<0.0.0> State: Running Name: init Spawned as: otp_ring0:start/2 Run queue: 0 Spawned by: [] Started: Tue Nov 18 02:52:35 2014 Message queue length: 0 Number of heap fragments: 0 Heap fragment data: 0 Link list: [<0.3.0>, <0.7.0>, <0.6.0>] Reductions: 29265 Stack+heap: 1598 OldHeap: 610 Heap unused: 656 OldHeap unused: 468 Memory: 18584 Program counter: 0x00007f42f9566200 (init:boot_loop/2 + 64) CP: 0x0000000000000000 (invalid) =proc:<0.3.0> State: Waiting ... =port:#Port<0.0> Slot: 0 Connected: <0.3.0> Links: <0.3.0> Port controls linked-in driver: efile =port:#Port<0.14> Slot: 112 Connected: <0.3.0> ... |
產生下面的結果:
1 2 3 4 5 6 7 8 9 10 |
$ awk -f queue_fun.awk $PATH_TO_DUMP MESSAGE QUEUE LENGTH: CURRENT FUNCTION ====================================== 10641: io:wait_io_mon_reply/2 12646: io:wait_io_mon_reply/2 32991: io:wait_io_mon_reply/2 2183837: io:wait_io_mon_reply/2 730790: io:wait_io_mon_reply/2 80194: io:wait_io_mon_reply/2 ... |
這是在Erlang程式裡執行的函式列表,它們導致了mailboxe變得很龐大。指令碼在這:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# Parse Erlang Crash Dumps and correlate mailbox size to the currently running # function. # # Once in the procs section of the dump, all processes are displayed with # =proc:<0.M.N> followed by a list of their attributes, which include the # message queue length and the program counter (what code is currently # executing). # # Run as: # # $ awk -v threshold=$THRESHOLD -f queue_fun.awk $CRASHDUMP # # Where $THRESHOLD is the smallest mailbox you want inspects. Default value # is 1000. BEGIN { if (threshold == "") { threshold = 1000 # default mailbox size } procs = 0 # are we in the =procs entries? print "MESSAGE QUEUE LENGTH: CURRENT FUNCTION" print "======================================" } # Only bother with the =proc: entries. Anything else is useless. procs == 0 && /^=proc/ { procs = 1 } # entering the =procs entries procs == 1 && /^=/ && !/^=proc/ { exit 0 } # we're done # Message queue length: 1210 # 1 2 3 4 /^Message queue length: / && $4 >= threshold { flag=1; ct=$4 } /^Message queue length: / && $4 < threshold { flag=0 } # Program counter: 0x00007f5fb8cb2238 (io:wait_io_mon_reply/2 + 56) # 1 2 3 4 5 6 flag == 1 && /^Program counter: / { print ct ":", substr($4,2) } |
你跟上思路沒?如果跟上了,你已經瞭解了Awk。恭喜!