awk有很多種版本,例如nawk、gawk。gawk是GNU awk,它的功能很豐富。
本教學採用的是gawk 4.2.0版本,4.2.0版本的gawk是一個比較大的改版,新支援的一些特性非常好用,而在低於4.2.0版本時這些語法可能會報錯。所以,請先安裝4.2.0版本或更高版本的gawk。
檢視awk版本
awk --version
這裡以安裝gawk 4.2.0為例。
# 1.下載
wget --no-check-certificate https://mirrors.tuna.tsinghua.edu.cn/gnu/gawk/gawk-4.2.0.tar.gz
# 2.解壓、進入解壓後目錄
tar xf gawk-4.2.0.tar.gz
cd gawk-4.2.0/
# 3.編譯,並執行安裝目錄為/usr/local/gawk4.2
./configure --prefix=/usr/local/gawk4.2 && make && make install
# 4.建立一個軟連結:讓awk指向剛新裝的gawk版本
ln -fs /usr/local/gawk4.2/bin/gawk /usr/bin/awk
# 此時,呼叫awk將呼叫新版本的gawk,呼叫gawk將呼叫舊版本的gawk
awk --version
gawk --version
本系列的awk教學中,將大量使用到如下範例檔案a.txt。
ID name gender age email phone
1 Bob male 28 [email protected] 18023394012
2 Alice female 24 [email protected] 18084925203
3 Tony male 21 [email protected] 17048792503
4 Kevin male 21 [email protected] 17023929033
5 Alex male 18 [email protected] 18185904230
6 Andy female 22 [email protected] 18923902352
7 Jerry female 25 [email protected] 18785234906
8 Peter male 20 [email protected] 17729348758
9 Steven female 23 [email protected] 15947893212
10 Bruce female 27 [email protected] 13942943905
讀取檔案有如下幾種常見的方式:
\n
下面使用Shell的read命令來演示前4種讀取檔案的方式(第五種按位元組數讀取的方式read不支援)。
read的-n選項和-N選項可以指定一次性讀取多少個字元。
# 唯讀一個字元
read -n 1 data <a.txt
# 讀100個字元,但如果不足100字元時遇到換行符則停止讀取
read -n 100 data < a.txt
# 強制讀取100字元,遇到換行符也不停止
read -N 100 data < a.txt
如果按照字元數量讀取,直到把檔案讀完,則使用while迴圈,且將檔案放在while結構的後面,而不能放在while迴圈的條件位置:
# 正確
while read -N 3 data;do
echo "$data"
done <a.txt
# 錯誤
while read -N 3 data < a.txt;do
echo "$data"
done
read命令的-d選項可以指定讀取檔案時的分隔符。
# 一直讀取,直到遇到字元m才停止,並將讀取的資料儲存到data變數中
read -d "m" data <a.txt
如果要按分隔符讀取並讀完整個檔案,則使用while迴圈:
while read -d "m" data ;do
echo "$data"
done <a.txt
read預設情況下就是按行讀取的,一次讀取一行。
# 從a.txt中讀取第一行儲存到變數data中
read line <a.txt
如果要求按行讀取完整個檔案,則使用while迴圈:
while read line;do
echo "$line"
done <a.txt
要一次性讀取完整個檔案,有兩種方式:
# 指定超出檔案大小的字元數量
read -N 1000000 data <a.txt
echo "$data"
# 指定檔案中不存在的字元作為分隔符
read -d "_" data <a.txt
echo "$data"
awk 'awk_program' a.txt
$
符號,而$
符號在Shell是變數符號,如果使用雙引號包圍awk程式碼,則$
符號會被Shell解析成Shell變數,然後進行Shell變數替換。使用單引號包圍awk程式碼,則$
會脫離Shell的魔掌,使得$符號留給了awk去解析awk範例:
# 輸出a.txt中的每一行
awk '{print $0}' a.txt
# 多個程式碼塊,程式碼塊中多個語句
# 輸出每行之後還輸出兩行:hello行和world行
awk '{print $0}{print "hello";print "world"}' a.txt
對於awk '{print $0}' a.txt
,它類似於shell的while迴圈while read line;do echo "$line";done <a.txt
。awk隱藏了讀取每一行的while迴圈,它會自動讀取每一行,其中的{print $0}
對應於Shell的while迴圈體echo "$line"
部分。
下面再分析該awk命令的執行過程:
awk的所有程式碼(目前這麼認為)都是寫在語句塊中的。
例如:
awk '{print $0}' a.txt
awk '{print $0}{print $0;print $0}' a.txt
每個語句塊前面可以有pattern,所以格式為:
pattern1{statement1}pattern2{statement3;statement4;...}
語句塊可分為3類:BEGIN語句塊、END語句塊和main語句塊。其中BEGIN語句塊和END語句塊都是的格式分別為BEGIN{...}
和END{...}
,而main語句塊是一種統稱,它的pattern部分沒有固定格式,也可以省略,main程式碼塊是在讀取檔案的每一行的時候都執行的程式碼塊。
分析下面三個awk命令的執行結果:
awk 'BEGIN{print "我在前面"}{print $0}' a.txt
awk 'END{print "我在後面"}{print $0}' a.txt
awk 'BEGIN{print "我在前面"}{print $0}END{print "我在後面"}' a.txt
根據上面3行命令的執行結果,可總結出如下有關於BEGIN、END和main程式碼塊的特性:
awk [ -- ] program-text file ... (1)
awk -f program-file [ -- ] file ... (2)
awk -e program-text [ -- ] file ... (3)
其中:
awk語法結構即awk程式碼部分的結構。
awk的語法充斥著pattern{action}
的模式,它們稱為awk rule。
例如:
awk '
BEGIN{n=3}
/^[0-9]/{$1>5{$1=333;print $1}
/Alice/{print "Alice"}
END{print "hello"}
' a.txt
# 等價的單行式:
awk 'BEGIN{n=3} /^[0-9]/{$1>5{$1=333;print $1} /Alice/{print "Alice"} END{print "hello"}' a.txt
上面範例中,有BEGIN語句塊,有END語句塊,還有2個main程式碼塊,兩個main程式碼塊都使用了正規表示式作為pattern。
關於awk的語法:
pattern{action}
可以直接連線連用對於pattern{action}
語句結構(都稱之為語句塊),其中的pattern部分可以使用下面列出的模式:
# 特殊pattern
BEGIN
END
# 布林程式碼塊
/regular expression/ # 正則匹配成功與否 /a.*ef/{action}
relational expression # 即等值比較、大小比較 3>2{action}
pattern && pattern # 邏輯與 3>2 && 3>1 {action}
pattern || pattern # 邏輯或 3>2 || 3<1 {action}
! pattern # 邏輯取反 !/a.*ef/{action}
(pattern) # 改變優先順序
pattern ? pattern : pattern # 三目運運算元決定的布林值
# 範圍pattern,非布林程式碼塊
pattern1, pattern2 # 範圍,pat1開啟、pat2關閉,即flip,flop模式
action部分,可以是任何語句,例如print。
awk讀取輸入檔案時,每次讀取一條記錄(record)(預設情況下按行讀取,所以此時記錄就是行)。每讀取一條記錄,將其儲存到$0
中,然後執行一次main程式碼段。
awk '{print $0}' a.txt
如果是空檔案,則因為無法讀取到任何一條記錄,將導致直接關閉檔案,而不會進入main程式碼段。
touch x.log # 建立一個空檔案
awk '{print "hello world"}' x.log
可設定表示輸入記錄分隔符的預定義變數RS(Record Separator)來改變每次讀取的記錄模式。
# RS="\n" 、 RS="m"
awk 'BEGIN{RS="\n"}{print $0}' a.txt
awk 'BEGIN{RS="m"}{print $0}' a.txt
RS通常設定在BEGIN程式碼塊中,因為要先於讀取檔案就確定好RS分隔符。
RS指定輸入記錄分隔符時,所讀取的記錄中是不包含分隔符字元的。例如
RS="a"
,則$0
中一定不可能出現字元a。
RS兩種可能情況:
特殊的RS值用來解決特殊讀取需求:
範例:
# 按段落讀取:RS=''
$ awk 'BEGIN{RS=""}{print $0"------"}' a.txt
# 一次性讀取所有資料:RS='\0' RS="^$"
$ awk 'BEGIN{RS="\0"}{print $0"------"}' a.txt
$ awk 'BEGIN{RS="^$"}{print $0"------"}' a.txt
# 忽略空行:RS='\n+'
$ awk 'BEGIN{RS="\n+"}{print $0"------"}' a.txt
# 忽略大小寫:預定義變數IGNORECASE設定為非0值
$ awk 'BEGIN{IGNORECASE=1}{print $0"------"}' RS='[ab]' a.txt
預定義變數RT:
在awk每次讀完一條記錄時,會設定一個稱為RT的預定義變數,表示Record Termination。
當RS為單個字元時,RT的值和RS的值是相同的。
當RS為多個字元(正規表示式)時,則RT設定為正則匹配到記錄分隔符之後,真正用於劃分記錄時的字元。
當無法匹配到記錄分隔符時,RT設定為控制空字串(即預設的初始值)。
awk 'BEGIN{RS="(fe)?male"}{print RT}' a.txt
在讀取每條記錄之後,將其賦值給$0,同時還會設定NR、FNR、RT。
awk '{print NR}' a.txt a.txt
awk '{print FNR}' a.txt a.txt
awk讀取每一條記錄之後,會將其賦值給$0
,同時還會對這條記錄按照預定義變數FS劃分欄位,將劃分好的各個欄位分別賦值給$1 $2 $3 $4...$N
,同時將劃分的欄位數量賦值給預定義變數NF。
$N
參照欄位:
N=0
:即$0
,參照記錄本身0<N<=NF
:參照對應欄位N>NF
:表示參照不存在的欄位,返回空字串N<0
:報錯可使用變數或計算的方式指定要獲取的欄位序號。
awk '{n = 5;print $n}' a.txt
awk '{print $(2+2)}' a.txt # 括號必不可少,用於改變優先順序
awk '{print $(NF-3)}' a.txt
讀取record之後,將使用預定義變數FS、FIELDWIDTHS或FPAT中的一種來分割欄位。分割完成之後,再進入main程式碼段(所以,在main中設定FS對本次已經讀取的record是沒有影響的,但會影響下次讀取)。
FS
或者-F
:欄位分隔符
# 欄位分隔符指定為單個字元
awk -F":" '{print $1}' /etc/passwd
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd
# 欄位分隔符指定為正規表示式
awk 'BEGIN{FS=" +|@"}{print $1,$2,$3,$4,$5,$6}' a.txt
指定預定義變數FIELDWIDTHS按字元寬度分割欄位,這是gawk提供的高階功能。在處理某欄位缺失時非常好用。
用法:
FIELDWIDTHS="3 5 6 9"
表示第一個欄位3字元,第二欄位5字元...FIELDWIDTHS = "8 1:5 6 2:33"
表示:
FIELDWIDTHS="2 3 *"
:
範例1:
# 沒取完的字串DDD被丟棄,且NF=3
$ awk 'BEGIN{FIELDWIDTHS="2 3 2"}{print $1,$2,$3,$4}' <<<"AABBBCCDDDD"
AA BBB CC
# 字串不夠長度時無視
$ awk 'BEGIN{FIELDWIDTHS="2 3 2 100"}{print $1,$2,$3,$4"-"}' <<<"AABBBCCDDDD"
AA BBB CC DDDD-
# *號取剩餘所有,NF=3
$ awk 'BEGIN{FIELDWIDTHS="2 3 *"}{print $1,$2,$3}' <<<"AABBBCCDDDD"
AA BBB CCDDDD
# 欄位數多了,則取完字串即可,NF=2
$ awk 'BEGIN{FIELDWIDTHS="2 30 *"}{print $1,$2,NF}' <<<"AABBBCCDDDD"
AA BBBCCDDDD 2
範例2:處理某些欄位缺失的資料。
如果按照常規的FS進行欄位分割,則對於缺失欄位的行和沒有缺失欄位的行很難統一處理,但使用FIELDWIDTHS則非常方便。
假設a.txt文字內容如下:
ID name gender age email phone
1 Bob male 28 [email protected] 18023394012
2 Alice female 24 [email protected] 18084925203
3 Tony male 21 [email protected] 17048792503
4 Kevin male 21 [email protected] 17023929033
5 Alex male 18 18185904230
6 Andy female 22 [email protected] 18923902352
7 Jerry female 25 [email protected] 18785234906
8 Peter male 20 [email protected] 17729348758
9 Steven female 23 [email protected] 15947893212
10 Bruce female 27 [email protected] 13942943905
因為email欄位有的是空欄位,所以直接用FS劃分欄位不便處理。可使用FIELDWIDTHS。
# 欄位1:4字元
# 欄位2:8字元
# 欄位3:8字元
# 欄位4:2字元
# 欄位5:先跳過3字元,再讀13字元,該欄位13字元
# 欄位6:先跳過2字元,再讀11字元,該欄位11字元
awk '
BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
NR>1{
print "<"$1">","<"$2">","<"$3">","<"$4">","<"$5">","<"$6">"
}' a.txt
# 如果email為空,則輸出它
awk '
BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
NR>1{
if($5 ~ /^ +$/){print $0}
}' a.txt
FS是指定欄位分隔符,來取得除分隔符外的部分作為欄位。
FPAT是取得匹配的字元部分作為欄位。它是gawk提供的一個高階功能。
FPAT根據指定的正則來全域性匹配record,然後將所有匹配成功的部分組成$1、$2...
,不會修改$0
。
awk 'BEGIN{FPAT="[0-9]+"}{print $3"-"}' a.txt
FPAT常用於欄位中包含了欄位分隔符的場景。例如,CSV檔案中的一行資料如下:
Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA
其中逗號分隔每個欄位,但雙引號包圍的是一個欄位整體,即使其中有逗號。
這時使用FPAT來劃分各欄位比使用FS要方便的多。
echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |\
awk '
BEGIN{FPAT="[^,]*|(\"[^\"]*\")"}
{
for (i=1;i<NF;i++){
print "<"$i">"
}
}
'
最後,patsplit()函數和FPAT的功能一樣。
有FS、FIELDWIDTHS、FPAT三種獲取欄位的方式,可使用PROCINFO
陣列來確定本次使用何種方式獲得欄位。
PROCINFO是一個陣列,記錄了awk程序工作時的狀態資訊。
如果:
PROCINFO["FS"]=="FS"
,表示使用FS分割獲取欄位PROCINFO["FPAT"]=="FPAT"
,表示使用FPAT匹配獲取欄位PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS"
,表示使用FIELDWIDTHS分割獲取欄位例如:
if(PROCINFO["FS"]=="FS"){
...FS spliting...
} else if(PROCINFO["FPAT"]=="FPAT"){
...FPAT spliting...
} else if(PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS"){
...FIELDWIDTHS spliting...
}
注意下面的分割和計算兩詞:分割表示使用FS(field Separator),計算表示使用預定義變數OFS(Output Field Separator)。
當讀取一條record之後,將原原本本地被儲存到$0
當中。
awk '{print $0}' a.txt
但是,只要出現了上面所說的任何一種導致$0
重新計算的操作,都會立即使用OFS去重建$0
。
換句話說,沒有導致$0
重建,$0
就一直是原原本本的資料,所以指定OFS也無效。
awk 'BEGIN{OFS="-"}{print $0}' a.txt # OFS此處無效
當$0
重建後,將自動使用OFS重建,所以即使沒有指定OFS,它也會採用預設值(空格)進行重建。
awk '{$1=$1;print $0}' a.txt # 輸出時將以空格分隔各欄位
awk '{print $0;$1=$1;print $0}' OFS="-" a.txt
如果重建$0
之後,再去修改OFS,將對當前行無效,但對之後的行有效。所以如果也要對當前行生效,需要再次重建。
# OFS對第一行無效
awk '{$4+=10;OFS="-";print $0}' a.txt
# 對所有行有效
awk '{$4+=10;OFS="-";$1=$1;print $0}' a.txt
關注$0
重建是一個非常有用的技巧。
例如,下面通過重建$0
的技巧來實現去除行首行尾空格並壓縮中間空格:
$ echo " a b c d " | awk '{$1=$1;print}'
a b c d
$ echo " a b c d " | awk '{$1=$1;print}' OFS="-"
a-b-c-d
# 1.根據行號篩選
awk 'NR==2' a.txt # 篩選出第二行
awk 'NR>=2' a.txt # 輸出第2行和之後的行
# 2.根據正規表示式篩選整行
awk '/qq.com/' a.txt # 輸出帶有qq.com的行
awk '$0 ~ /qq.com/' a.txt # 等價於上面命令
awk '/^[^@]+$/' a.txt # 輸出不包含@符號的行
awk '!/@/' a.txt # 輸出不包含@符號的行
# 3.根據欄位來篩選行
awk '($4+0) > 24{print $0}' a.txt # 輸出第4欄位大於24的行
awk '$5 ~ /qq.com/' a.txt # 輸出第5欄位包含qq.com的行
# 4.將多個篩選條件結合起來進行篩選
awk 'NR>=2 && NR<=7' a.txt
awk '$3=="male" && $6 ~ /^170/' a.txt
awk '$3=="male" || $6 ~ /^170/' a.txt
# 5.按照範圍進行篩選 flip flop
# pattern1,pattern2{action}
awk 'NR==2,NR==7' a.txt # 輸出第2到第7行
awk 'NR==2,$6 ~ /^170/' a.txt
修改欄位時,一定要注意,可能帶來的聯動效應:即使用OFS重建$0。
awk 'NR>1{$4=$4+5;print $0}' a.txt
awk 'BEGIN{OFS="-"}NR>1{$4=$4+5;print $0}' a.txt
awk 'NR>1{$6=$6"*";print $0}' a.txt
從ifconfig命令的結果中篩選出除了lo網路卡外的所有IPv4地址。
# 1.法一:多條件篩選
ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'
# 2.法二:按段落讀取,然後取IPv4欄位
ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'
# 3.法三:按段落讀取,每行1欄位,然後取IPv4欄位
ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'
參考自:man awk
的"AWK PROGRAM EXECUTION"段。
man --pager='less -p ^"AWK PROGRAM EXECUTION"' awk
執行步驟:
除了可以從標準輸入或非選項型引數所指定的檔案中讀取資料,還可以使用getline從其它各種渠道獲取需要處理的資料,它的用法有很多種。
getline的返回值:
ERRNO
變數來描述錯誤為了健壯性,getline時強烈建議進行判斷。例如:
if( (getline) <= 0 ){...}
if((getline) < 0){...}
if((getline) > 0){...}
上面的getline的括號儘量加上,因為getline < 0
表示的是輸入重定向,而不是和數值0進行小於號的比較。
getline無引數時,表示從當前正在處理的檔案中立即讀取下一條記錄儲存到$0
中,並進行欄位分割,然後繼續執行後續程式碼邏輯。
此時的getline會設定NF、RT、NR、FNR、$0和$N。
next也可以讀取下一行。
getline:讀取下一行之後,繼續執行getline後面的程式碼
next:讀取下一行,立即回頭awk迴圈的頭部,不會再執行next後面的程式碼
它們之間的區別用虛擬碼描述,類似於:
# next
exec 9<> filename
while read -u 9 line;do
...code...
continue # next
...code... # 這部分程式碼在本輪迴圈當中不再執行
done
# getline
while read -u 9 line;do
...code...
read -u 9 line # getline
...code...
done
例如,匹配到某行之後,再讀一行就退出:
awk '/^1/{print;getline;print;exit}' a.txt
為了更健壯,應當對getline的返回值進行判斷。
awk '/^1/{print;if((getline)<=0){exit};print}' a.txt
沒有引數的getline是讀取下一條記錄之後將記錄儲存到$0
中,並對該記錄進行欄位的分割。
一個引數的getline是將讀取的記錄儲存到指定的變數當中,並且不會對其進行分割。
getline var
此時的getline只會設定RT、NR、FNR變數和指定的變數var。因此$0和$N以及NF保持不變。
awk '
/^1/{
if((getline var)<=0){exit}
print var
print $0"--"$2
}' a.txt
getline < filename
:從指定檔案filename中讀取一條記錄並儲存到$0
中
$0 $N NF
,不會設定變數NR FNR
getline var < filename
:從指定檔案filename中讀取一條記錄並儲存到指定變數var中
NR FNR NF $0 $N
filename需使用雙引號包圍表示檔案名字元串,否則會當作變數解析getline < "c.txt"
。此外,如果路徑是使用變數構建的,則應該使用括號包圍路徑部分。例如getline < dir "/" filename
中使用了兩個變數構建路徑,這會產生歧義,應當寫成getline <(dir "/" filename)
。
注意,每次從filename讀取之後都會做好位置偏移標記,下次再從該檔案讀取時將根據這個位置標記繼續向後讀取。
例如,每次行首以1開頭時就讀取c.txt檔案的所有行。
awk '
/^1/{
print;
while((getline < "c.txt")>0){print};
close("c.txt")
}' a.txt
上面的close("c.txt")
表示在while(getline)
讀取完檔案之後關掉,以便後面再次讀取,如果不關掉,則檔案偏移指標將一直在檔案結尾處,使得下次讀取時直接遇到EOF。
cmd | getline
:從Shell命令cmd的輸出結果中讀取一條記錄儲存到$0
中
$0 NF $N RT
,不會修改變數NR FNR
cmd | getline var
:從Shell命令cmd的輸出結果中讀取資料儲存到var中
如果要再次執行cmd並讀取其輸出資料,則需要close關閉該命令。例如close("seq 1 5")
,參見下面的範例。
例如:每次遇到以1開頭的行都輸出seq命令產生的1 2 3 4 5
。
awk '/^1/{print;while(("seq 1 5"|getline)>0){print};close("seq 1 5")}' a.txt
再例如,呼叫Shell的date命令生成時間,然後儲存到awk變數cur_date中:
awk '
/^1/{
print
"date +\"%F %T\""|getline cur_date
print cur_date
close("date +\"%F %T\"")
}' a.txt
可以將cmd儲存成一個字串變數。
awk '
BEGIN{get_date="date +\"%F %T\""}
/^1/{
print
get_date | getline cur_date
print cur_date
close(get_date)
}' a.txt
更為複雜一點的,cmd中可以包含Shell的其它特殊字元,例如管道、重定向符號等:
awk '
/^1/{
print
if(("seq 1 5 | xargs -i echo x{}y 2>/dev/null"|getline) > 0){
print
}
close("seq 1 5 | xargs -i echo x{}y 2>/dev/null")
}' a.txt
awk雖然強大,但是有些資料仍然不方便處理,這時可將資料交給Shell命令去幫助處理,然後再從Shell命令的執行結果中取回處理後的資料繼續awk處理。
awk通過|&
符號來支援coproc。
awk_print[f] "something" |& Shell_Cmd
Shell_Cmd |& getline [var]
這表示awk通過print輸出的資料將傳遞給Shell的命令Shell_Cmd去執行,然後awk再從Shell_Cmd的執行結果中取回Shell_Cmd產生的資料。
例如,不想使用awk的substr()來取子串,而是使用sed命令來替換。
awk '
BEGIN{
CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
}
NR>1{
print $5;
print $5 |& CMD;
close(CMD,"to");
CMD |& getline email_domain;
close(CMD);
print email_domain;
}' a.txt
對於awk_print |& cmd; cmd |& getline
的使用,須注意的是:
對於那些要求讀完所有資料再執行的命令,例如sort命令,它們有可能需要等待資料已經完成後(遇到EOF標記)才開始執行任務,對於這些命令,可以多次向coprocess中寫入資料,最後close(CMD,"to")
讓coprocess執行起來。
例如,對age欄位(即$4
)使用sort命令按數值大小進行排序:
awk '
BEGIN{
CMD="sort -k4n";
}
# 將所有行都寫進管道
NR>1{
print $0 |& CMD;
}
END{
close(CMD,"to"); # 關閉管道通知sort開始排序
while((CMD |& getline)>0){
print;
}
close(CMD);
} ' a.txt
close(filename)
close(cmd,[from | to]) # to引數只用於coprocess的第一個階段
如果close()關閉的物件不存在,awk不會報錯,僅僅只是讓其返回一個負數返回值。
close()有兩個基本作用:
awk中任何檔案都只會在第一次使用時開啟,之後都不會再重新開啟。只有關閉之後,再使用才會重新開啟。
例如一個需求是隻要在a.txt中匹配到1開頭的行就輸出另一個檔案x.log的所有內容,那麼在第一次輸出x.log檔案內容之後,檔案偏移指標將在x.log檔案的結尾處,如果不關閉該檔案,則後續所有讀取x.log的檔案操作都從結尾處繼續讀取,但是顯然總是得到EOF異常,所以getline返回值為0,而且也讀取不到任何資料。所以,必須關閉它才能在下次匹配成功時再次從頭讀取該檔案。
awk '
/^1/{
print;
while((getline var <"x.log")>0){
print var
}
close("x.log")
}' a.txt
在處理Coprocess的時候,close()可以指定第二個引數"from"或"to",它們都針對於coproc而言,from時表示關閉coproc |& getline
的管道,使用to時,表示關閉print something |& coproc
的管道。
awk '
BEGIN{
CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
}
NR>1{
print $5;
print $5 |& CMD;
close(CMD,"to"); # 本次close()是必須的
CMD |& getline email_domain;
close(CMD);
print email_domain;
}' a.txt
上面的第一個close是必須的,否則sed會一直阻塞。因為sed一直認為還有資料可讀,只有關閉管道傳送一個EOF,sed才會開始處理。
多數時候,使用awk的print cmd | "sh"
即可實現呼叫shell命令的功能。
$ awk 'BEGIN{print "date +\"%s.%N\" | "sh"}'
但也可以使用system()函數來直接執行一個Shell命令,system()的返回值是命令的退出狀態碼。
$ awk 'BEGIN{system("date +\"%s.%N\"")}'
1572328598.653524342
$ awk 'BEGIN{system("date +\"%s.%N\" >/dev/null")}'
$ awk 'BEGIN{system("date +\"%s.%N\" | cat")}'
1572328631.308807331
system()在開始執行之前會flush gawk的緩衝。特別的,空字串引數的system("")
,它會被gawk特殊對待,它不會去啟動一個shell來執行空命令,而是僅執行flush操作。
關於flush的行為,參考下文。
gawk會按塊緩衝模式來緩衝輸出結果,使用fflush()會將緩衝資料刷出。
fflush([filename])
從gawk 4.0.2之後的版本(不包括4.0.2),無引數fflush()將刷出所有緩衝資料。
此外,終端裝置是行緩衝模式,此時不需要fflush,而重定向到檔案、到管道都是塊緩衝模式,此時可能需要fflush()。
此外,system()在執行時也會flush gawk的緩衝。特別的,如果system的引數為空字串system("")
,則它不會去啟動一個shell子程序而是僅僅執行flush操作。
沒有flush時:
# 在終端輸入幾行資料,將不會顯示,直到按下Ctrl + D
awk '{print "first";print "second"}' | cat
使用fflush():
# 在終端輸入幾行資料,觀察
awk '{print "first";fflush();print "second"}' | cat
使用system()來flush:
# 在終端輸入幾行資料,觀察
awk '{print "first";system("echo system");print "second"}' | cat
awk '{print "first";system("");print "second"}' | cat
也可以使用stdbuf -oL
命令來強制gawk按行緩衝而非預設的按塊緩衝。
# 在終端輸入幾行資料,觀察
stdbuf -oL awk '{print "first";print "second"}' | cat
fflush()也可以指定檔名或命令,表示只刷出到該檔案或該命令的緩衝資料。
# 刷出所有流向到標準輸出的緩衝資料
awk '{print "first";fflush("/dev/stdout");print "second"}' | cat
最後注意,fflush()刷出緩衝資料不代表傳送EOF標記。
awk可以通過print、printf將資料輸出到標準輸出或重定向到檔案。
print elem1,elem2,elem3...
print(elem1,elem2,elem3...)
逗號分隔要列印的欄位列表,各欄位都會自動轉換成字串格式,然後通過預定義變數OFS(output field separator)的值(其預設值為空格)連線各欄位進行輸出。
$ awk 'BEGIN{print "hello","world"}'
hello world
$ awk 'BEGIN{OFS="-";print "hello","world"}'
hello-world
print要輸出的資料稱為輸出記錄,在print輸出時會自動在尾部加上輸出記錄分隔符,輸出記錄分隔符的預定義變數為ORS,其預設值為\n
。
$ awk 'BEGIN{OFS="-";ORS="_\n";print "hello","world"}'
hello-world_
括號可省略,但如果要列印的元素中包含了特殊符號>
,則必須使用括號包圍(如print("a" > "A")
),因為它是輸出重定向符號。
如果省略引數,即print;
等價於print $0;
。
print在輸出資料時,總是會先轉換成字串再輸出。
對於數值而言,可以自定義轉換成字串的格式,例如使用sprintf()進行格式化。
print在自動轉換數值(專指小數)為字串的時候,採用預定義變數OFMT(Output format)定義的格式按照sprintf()相同的方式進行格式化。OFMT預設值為%.6g
,表示有效位(整數部分加小數部分)最多為6。
$ awk 'BEGIN{print 3.12432623}'
3.12433
可以修改OFMT,來自定義數值轉換為字串時的格式:
$ awk 'BEGIN{OFMT="%.2f";print 3.99989}'
4.00
# 格式化為整數
$ awk 'BEGIN{OFMT="%d";print 3.99989}'
3
$ awk 'BEGIN{OFMT="%.0f";print 3.99989}'
4
printf format, item1, item2, ...
格式化字元:
修飾符:均放在格式化字元的前面
N$ N是正整數。預設情況下,printf的欄位列表順序和格式化字元
串中的%號順序是一一對應的,使用N$可以自行指定順序。
printf "%2$s %1$s","world","hello"輸出hello world
N$可以重複指定,例如"%1$s %1$s"將取兩次第一個欄位
寬度 指定該欄位佔用的字元數量,不足寬度預設使用空格填充,超出寬度將無視。
printf "%5s","ni"輸出"___ni",下劃線表示空格
- 表示左對齊。預設是右對齊的。
printf "%5s","ni"輸出"___ni"
printf "%-5s","ni"輸出"ni___"
空格 針對於數值。對於正數,在其前新增一個空格,對於負數,無視
printf "% d,% d",3,-2輸出"_3,-2",下劃線表示空格
+ 針對於數值。對於正數,在其前新增一個+號,對於負數,無視
printf "%+d,%+d",3,-2輸出"+3,-2",下劃線表示空格
# 可變的數值字首。對於%o,將新增字首0,對於%x或%X,將新增字首0x或0X
0 只對數值有效。使用0而非預設的空格填充在左邊,對於左對齊的數值無效
printf "%05d","3"輸出00003
printf "%-05d","3"輸出3
printf "%05s",3輸出____3
' 單引號,表示對數值加上千分位逗號,只對支援千分位表示的locale有效
$ awk "BEGIN{printf \"%'d\n\",123457890}"
123,457,890
$ LC_ALL=C awk "BEGIN{printf \"%'d\n\",123457890}"
123457890
.prec 指定精度。在不同格式化字元下,精度含義不同
%d,%i,%o,%u,%x,%X 的精度表示最大數位字元數量
%e,%E,%f,%F 的精度表示小數點後幾位數
%s 的精度表示最長字元數量,printf "%.3s","foob"輸出foo
%g,%G 的精度表示表示最大有效位數,即整數加小數位的總數量
sprintf()採用和printf相同的方式格式化字串,但是它不會輸出格式化後的字串,而是返回格式化後的字串。所以,可以將格式化後的字串賦值給某個變數。
awk '
BEGIN{
a = sprintf("%03d", 12.34)
print a # 012
}
'
print[f] something | Shell_Cmd
時,awk將建立一個管道,然後啟動Shell命令,print[f]產生的資料放入管道,而命令將從管道中讀取資料。
# 例1:
awk '
NR>1{
print $2 >"name.unsort"
cmd = "sort >name.sort"
print $2 | cmd
#print $2 | "sort >name.sort"
}
END{close(cmd)}
' a.txt
# 例2:awk中構建Shell命令,通過管道交給shell執行
awk 'BEGIN{printf "seq 1 5" | "bash"}'
print[f] something |& Shell_Cmd
時,print[f]產生的資料交給Coprocess。之後,awk再從Coprocess中取回資料。這裡的|&
有點類似於能夠讓Shell_Cmd後臺非同步執行的管道。
awk重定向時可以直接使用/dev/stdin
、/dev/stdout
和/dev/stderr
。還可以直接使用某個已開啟的檔案描述符/dev/fd/N
。
例如:
awk 'BEGIN{print "something OK" > "/dev/stdout"}'
awk 'BEGIN{print "something wrong" > "/dev/stderr"}'
awk 'BEGIN{print "something wrong" | "cat >&2"}'
awk 'BEGIN{getline < "/dev/stdin";print $0}'
$ exec 4<> a.txt
$ awk 'BEGIN{while((getline < "/dev/fd/4")>0){print $0}}'
awk的變數是動態變數,在使用時宣告。
所以awk變數有3種狀態:
參照未賦值的變數,其預設初始值為空字串或數值0。
在awk中未宣告的變數稱為untyped,宣告了但未賦值(只要參照了就宣告了)的變數其型別為unassigned。
gawk 4.2版提供了typeof()
函數,可以測試變數的資料型別,包括測試變數是否宣告。
awk 'BEGIN{
print(typeof(a)) # untyped
if(b==0){print(typeof(b))} # unassigned
}'
除了typeof(),還可以使用下面的技巧進行檢測:
awk 'BEGIN{
if(a=="" && a==0){ # 未賦值時,兩個都true
print "untyped or unassigned"
} else {
print "assigned"
}
}'
awk中的變數賦值語句也可以看作是一個有返回值的表示式。
例如,a=3
賦值完成後返回3,同時變數a也被設定為3。
基於這個特點,有兩點用法:
x=y=z=5
,等價於z=5 y=5 x=5
x != (y = 1)
awk 'BEGIN{print (a=4);print a}'
問題:a=1;arr[a+=2] = (a=a+6)
是怎麼賦值的,對應元素結果等於?arr[3]=7
。但不要這麼做,因為不同awk的賦值語句左右兩邊的評估順序有可能不同。
要在awk中使用Shell變數,有三種方式:
1.在-v選項中將Shell變數賦值給awk變數
num=$(cat a.txt | wc -l)
awk -v n=$num 'BEGIN{print n}'
-v選項是在awk工作流程的第一階段解析的,所以-v選項宣告的變數在BEGIN{}、END{}和main程式碼段中都能直接使用。
2.在非選項型引數位置處使用var=value
格式將Shell變數賦值給awk變數
num=$(cat a.txt | wc -l)
awk '{print n}' n=$num a.txt
非選項型引數設定的變數不能在BEGIN程式碼段中使用。
3.直接在awk程式碼部分暴露Shell變數,交給Shell解析進行Shell的變數替換
num=$(cat a.txt | wc -l)
awk 'BEGIN{print '"$num"'}'
這種方式最靈活,但可讀性最差,可能會出現大量的引號。
gawk有兩種基本的資料型別:數值和字串。在gawk 4.2.0版本中,還支援第三種基本的資料型別:正規表示式型別。
資料是什麼型別在使用它的上下文中決定:在字串操作環境下將轉換為字串,在數值操作環境下將轉換為數值。這和自然語言中的一個詞語、一個單詞在不同句子內的不同語意是一樣的。
隱式轉換:
"123" + 0
返回數值123" 123abc" + 0
轉換為數值時為123"abc"+3
返回3123""
轉換為字串"123"awk 'BEGIN{a="123";print typeof(a+0)}' # number
awk 'BEGIN{a=123;print typeof(a"")}' # string
awk 'BEGIN{a=2;b=3;print(a b)+4}' # 27
顯式轉換:
awk 'BEGIN{a=123.4567;CONVFMT="%.2f";print a""}' #123.46
awk 'BEGIN{a=123.4567;print sprintf("%.2f", a)}' #123.46
awk 'BEGIN{a=123.4567;printf("%.2f",a)}'
gawk 'BEGIN{a="123.4567";print strtonum(a)}' # 123.457
awk中有3種字面量:字串字面量、數值字面量和正規表示式字面量。
# 結果是123而非123.0
awk 'BEGIN{a=123.0;print a}'
++ -- 自增、自減,支援i++和++i或--i或i--
^ 冪運算(**也用於冪運算)
+ - 一元運運算元(正負數符號)
* / % 乘除取模運算
+ - 加減法運算
# 注:
# 1.++和--既可以當作獨立語句,也可以作為表示式,如:
# awk 'BEGIN{a=3;a++;a=++a;print a}'
# 2.**或^冪運算是從右向左計算的:print 2**1**3得到2而不是8
賦值操作(優先順序最低):
= += -= *= /= %= ^= **=
疑惑:b = 6;print b += b++
輸出結果?可能是12或13。不同的awk的實現在評估順序上不同,所以不要用這種可能產生歧義的語句。
awk中的字串都以雙引號包圍,不能以單引號包圍。
"abc"
""
"\0"
、"\n"
字串連線(串聯):awk沒有為字串的串聯操作提供運運算元,可以直接連線或使用空格連線。
awk 'BEGIN{print ("one" "two")}' # "onetwo"
awk 'BEGIN{print ("one""two")}'
awk 'BEGIN{a="one";b="two";print (a b)}'
注意:字串串聯雖然方便,但是要考慮串聯的優先順序。例如下面的:
# 下面第一個串聯成功,第二個串聯失敗,
# 因為串聯優先順序低於加減運算,等價於`12 (" " -23)`
# 即:先轉為數值0-23,再轉為字串12-23
$ awk 'BEGIN{a="one";b="two";print (12 " " 23)}'
12 23
$ awk 'BEGIN{a="one";b="two";print (12 " " -23)}'
12-23
普通正則:
/[0-9]+/
"str" ~ /pattern/
或"str" !~ /pattern/
/pattern/
都等價於$0 ~ /pattern/
if(/pattern/)
等價於if($0 ~ /pattern/)
a=/pattern/
等價於將$0 ~ /pattern/
的匹配返回值(0或1)賦值給a/pattern/ ~ $1
等價於$0 ~ /pattern/ ~ $1
,表示用$1
去匹配0或1/pattern/
作為引數傳給函數時,傳遞的是$0~/pat/
的結果0或1強型別的正則字面量(gawk 4.2.0才支援):
. # 匹配任意字元,包括換行符
^
$
[...]
[^...]
|
+
*
?
()
{m}
{m,}
{m,n}
{,n}
[:lower:]
[:upper:]
[:alpha:]
[:digit:]
[:alnum:]
[:xdigit:]
[:blank:]
[:space:]
[:punct:]
[:graph:]
[:print:]
[:cntrl:]
以下是gawk支援的:
\y 匹配單詞左右邊界部分的空字元位置 "hello world"
\B 和\y相反,匹配單詞內部的空字元位置,例如"crate" ~ `/c\Brat\Be/`成功
\< 匹配單詞左邊界
\> 匹配單詞右邊界
\s 匹配空白字元
\S 匹配非空白字元
\w 匹配單片語成字元(大小寫字母、數位、下劃線)
\W 匹配非單片語成字元
\` 匹配字串的絕對行首 "abc\ndef"
\' 匹配字串的絕對行尾
gawk不支援正則修飾符,所以無法直接指定忽略大小寫的匹配。
如果想要實現忽略大小寫匹配,則可以將字串先轉換為大寫、小寫再進行匹配。或者設定預定義變數IGNORECASE為非0值。
# 轉換為小寫
awk 'tolower($0) ~ /bob/{print $0}' a.txt
# 設定IGNORECASE
awk '/BOB/{print $0}' IGNORECASE=1 a.txt
在awk中,沒有像其它語言一樣專門提供true、false這樣的關鍵字。
但它的布林值邏輯非常簡單:
awk '
BEGIN{
if(1){print "haha"}
if("0"){print "hehe"}
if(a=3){print "hoho"} # if(3){print "hoho"}
if(a==3){print "aoao"}
if(/root/){print "heihei"} # $0 ~ /root/
}'
awk最基本的資料型別只有string和number(gawk 4.2.0版本之後支援正規表示式型別)。但是,對於使用者輸入資料(例如從檔案中讀取的各個欄位值),它們理應屬於string型別,但有時候它們看上去可能像是數值(例如$2=37
),而有時候有需要這些值是數值型別。
注意,strnum型別只針對於awk中除數值常數、字串常數、表示式計算結果外的資料。例如從檔案中讀取的欄位$1
、$2
、ARGV陣列中的元素等等。
$ echo "30" | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
$ echo "+30" | awk '{print typeof($1)}'
strnum
$ echo "30a" | awk '{print typeof($1)}'
string
$ echo "30 a" | awk '{print typeof($0) " " typeof($1)}'
string strnum
$ echo " +30 " | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
比較操作符:
< > <= >= != == 大小、等值比較
in 陣列成員測試
比較規則:
|STRING NUMERIC STRNUM
-------|-----------------------
STRING |string string string
NUMERIC|string numeric numeric
STRNUM |string numeric numeric
簡單來說,string優先順序最高,只要string型別參與比較,就都按照string的比較方式,所以可能會進行隱式的型別轉換。
其它時候都採用num型別比較。
$ echo ' +3.14' | awk '{print typeof($0) " " typeof($1)}' #strnum strnum
$ echo ' +3.14' | awk '{print($0 == " +3.14")}' #1
$ echo ' +3.14' | awk '{print($0 == "+3.14")}' #0
$ echo ' +3.14' | awk '{print($0 == "3.14")}' #0
$ echo ' +3.14' | awk '{print($0 == 3.14)}' #1
$ echo ' +3.14' | awk '{print($1 == 3.14)}' #1
$ echo ' +3.14' | awk '{print($1 == " +3.14")}' #0
$ echo ' +3.14' | awk '{print($1 == "+3.14")}' #1
$ echo ' +3.14' | awk '{print($1 == "3.14")}' #0
$ echo 1e2 3|awk ’{print ($1<$2)?"true":"false"}’ #false
採用字串比較時需注意,它是逐字元逐字元比較的。
"11" < "9" # true
"ab" < 99 # false
&& 邏輯與
|| 邏輯或
! 邏輯取反
expr1 && expr2 # 如果expr1為假,則不用計算expr2
expr1 || expr2 # 如果expr1為真,則不用計算expr2
# 注:
# 1. && ||會短路運算
# 2. !優先順序高於&&和||
# 所以`! expr1 && expr2`等價於`(! expr1) && expr2`
!
可以將資料轉換成數值的1或0,取決於資料是布林真還是布林假。!!
可將資料轉換成等價布林值的1或0。
$ awk 'BEGIN{print(!99)}' # 0
$ awk 'BEGIN{print(!"ab")}' # 0
$ awk 'BEGIN{print(!0)}' # 1
$ awk 'BEGIN{print(!ab)}' # 1,因為ab變數不存在
$ awk 'BEGIN{print(!!99)}' # 1
$ awk 'BEGIN{print(!!"ab")}' # 1
$ awk 'BEGIN{print(!!0)}' # 0
$ awk 'BEGIN{print(!!ab)}' # 0
由於awk中的變數未賦值時預設初始化為空字串或數值0,也就是布林假。那麼可以直接對一個未賦值的變數執行!
操作。
下面是一個非常有意思的awk技巧,它通過多次!
對一個flag取反來實現只輸出指定範圍內的行。
# a.txt
$1==1{flag=!flag;print;next} # 在匹配ID=1的行時,flag=1
flag{print} # 將輸出ID=2,3,4,5的行
$1==5{flag=!flag;next} # ID=5時,flag=0
藉此,就可以讓awk實現一個多行處理模式。例如,將指定範圍內的資料儲存到一個變數當中去。
$1==1{flag=!flag;next}
flag{multi_line=multi_line$0"\n"}
$1==5{flag=!flag;next}
END{printf multi_line}
優先順序從高到低:man awk
()
$ # $(2+2)
++ --
^ **
+ - ! # 一元運運算元
* / %
+ -
space # 這是字元連線操作 `12 " " 23` `12 " " -23`
| |&
< > <= >= != == # 注意>即是大於號,也是print/printf的重定向符號
~ !~
in
&&
||
?:
= += -= *= /= %= ^=
對於相同優先順序的運運算元,通常都是從左開始運算,但下面2種例外,它們都從右向左運算:
= += -= *=
a - b + c => (a - b) + c
a = b = c => a =(b = c)
2**2**3 => 2**(2**3)
再者,注意print和printf中出現的>
符號,這時候它表示的是重定向符號,不能再出現優先順序比它低的運運算元,這時可以使用括號改變優先順序。例如:
awk 'BEGIN{print "foo" > a < 3 ? 2 : 1)' # 語法錯誤
awk 'BEGIN{print "foo" > (a < 3 ? 2 : 1)}' # 正確
注:awk中語句塊沒有作用域,都是全域性變數。
if (condition) statement [ else statement ]
expr1?expr2:expr3
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
for (var in array) statement
break
continue
next
nextfile
exit [ expression ]
{ statements }
switch (expression) {
case value|regex : statement
...
[ default: statement ]
}
{statement}
# 單獨的if
if(cond){
statements
}
# if...else
if(cond1){
statements1
} else {
statements2
}
# if...else if...else
if(cond1){
statements1
} else if(cond2){
statements2
} else if(cond3){
statements3
} else{
statements4
}
搞笑題:妻子告訴程式設計師老公,去買一斤包子,如果看見賣西瓜的,就買兩個。結果是買了兩個包子回來。
# 自然語言的語意
買一斤包子
if(有西瓜){
買兩個西瓜
}
# 程式設計師理解的語意
if(沒有西瓜){
買一斤包子
}else{
買兩個包子
}
awk '
BEGIN{
mark = 999
if (mark >=0 && mark < 60) {
print "學渣"
} else if (mark >= 60 && mark < 90) {
print "還不錯"
} else if (mark >= 90 && mark <= 100) {
print "學霸"
} else {
print "錯誤分數"
}
}
'
expr1 ? expr2 : expr3
if(expr1){
expr2
} else {
expr3
}
awk 'BEGIN{a=50;b=(a>60) ? "及格" : "不及格";print(b)}'
awk 'BEGIN{a=50; a>60 ? b="及格" : b="不及格";print(b)}'
switch (expression) {
case value1|regex1 : statements1
case value2|regex2 : statements2
case value3|regex3 : statements3
...
[ default: statement ]
}
awk 中的switch分支語句功能較弱,只能進行等值比較或正則匹配。
各分支結尾需使用break來終止。
{
switch($1){
case 1:
print("Monday")
break
case 2:
print("Tuesday")
break
case 3:
print("Wednesday")
break
case 4:
print("Thursday")
break
case 5:
print("Friday")
break
case 6:
print("Saturday")
break
case 7:
print("Sunday")
break
default:
print("What day?")
break
}
}
分支穿透:
{
switch($1){
case 1:
case 2:
case 3:
case 4:
case 5:
print("Weekday")
break
case 6:
case 7:
print("Weekend")
break
default:
print("What day?")
break
}
}
while(condition){
statements
}
do {
statements
} while(condition)
while先判斷條件再決定是否執行statements,do...while先執行statements再判斷條件決定下次是否再執行statements。
awk 'BEGIN{i=0;while(i<5){print i;i++}}'
awk 'BEGIN{i=0;do {print i;i++} while(i<5)}'
多數時候,while和do...while是等價的,但如果第一次條件判斷失敗,則do...while和while不同。
awk 'BEGIN{i=0;while(i == 2){print i;i++}}'
awk 'BEGIN{i=0;do {print i;i++} while(i ==2 )}'
所以,while可能一次也不會執行,do...while至少會執行一次。
一般用while,do...while相比while來說,用的頻率非常低。
for (expr1; expr2; expr3) {
statement
}
for (idx in array) {
statement
}
break可退出for、while、do...while、switch語句。
continue可讓for、while、do...while進入下一輪迴圈。
awk '
BEGIN{
for(i=0;i<10;i++){
if(i==5){
break
}
print(i)
}
# continue
for(i=0;i<10;i++){
if(i==5)continue
print(i)
}
}'
next會在當前語句處立即停止後續操作,並讀取下一行,進入迴圈頂部。
例如,輸出除第3行外的所有行。
awk 'NR==3{next}{print}' a.txt
awk 'NR==3{getline}{print}' a.txt
nextfile會在當前語句處立即停止後續操作,並直接讀取下一個檔案,並進入迴圈頂部。
例如,每個檔案只輸出前2行:
awk 'FNR==3{nextfile}{print}' a.txt a.txt
exit [exit_code]
直接退出awk程式。
注意,END語句塊也是exit操作的一部分,所以在BEGIN或main段中執行exit操作,也會執行END語句塊。
如果exit在END語句塊中執行,則立即退出。
所以,如果真的想直接退出整個awk,則可以先設定一個flag變數,然後在END語句塊的開頭檢查這個變數再exit。
BEGIN{
...code...
if(cond){
flag=1
exit
}
}
{}
END{
if(flag){
exit
}
...code...
}
awk '
BEGIN{print "begin";flag=1;exit}
{}
END{if(flag){exit};print "end2"}
'
exit可以指定退出狀態碼,如果觸發了兩次exit操作,即BEGIN或main中的exit觸發了END中的exit,且END中的exit沒有指定退出狀態碼時,則採取前一個退出狀態碼。
$ awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit 1}}'
$ echo $?
1
$ awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit}}'
$ echo $?
2
awk陣列特性:
arr[idx]
arr[idx] = value
索引可以是整數、負數、0、小數、字串。如果是數值索引,會按照CONVFMT變數指定的格式先轉換成字串。
例如:
awk '
BEGIN{
arr[1] = 11
arr["1"] = 111
arr["a"] = "aa"
arr[-1] = -11
arr[4.3] = 4.33
# 本文來自駿馬金龍:www.junmajinlong.com
print arr[1] # 111
print arr["1"] # 111
print arr["a"] # aa
print arr[-1] # -11
print arr[4.3] # 4.33
}
'
通過索引的方式存取陣列中不存在的元素時,會返回空字串,同時會建立這個元素並將其值設定為空字串。
awk '
BEGIN{
arr[-1]=3;
print length(arr); # 1
print arr[1];
print length(arr) # 2
}'
awk提供了length()
函數來獲取陣列的元素個數,它也可以用於獲取字串的字元數量。還可以獲取數值轉換成字串後的字元數量。
awk 'BEGIN{arr[1]=1;arr[2]=2;print length(arr);print length("hello")}'
delete arr[idx]
:刪除陣列arr[idx]
元素
delete arr
:刪除陣列所有元素$ awk 'BEGIN{arr[1]=1;arr[2]=2;arr[3]=3;delete arr[2];print length(arr)}'
2
isarray(arr)
可用於檢測arr是否是陣列,如果是陣列則返回1,否則返回0。
typeof(arr)
可返回資料型別,如果arr是陣列,則其返回"array"。
awk 'BEGIN{
arr[1]=1;
print isarray(arr);
print (typeof(arr) == "array")
}'
不要使用下面的方式來測試元素是否在陣列中:
if(arr["x"] != ""){...}
這有兩個問題:
應當使用陣列成員測試操作符in來測試:
# 注意,idx不要使用index,它是一個內建函數
if (idx in arr){...}
它會測試索引idx是否在陣列中,如果存在則返回1,不存在則返回0。
awk '
BEGIN{
# 本文來自駿馬金龍:www.junmajinlong.com
arr[1]=1;
arr[2]=2;
arr[3]=3;
arr[1]="";
delete arr[2];
print (1 in arr); # 1
print (2 in arr); # 0
}'
awk提供了一種for變體來遍歷陣列:
for(idx in arr){print arr[idx]}
因為awk陣列是關聯陣列,元素是不連續的,也就是說沒有順序。遍歷awk陣列時,順序是不可預測的。
例如:
# 本文來自駿馬金龍:www.junmajinlong.com
awk '
BEGIN{
arr["one"] = 1
arr["two"] = 2
arr["three"] = 3
arr["four"] = 4
arr["five"] = 5
for(i in arr){
print i " -> " arr[i]
}
}
'
此外,不要隨意使用for(i=0;i<length(arr);i++)
來遍歷陣列,因為awk陣列是關聯陣列。但如果已經明確知道陣列的所有元素索引都位於某個數值範圍內,則可以使用該方式進行遍歷。
例如:
# 本文來自駿馬金龍:www.junmajinlong.com
awk '
BEGIN{
arr[1] = "one"
arr[2] = "two"
arr[3] = "three"
arr[4] = "four"
arr[5] = "five"
arr[10]= "ten"
for(i=0;i<=10;i++){
if(i in arr){
print arr[i]
}
}
}
'
在awk中,很多時候單純的一個陣列只能存放兩個資訊:一個索引、一個值。但在一些場景下,這樣簡單的儲存能力在處理複雜需求的時候可能會捉襟見肘。
為了儲存更多資訊,方式之一是將第3份、第4份等資訊全部以特殊方式存放到值中,但是這樣的方式在實際使用過程中並不方便,每次都需要去分割值從而取出各部分的值。
另一種方式是將第3份、第4份等資訊存放在索引中,將多份資料組成一個整體構成一個索引。
gawk中提供了將多份資料資訊組合成一個整體當作一個索引的功能。預設方式為arr[x,y]
,其中x和y是要結合起來構建成一個索引的兩部分資料資訊。逗號稱為下標分隔符,在構建索引時會根據預定義變數SUBSEP的值將多個索引組合起來。所以arr[x,y]
其實完全等價於arr[x SUBSEP y]
。
例如,如果SUBSEP設定為"@",那麼arr[5,12] = 512
儲存時,其真實索引為5@12
,所以要存取該元素需使用arr["5@12"]
。
SUBSEP的預設值為\034
,它是一個不可列印的字元,幾乎不可能會出現在字串當中。
如果我們願意的話,我們也可以自己將多份資料組合起來去構建成一個索引,例如arr[x" "y]
。但是awk提供了這種更為簡便的方式,直接用即可。
為了測試這種複雜陣列的索引是否在陣列中,可以使用如下方式:
arr["a","b"] = 12
if (("a", "b") in arr){...}
例如,順時針倒轉下列資料:
1 2 3 4 5 6
2 3 4 5 6 1
3 4 5 6 1 2
4 5 6 1 2 3
結果:
4 3 2 1
5 4 3 2
6 5 4 3
1 6 5 4
2 1 6 5
3 2 1 6
{
nf = NF
nr = NR
for(i=1;i<=NF;i++){
arr[NR,i] = $i
}
}
END{
for(i=1;i<=nf;i++){
for(j=nr;j>=1;j--){
if(j%nr == 1){
printf "%s\n", arr[j,i]
}else {
printf "%s ", arr[j,i]
}
}
}
}
子陣列是指陣列中的元素也是一個陣列,即Array of Array,它也稱為子陣列(subarray)。
awk也支援子陣列,在效果上即是巢狀陣列或多維陣列。
a[1][1] = 11
a[1][2] = 12
a[1][3] = 13
a[2][1] = 21
a[2][2] = 22
a[2][3] = 23
a[2][4][1] = 241
a[2][4][2] = 242
a[2][4][1] = 241
a[2][4][3] = 243
通過如下方式遍歷二維陣列:
由於awk陣列是關聯陣列,預設情況下,for(idx in arr)
遍歷陣列時順序是不可預測的。
但是gawk提供了PROCINFO["sorted_in"]
來指定遍歷的元素順序。它可以設定為兩種型別的值:
@unsorted
:預設值,遍歷時無序@ind_str_asc
:索引按字串比較方式升序遍歷@ind_str_desc
:索引按字串比較方式降序遍歷@ind_num_asc
:索引強制按照數值比較方式升序遍歷。所以無法轉換為數值的字串索引將當作數值0進行比較@ind_num_desc
:索引強制按照數值比較方式降序遍歷。所以無法轉換為數值的字串索引將當作數值0進行比較@val_type_asc
:按值升序比較,此外數值型別出現在前面,接著是字串型別,最後是陣列類(即認為num<str<arr
)@val_type_desc
:按值降序比較,此外陣列型別出現在前面,接著是字串型別,最後是數值型(即認為num<str<arr
)@val_str_asc
:按值升序比較,數值轉換成字串再比較,而陣列出現在尾部(即認str<arr
)@val_str_desc
:按值降序比較,數值轉換成字串再比較,而陣列出現在頭部(即認str<arr
)@val_num_asc
:按值升序比較,字串轉換成數值再比較,而陣列出現在尾部(即認num<arr
)@val_num_desc
:按值降序比較,字串轉換成數值再比較,而陣列出現在頭部(即認為num<arr
)例如:
awk '
BEGIN{
arr[1] = "one"
arr[2] = "two"
arr[3] = "three"
arr["a"] ="aa"
arr["b"] ="bb"
arr[10]= "ten"
#PROCINFO["sorted_in"] = "@ind_num_asc"
#PROCINFO["sorted_in"] = "@ind_str_asc"
PROCINFO["sorted_in"] = "@val_str_asc"
for(idx in arr){
print idx " -> " arr[idx]
}
}'
a -> aa
b -> bb
1 -> one
2 -> two
3 -> three
10 -> ten
# 本文來自駿馬金龍:www.junmajinlong.com
如果指定為使用者自定義的排序函數,其函數格式為:
function sort_func(i1,v1,i2,v2){
...
return <0;0;>0
}
其中,i1和i2是每次所取兩個元素的索引,v1和v2是這兩個索引的對應值。
如果返回值小於0,則表示i1在i2前面,i1先被遍歷。如果等於0,則表示i1和i2具有等值關係,它們的遍歷順序不可保證。如果大於0,則表示i2先於i1被遍歷。
例如,對陣列元素按數值大小比較來決定遍歷順序。
awk '
function cmp_val_num(i1, v1, i2, v2){
if ((v1 - v2) < 0) {
return -1
} else if ((v1 - v2) == 0) {
return 0
} else {
return 1
}
# return (v1-v2)
}
NR > 1 {
arr[$0] = $4
}
END {
PROCINFO["sorted_in"] = "cmp_val_num"
for (i in arr) {
print i
}
}' a.txt
再比如,按陣列元素值的字元大小來比較。
function cmp_val_str(i1,v1,i2,v2) {
v1 = v1 ""
v2 = v2 ""
if(v1 < v2){
return -1
} else if(v1 == v2){
return 0
} else {
return 1
}
# return (v1 < v2) ? -1 : (v1 != v2)
}
NR>1{
arr[$0] = $2
}
END{
PROCINFO["sorted_in"] = "cmp_val_str"
for(line in arr)
{
print line
}
}
再比如,對元素值按數值升序比較,且相等時再按第一個欄位ID進行數值降序比較。
awk '
function cmp_val_num(i1,v1,i2,v2, a1,a2) {
if (v1<v2) {
return - 1
} else if(v1 == v2){
split(i1, a1, SUBSEP)
split(i2, a2, SUBSEP)
return a2[2] - a1[2]
} else {
return 1
}
}
NR>1{
arr[$0,$1] = $4
}
END{
PROCINFO["sorted_in"] = "cmp_val_num"
for(str in arr){
split(str, a, SUBSEP)
print a[1]
}
}
' a.txt
上面使用的arr[x,y]
來儲存額外資訊,下面使用arr[x][y]
多維陣列的方式來儲存額外資訊實現同樣的排序功能。
NR>1{
arr[NR][$0] = $4
}
END{
PROCINFO["sorted_in"] = "cmp_val_num"
for(nr in arr){
for(line in arr[nr]){
print line
}
# 本文來自駿馬金龍:www.junmajinlong.com
}
}
function cmp_val_num(i1,v1,i2,v2, ii1,ii2){
# 獲取v1/v2的索引,即$0的值
for(ii1 in v1){ }
for(ii2 in v2){ }
if(v1[ii1] < v2[ii2]){
return -1
}else if(v1[ii1] > v2[ii2]){
return 1
}else{
return (i2 - i1)
}
}
此外,gawk還提供了兩個內建函數asort()和asorti()來對陣列進行排序。
預定義變數ARGV是一個陣列,包含了所有的命令列引數。該陣列使用從0開始的數值作為索引。
預定義變數ARGC初始時是ARGV陣列的長度,即命令列引數的數量。
ARGV陣列的數量和ARGC的值只有在awk剛開始執行的時候是保證相等的。
$ awk -va=1 -F: '
BEGIN{
print ARGC;
for(i in ARGV){
print "ARGV[" i "]= " ARGV[i]
}
}' b=3 a.txt b.txt
4
ARGV[0]= awk
ARGV[1]= b=3
ARGV[2]= a.txt
ARGV[3]= b.txt
awk讀取檔案是根據ARGC的值來進行的,有點類似於如下虛擬碼形式:
while(i=1;i<ARGC;i++){
read from ARGV[i]
}
預設情況下,awk在讀完ARGV中的一個檔案時,會自動從它的下一個元素開始讀取,直到讀完所有檔案。
直接減小ARGC的值,會導致awk不會讀取尾部的一些檔案。此外,增減ARGC的值,都不會影響ARGV陣列,僅僅只是影響awk讀取檔案的數量。
# 不會讀取b.txt
awk 'BEGIN{ARGC=2}{print}' a.txt b.txt
# 讀完b.txt後自動退出
awk 'BEGIN{ARGC=5}{print}' a.txt b.txt
可以將ARGV中某個元素賦值為空字串"",awk在選擇下一個要讀取的檔案時,會自動忽略ARGV中的空字串元素。
也可以delete ARGV[i]
的方式來刪除ARGV中的某元素。
使用者手動增、刪ARGV元素時,不會自動修改ARGC,而awk讀取檔案時是根據ARGC值來確定的。所以,在增加ARGV元素之後,要手動的去增加ARGC的值。
# 不會讀取b.txt檔案
$ awk 'BEGIN{ARGV[2]="b.txt"}{print}' a.txt
# 會讀取b.txt檔案
$ awk 'BEGIN{ARGV[2]="b.txt";ARGC++}{print}' a.txt
awk命令列中可能會給出一些不存在或無許可權或其它原因而無法被awk讀取的檔名,這時可以判斷並從中剔除掉不可讀取的檔案。
BEGIN{
for(i=1;i<ARGC;i++){
if(ARGV[i] ~ /[a-zA-Z_][a-zA-Z0-9_]*=.*/ \
|| ARGV[i]=="-" || ARGV[i]=="/dev/stdin"){
continue
} else if((getline var < ARGV[i]) < 0){
delete ARGV[i]
} else{
close(ARGV[i])
}
}
}
可以定義一個函數將多個操作整合在一起。函數定義之後,可以到處多次呼叫,從而方便複用。
使用function關鍵字來定義函數:
function func_name([parameters]){
function_body
}
對於gawk來說,也支援func關鍵字來定義函數。
func func_name(){}
函數可以定義在下面使用下劃線的地方:
awk '_ BEGIN{} _ MAIN{} _ END{} _'
無論函數定義在哪裡,都能在任何地方呼叫,因為awk在BEGIN之前,會先編譯awk程式碼為內部格式,在這個階段會將所有函數都預定義好。
例如:
awk '
BEGIN{
f()
f()
f()
}
function f(){
print "星期一"
print "星期二"
print "星期三"
print "星期四"
print "星期五"
print "星期六"
print "星期日"
}
'
如果想要讓函數有返回值,那麼需要在函數中使用return語句。
return語句也可以用來立即結束函數的執行。
例如:
awk '
function add(){
return 40
}
BEGIN{
print add()
res = add()
print res
}
'
如果不使用return或return沒有引數,則返回值為空,即空字串。
awk '
function f1(){ }
function f2(){return }
function f3(){return 3}
BEGIN{
print "-"f1()"-"
print "-"f2()"-"
print "-"f3()"-"
}
'
為了讓函數和呼叫者能夠進行資料的互動,可以使用引數。
awk '
function f(a,b){
print a
print b
return a+b
}
BEGIN{
x=10
y=20
res = f(x,y)
print res
print f(x,y)
}
'
例如,實現一個重複某字串指定次數的函數:
awk '
function repeat(str,cnt ,res_str){
for(i=0;i<cnt;i++){
res_str = res_str""str
}
return res_str
}
BEGIN{
print repeat("abc",3)
print repeat("-",30)
}
'
呼叫函數時,實引數量可以比形引數量少,也可以比形引數量多。但是,在多於形引數量時會給出警告資訊。
awk '
function f(a,b){
print a
print b
return a+b
}
BEGIN{
x=10
y=20
print "---1----"
print "-"f()"-" # 不傳遞引數
print "---2----"
print "-"f(30)"-" # 傳遞1個引數
print "---3----"
print "-"f(10,20,30)"-" # 傳遞多個引數
}
'
如果函數內部使用引數的型別和函數外部變數的型別不一致,會出現資料型別不同而導致報錯。
awk '
function f(a){
a[1]=30
}
BEGIN{
a="hello world"
f(a) # 報錯
f(x)
x=10 # 報錯
}
'
函數內部引數對應的是陣列,那麼外面對應的也必須是陣列型別。
在呼叫函數時,將資料作為函數引數傳遞給函數時,有兩種傳遞方式:
# 傳遞普通變數:按值拷貝
awk '
function modify(a){
a=30
print a
}
BEGIN{
a=40
modify(a)
print a
}
'
# 傳遞陣列:按參照拷貝
awk '
function modify(a){
a[1]=20
}
BEGIN{
a[1]=10
modify(a)
print a[1]
}
'
awk只有在函數引數中才是區域性變數,其它地方定義的變數均為全域性變數。
函數內部新增的變數是全域性變數,會影響到全域性,所以在函數退出後仍然能存取。例如上面的e變數。
awk '
function f(){
a=30 # 新增的變數,是全域性變數
print "in f: " a
}
BEGIN{
a=40
f()
print a # 30
}
'
函數引數會遮掩全域性同名變數,所以在函數執行時,無法存取到或操作與引數同名的全域性變數,函數退出時會自動撤掉遮掩,這時才能存取全域性變數。所以,引數具有區域性效果。
awk '
function f(a){
print a # 50,按值拷貝,和全域性a已經沒有關係
a=40
print a # 40
}
BEGIN{
a=50
f(a)
print a # 50,函數退出,重新存取全域性變數
}
'
由於函數內部新增變數均為全域性變數,awk也沒有提供關鍵字來修飾一個變數使其成為區域性變數。所以,awk只能將本該出現在函數體內的區域性變數放在參數列中,只要呼叫函數時不要為這些引數傳遞資料即可,從而實現區域性變數的效果。
awk '
function f(a,b ,c,d){
# a,b是引數,呼叫時需傳遞兩個引數
# c,d是區域性變數,呼叫時不要給c和d傳遞資料
a=30
b=40
c=50
d=60
e=70 # 全域性變數
print a,b,c,d,e # 30 40 50 60 70
}
BEGIN{
a=31
b=41
c=51
d=61
f(a,b) # 呼叫函數時值傳遞兩個引數
print a,b,c,d,e # 31 41 51 61 70
}
'
所以,awk對函數參數列做了兩類區分:
local variables是awk實現真正區域性變數的技巧,只是因為函數內部新增的變數都是全域性變數,所以退而求其次將其放在參數列上來實現區域性變數。
function readfile(file ,rs_bak,data){
rs_bak=RS
RS="^$"
if ( (getline data < file) < 0 ){
print "read file failed"
exit 1
}
close(file)
RS=rs_bak
return data
}
/^1/{
print $0
content = readfile("c.txt")
print content
}
將RS設定為^$
是永遠不可能出現的分隔符,除非這個檔案為空檔案。
實現一個rewind()功能來重置檔案偏移指標,從而模擬實現重讀當前檔案。
function rewind( i){
# 將當前正在讀取的檔案新增到ARGV中當前檔案的下一個元素
for(i=ARGC;i>ARCIND;i--){
ARGV[i] = ARGV[i-1]
}
# 隨著增加ARGC,以便awk能夠讀取到因ARGV增加元素後的最後一個檔案
ARGC++
# 直接進入下一個檔案
nextfile
}
要注意可能出現無限遞迴的場景:
awk -f rewind.awk 'NR==3{rewind()}{print FILENAME, FNR, $0}' a.txt
# 下面這個會無限遞迴,因為FNR==3很可能每次重讀時都會為真
awk -f rewind.awk 'FNR==3{rewind()}{print FILENAME, FNR, $0}' a.txt
實現一個a2s()函數。
BEGIN{
arr["zhangsan"]=21
arr["lisi"]=22
arr["wangwu"]=23
print a2s(arr)
}
function a2s(arr ,content,i,cnt){
for(i in arr){
if(cnt){
content=content""(sprintf("\t%s:%s\n",i,arr[i]))
} else {
content=content""(sprintf("\n\t%s:%s\n",i,arr[i]))
}
cnt++
}
return "{"content"}"
}
awk '{}' ./a=b a.txt
中,a=b
會被awk識別為變數賦值操作。但是,如果使用者想要處理的正好是包含了等號的檔名,則應當去禁用該賦值操作。
禁用的方式很簡單,只需為其加上一個路徑字首./
即可。
為了方便控制,可通過-v
設定一個flag型別的選項標記。
function disable_assigns(argc,argv, i){
for(i=1;i<argc;i++){
if(argv[i] ~ /[[:alpha:]_][[:alnum:]_]*=.*/){
argv[i] = ("./"argv[i])
}
}
}
BEGIN{
if(assign_flag){
disable_assigns(ARGC,ARGV)
}
}
那麼,呼叫awk時採用如下方式:
awk -v assign_flag=1 -f assigns.awk '{print}' a=b.txt a.txt
-e program-text
--source program-text
指定awk程式表示式,可結合-f選項同時使用
在使用了-f選項後,如果不使用-e,awk program是不會執行的,它會被當作ARGV的一個引數
-f program-file
--file program-file
從檔案中讀取awk原始碼來執行,可指定多個-f選項
-F fs
--field-separator fs
指定輸入欄位分隔符(FS預定義變數也可設定)
-n
--non-decimal-data
識別檔案輸入中的8進位制數(0開頭)和16進位制數(0x開頭)
echo '030' | awk -n '{print $1+0}'
-o [filename]
格式化awk程式碼。
不指定filename時,則預設儲存到awkprof.out
指定為`-`時,表示輸出到標準輸出
-v var=val
--assign var=val
在BEGIN之前,宣告並賦值變數var,變數可在BEGIN中使用
預定義變數分為兩類:控制awk工作的變數和攜帶資訊的變數。
第一類:控制AWK工作的預定義變數
RS
:輸入記錄分隔符,預設為換行符\n
,參考RSIGNORECASE
:預設值為0,表示所有的正則匹配不忽略大小寫。設定為非0值(例如1),之後的匹配將忽略大小寫。例如在BEGIN塊中將其設定為1,將使FS、RS都以忽略大小寫的方式分隔欄位或分隔recordFS
:讀取記錄後,劃分為欄位的欄位分隔符。參考FSFIELDWIDTHS
:以指定寬度切割欄位而非按照FS。參考FIELDWIDTHSFPAT
:以正則匹配匹配到的結果作為欄位,而非按照FS劃分。參考FPATOFS
:print命令輸出各欄位列表時的輸出欄位分隔符,預設為空格" "ORS
:print命令輸出資料時在尾部自動新增的記錄分隔符,預設為換行符\n
CONVFMT
:在awk中數值隱式轉換為字串時,將根據CONVFMT的格式按照sprintf()的方式自動轉換為字串。預設值為"%.6gOFMT
:在print中,數值會根據OFMT的格式按照sprintf()的方式自動轉換為字串。預設值為"%.6g第二類:攜帶資訊的預定義變數
ARGC
和ARGV
:awk命令列引數的數量、命令引數的陣列。參考ARGC和ARGVARGIND
:awk當前正在處理的檔案在ARGV中的索引位置。所以,如果awk正在處理命令列引數中的某檔案,則ARGV[ARGIND] == FILENAME
為真FILENAME
:awk當前正在處理的檔案(命令列中指定的檔案),所以在BEGIN中該變數值為空ENVIRON
:儲存了Shell的環境變數的陣列。例如ENVIRON["HOME"]
將返回當前使用者的家目錄NR
:當前已讀總記錄數,多個檔案從不會重置為0,所以它是一直疊加的
FNR
:當前正在讀取檔案的第幾條記錄,每次開啟新檔案會重置為0
NF
:當前記錄的欄位數,參考NFRT
:在讀取記錄時真正的記錄分隔符,參考RTRLENGTH
:match()函數正則匹配成功時,所匹配到的字串長度,如果匹配失敗,該變數值為-1RSTART
:match()函數匹配成功時,其首字元的索引位置,如果匹配失敗,該變數值為0SUBSEP
:arr[x,y]
中下標分隔符構建成索引時對應的字元,預設值為\034
,是一個不太可能出現在字串中的不可列印字元。參考複雜陣列預定義函數分為幾類:
int(expr) 截斷為整數:int(123.45)和int("123abc")都返回123,int("a123")返回0
sqrt(expr) 返回平方根
rand() 返回[0,1)之間的亂數,預設使用srand(1)作為種子值
srand([expr]) 設定rand()種子值,省略引數時將取當前時間的epoch值(精確到秒的epoch)作為種子值
例如:
$ awk 'BEGIN{srand();print rand()}'
0.0379114
$ awk 'BEGIN{srand();print rand()}'
0.0779783
$ awk 'BEGIN{srand(2);print rand()}'
0.893104
$ awk 'BEGIN{srand(2);print rand()}'
0.893104
生成[10,100]
之間的隨機整數。
awk 'BEGIN{srand();print 10+int(91*rand())}'
注意,awk中涉及到字元索引的函數,索引位都是從1開始計算,和其它語言從0開始不一樣。
sprintf(format, expression1, ...)
:返回格式化後的字串,參考sprintf
a=sprintf("%s\n","abc")
length()
:返回字串字元數量、陣列元素數量、或數值轉換為字串後的字元數量
awk '
BEGIN{
print length(1.23) # 4 # CONVFMT %.6g
print 1.234567 # 1.23457
print length(1.234567) # 7
print length(122341223432.1213241234) # 11
}'
strtonum(str)
:將字串轉換為十進位制數值
tolower(str)
:轉換為小寫
toupper(str)
:轉換為大寫
index(str,substr)
:從str中搜尋substr(子串),返回搜尋到的索引位置(索引從1開始),搜尋不到則返回0
substr(string,start[,length])
:從string中擷取子串start是擷取的起始索引位(索引位從1開始而非0),length表示擷取的子串長度。如果省略length,則表示從start開始擷取剩餘所有字元。
awk '
BEGIN{
str="abcdefgh"
print substr(str,3) # cdefgh
print substr(str,3,3) # cde
}
'
如果start值小於1,則將其看作為1對待,如果start大於字串的長度,則返回空字串。
如果length小於或等於0,則返回空字串。
split(string, array [, fieldsep [, seps ] ])
:將字串分割後儲存到陣列array中,陣列索引從1開始儲存。並返回分割得到的元素個數其中fieldsep指定分隔符,可以是正規表示式方式的。如果不指定該引數,則預設使用FS作為分隔符,而FS的預設值又是空格。
seps是一個陣列,儲存了每次分割時的分隔符。
例如:
split("abc-def-gho-pq",arr,"-",seps)
其返回值為4。同時得到的陣列a和seps為:
arr[1] = "abc"
arr[2] = "def"
arr[3] = "gho"
arr[4] = "pq"
seps[1] = "-"
seps[2] = "-"
seps[3] = "-"
split在開始工作時,會先清空陣列,所以,將split的string引數設定為空,可以用於清空陣列。
awk 'BEGIN{arr[1]=1;split("",arr);print length(arr)}' # 0
如果分隔符無法匹配字串,則整個字串當作一個陣列元素儲存到陣列array中。
awk 'BEGIN{split("abcde",arr,"-");print arr[1]}' # abcde
patsplit(string, array [, fieldpat [, seps ] ])
:用正規表示式fieldpat匹配字串string,將所有匹配成功的部分儲存到陣列array中,陣列索引從1開始儲存。返回值是array的元素個數,即匹配成功了多少次如果省略fieldpat,則預設採用預定義變數FPAT的值。
awk '
BEGIN{
patsplit("abcde",arr,"[a-z]")
print arr[1] # a
print arr[2] # b
print arr[3] # c
print arr[4] # d
print arr[5] # e
}
'
match(string,reg[,arr])
:使用reg匹配string,返回匹配成功的索引位(從1開始計數),匹配失敗則返回0。如果指定了arr引數,則arr[0]儲存的是匹配成功的字串,arr[1]、arr[2]、...儲存的是各個分組捕獲的內容match匹配時,同時會設定兩個預定義變數:RSTART和RLENGTH
例如:
awk '
BEGIN{
where = match("foooobazbarrrr","(fo+).*(bar*)",arr)
print where # 1
print arr[0] # foooobazbarrrr
print arr[1] # foooo
print arr[2] # barrrr
print RSTART # 1
print RLENGTH # 14
}
'
因為match()匹配成功時返回值為非0,而匹配失敗時返回值為0,所以可以直接當作條件判斷:
awk '
{
if(match($0,/A[a-z]+/,arr)){
print NR " : " arr[0]
}
}
' a.txt
sub(regexp, replacement [, target])
gsub(regexp, replacement [, target])
:sub()的全域性模式sub()從字串target中進行正則匹配,並使用replacement對第一次匹配成功的部分進行替換,替換後儲存回target中。返回替換成功的次數,即0或1。
target必須是一個可以賦值的變數名、$N或陣列元素名,以便用它來儲存替換成功後的結果。不能是字串字面量,因為它無法儲存資料。
如果省略target,則預設使用$0
。
需要注意的是,如果省略target,或者target是$N
,那麼替換成功後將會使用OFS重新計算$0
。
awk '
BEGIN{
str="water water everywhere"
#how_many = sub(/at/, "ith", str)
how_many = gsub(/at/, "ith", str)
print how_many # 1
print str # wither water everywhere
}
'
在replacement引數中,可以使用一個特殊的符號&
來參照匹配成功的部分。注意sub()和gsub()不能在replacement中使用反向參照\N
。
awk '
BEGIN{
str = "daabaaa"
gsub(/a+/,"C&C",str)
print str # dCaaCbaaa
}
'
如果想要在replacement中使用&
純字元,則跳脫即可。
sub(/a+/,"C\\&C",str)
兩根反斜線:
因為awk在正則開始工作時,首先會掃描所有awk程式碼然後編譯成awk的內部格式,掃描期間會解析反斜線跳脫,使得\\
變成一根反斜線。當真正開始執行後,sub()又要解析,這時\&
才表示的是對&做跳脫。
掃描程式碼階段稱為詞法解析階段,執行解析階段稱為執行時解析階段。
gawk支援的gensub(),完全可以取代sub()和gsub()。
gensub(regexp, replacement, how [, target])
:可以替代sub()和gsub()。
how指定替換第幾個匹配,例如指定為1表示只替換第一個匹配。此外,還可以指定為g
或G
開頭的字串,表示全域性替換。
awk 'BEGIN{
a = "abc def"
b = gensub(/(.+) (.*)/, "\\2 \\1, \\0 , &", "g", a)
print b # def abc, abc def , abc def
}'
asort(src,[dest [,how]])
asorti(src,[dest [,how]])
asort對陣列src的值進行排序,然後將排序後的值的索引改為1、2、3、4...序列。返回src中的元素個數,它可以當作排序後的索引最大值。
asorti對陣列src的索引進行排序,然後將排序後的索引值的索引改為1、2、3、4...序列。返回src中的元素個數,它可以當作排序後的索引最大值。
arr["last"] = "de"
arr["first"] = "sac"
arr["middle"] = "cul"
asort(arr)得到:
arr[1] = "cul"
arr[2] = "de"
arr[3] = "sac"
asorti(arr)得到:
arr[1] = "first"
arr[2] = "last"
arr[3] = "middle"
如果指定dest,則將原始陣列src備份到dest,然後對dest進行排序,而src保持不變。
how引數用於指定排序時的方式,其值指定方式和PROCINFO["sorted_in"]
一致:可以是預定義的排序函數,也可以是使用者自定義的排序函數。參考指定陣列遍歷順序。
close(filename [, how])
:關閉檔案或命令,參考closesystem(command)
:執行Shell命令,參考systemfflush([filename])
:gawk會按塊緩衝模式來緩衝輸出結果,使用fflush()會將緩衝資料刷出從gawk 4.0.2之後的版本(不包括4.0.2),無引數fflush()將刷出所有緩衝資料。
此外,終端裝置是行緩衝模式,此時不需要fflush,而重定向到檔案、到管道都是塊緩衝模式,此時可能需要fflush()。
此外,system()在執行時也會flush gawk的緩衝。特別的,如果system的引數為空字串system("")
,則它不會去啟動一個shell子程序而是僅僅執行flush操作。
使用system()來flush:
awk '{print "first";system("echo system");print "second"}' | cat
awk '{print "first";system("");print "second"}' | cat
也可以使用stdbuf -oL
命令來強制gawk按行緩衝而非預設的按塊緩衝。
stdbuf -oL awk '{print "first";print "second"}' | cat
fflush()也可以指定檔名或命令,表示只刷出到該檔案或該命令的緩衝資料。
# 刷出所有流向到標準輸出的緩衝資料
awk '{print "first";fflush("/dev/stdout");print "second"}' | cat
最後注意,fflush()刷出緩衝資料不代表傳送EOF標記。
isarray(var)
:測試var是否是陣列,返回1(是陣列)或0(不是陣列)typeof(var)
:返回var的資料型別,有以下可能的值:
@/a.*ef/
例如,輸出awk程序的內部資訊,但跳過陣列
awk '
BEGIN{
for(idx in PROCINFO){
if(typeof(PROCINFO[idx]) == "array"){
continue
}
print idx " -> "PROCINFO[idx]
}
}'
awk常用於處理紀錄檔,它支援簡單的時間類操作。有下面3個內建的時間函數:
mktime("YYYY MM DD HH mm SS [DST]")
:構建一個時間,返回這個時間點的秒級epoch,構建失敗則返回-1systime()
:返回當前系統時間點,返回的是秒級epoch值strftime([format [, timestamp [, utc-flag] ] ])
:將時間按指定格式轉換為字串並返回轉的結果字串注意,awk構建時間時都是返回秒級的epoch值,表示從1970-01-01 00:00:00
開始到指定時間已經過的秒數。
awk 'BEGIN{print systime();print mktime("2019 2 29 12 32 59")}'
1572364974
1551414779
mktime在構建時間時,如果傳遞的DD給定的值超出了月份MM允許的天數,則自動延申到下個月。例如,指定"2019 2 29 12 30 59"中2月只有28號,所以構建出來的時間是2019-03-01 12:30:59
。
此外,其它部分也不限定必須在範圍內。例如,2019 2 23 12 32 65
的秒超出了59,那麼多出來的秒數將進位到分鐘。
awk 'BEGIN{
print mktime("2019 2 23 12 32 65") | "xargs -i date -d@{} +\"%F %T\""
}'
2019-02-23 12:33:05
如果某部位的數值為負數,則表示在此時間點基礎上減幾。例如:
# 2019-02-23 12:00:59基礎上減1分鐘
$ awk 'BEGIN{print mktime("2019 2 23 12 -1 59") | "xargs -i date -d@{} +\"%F %T\""}'
2019-02-23 11:59:59
# 2019-02-23 00:32:59基礎上減1小時
$ awk 'BEGIN{print mktime("2019 2 23 -1 32 59") | "xargs -i date -d@{} +\"%F %T\""}'
2019-02-22 23:32:59
strftime([format [, timestamp [, utc-flag] ] ])
將指定的時間戳tiemstamp按照給定格式format轉換為字串並返回這個字串。
如果省略timestamp,則對當前系統時間進行格式化。
如果省略format,則採用PROCINFO["strftime"]的格式,其預設格式為%a %b %e %H:%M%:S %Z %Y
。該格式對應於Shell命令date
的預設輸出結果。
$ awk 'BEGIN{print strftime()}'
Wed Oct 30 00:20:01 CST 2014
$ date
Wed Oct 30 00:20:04 CST 2014
$ awk 'BEGIN{print strftime(PROCINFO["strftime"], systime())}'
Wed Oct 30 00:24:00 CST 2014
支援的格式包括:
%a 星期幾的縮寫,如Mon、Sun Wed Fri
%A 星期幾的英文全名,如Monday
%b 月份的英文縮寫,如Oct、Sep
%B 月份的英文全名,如February、October
%C 2位數的世紀,例如1970對應的世紀是19
%y 2位數的年份(00–99),通過年份模以100取得,例如2019/100的餘數位19
%Y 四位數年份(如2015)
%m 月份(01–12)
%j 年中天(001–366)
%d 月中天(01–31)
%e 空格填充的月中天
%H 24小時制的小時(00–23)
%I 12小時制的小時(01–12)
%p 12小時制時的AM/PM
%M 分鐘數(00–59)
%S 秒數(00–60)
%u 數值的星期幾(1–7),1表示星期一
%w 數值的星期幾(0–6),0表示星期日
%W 年中第幾周(00–53)
%z 時區偏移,格式為"+HHMM",如"+0800"
%Z 時區偏移的英文縮寫,如CST
%k 24小時制的時間(0-23),1位數的小時使用空格填充
%l 12小時制的時間(1-12),1位數的小時使用空格填充
%s 秒級epoch
##### 特殊符號
%n 換行符
%t 製表符
%% 百分號%
##### 等價寫法:
%x 等價於"%A %B %d %Y"
%F 等價於"%Y-%m-%d",用於表示ISO 8601日期格式
%T 等價於"%H:%M:%S"
%X 等價於"%T"
%r 12小時制的時間部分格式,等價於"%I:%M:%S %p"
%R 等價於"%H:%M"
%c 等價於"%A %B %d %T %Y",如Wed 30 Oct 2015 12:34:48 AM CST
%D 等價於"%m/%d/%y"
%h 等價於"%b"
例如:
$ awk 'BEGIN{print strftime("%s", mktime("2077 11 12 10 23 32"))}'
3403909412
$ awk 'BEGIN{print strftime("%F %T %Z", mktime("2077 11 12 10 23 32"))}'
2077-11-12 10:23:32 CST
$ awk 'BEGIN{print strftime("%F %T %z", mktime("2077 11 12 10 23 32"))}'
2077-11-12 10:23:32 +0800
例如:
2019-11-11T03:42:42+08:00
1.將日期時間字串中的年月日時分秒全都單獨儲存起來
2.將年月日時分秒構建成mktime()的字串格式"YYYY MM DD HH mm SS"
3.使用mktime()可以構建出時間點
function strptime(str, time_str,arr,Y,M,D,H,m,S){
time_str = gensub(/[-T:+]/," ","g",str)
split(time_str, arr, " ")
Y = arr[1]
M = arr[2]
D = arr[3]
H = arr[4]
m = arr[5]
S = arr[6]
# mktime失敗返回-1
return mktime(sprintf("%d %d %d %d %d %d", Y,M,D,H,m,S))
}
BEGIN{
str = "2019-11-11T03:42:42+08:00"
print strptime(str)
}
下面是更難一點的,月份使用的是英文或英文縮寫,日期時間分隔符也比較特殊。
Sat 26. Jan 15:36:24 CET 2013
function strptime(str, time_str,arr,Y,M,D,H,m,S){
time_str = gensub(/[.:]+/, " ", "g", str)
split(time_str, arr, " ")
Y = arr[8]
M = month_map(arr[3])
D = arr[2]
H = arr[4]
m = arr[5]
S = arr[6]
return mktime(sprintf("%d %d %d %d %d %d", Y,M,D,H,m,S))
}
function month_map(str, mon){
# mon = substr(str,1,3)
# return (((index("JanFebMarAprMayJunJelAugSepOctNovDec", mon)-1)/3)+1)
mon["Jan"] = 1
mon["Feb"] = 2
mon["Mar"] = 3
mon["Apr"] = 4
mon["May"] = 5
mon["Jun"] = 6
mon["Jul"] = 7
mon["Aug"] = 8
mon["Sep"] = 9
mon["Oct"] = 10
mon["Nov"] = 11
mon["Dec"] = 12
return mon[str]
}
BEGIN{
str = "Sat 26. Jan 15:36:24 CET 2013"
print strptime(str)
}
使用擴充套件的方式:
awk -l ext_name 'BEGIN{}{}END{}'
awk '@load "ext_name";BEGIN{}{}END{}'
awk和檔案相關的擴充套件是"filefuncs"。
它支援chdir()、stat()函數。
"fnmatch"擴充套件提供檔名通配。
@load "fnmatch"
result = fnmatch(pattern, string, flags)
awk通過載入inplace.awk,也可以實現sed -i
類似的功能,即內容直接修改原始檔(其本質是先寫入臨時檔案,寫完後將臨時檔案重新命名為原始檔進行覆蓋)。
例如:
"fork"擴充套件提供多程序相關功能。
@load "fork"
pid = fork()
建立一個子程序,對子程序返回值為0,對父程序返回值為子程序的PID,返回-1表示錯誤。
在子程序中,PROCINFO["pid"]和PROCINFO["ppid"]會隨之更新。
ret = waitpid(pid)
等待某個子程序退出。awk的waitpid是非阻塞的,如果等待的程序還未退出,則返回值為0,等待的程序已經退出,則返回該程序pid。
ret = wait()
等待任意一個子程序退出。wait()是阻塞的,必須等待到一個子程序退出,同時返回該子程序PID。
例如:
awk '
@load "fork"
BEGIN{
if( (pid=fork()) == 0 ){
print "Child Process"
print "CHILD PID: "PROCINFO["pid"]
print "CHILD PPID: "PROCINFO["ppid"]
system("sleep 1")
} else {
while(waitpid(pid) == 0){
system("sleep 1")
}
print "Parent PID: "PROCINFO["pid"]
print "Parent PPID: "PROCINFO["ppid"]
print "Parent Process"
}
}
'
"time"擴充套件提供了兩個函數。
@load "time"
the_time = gettimeofday()
獲取當前系統時間,以浮點數方式返回,精確的浮點小數位由作業系統決定
res = sleep(sec)
睡眠指定時間,可以是小數秒
$ awk '@load "time";BEGIN{printf "%.9f\n",gettimeofday()}'
1572422333.740148067
$ awk '@load "time";BEGIN{printf "%.19f\n",gettimeofday()}'
1572422391.5475890636444091797
睡眠是很好用的功能:
awk '@load "time";BEGIN{sleep(1.2);print "hello world"}'
Linux系列文章:https://www.cnblogs.com/f-ck-need-u/p/7048359.html
Shell系列文章:https://www.cnblogs.com/f-ck-need-u/p/7048359.html
網站架構系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html
MySQL/MariaDB系列文章:https://www.cnblogs.com/f-ck-need-u/p/7586194.html
Perl系列:https://www.cnblogs.com/f-ck-need-u/p/9512185.html
Go系列:https://www.cnblogs.com/f-ck-need-u/p/9832538.html
Python系列:https://www.cnblogs.com/f-ck-need-u/p/9832640.html
Ruby系列:https://www.cnblogs.com/f-ck-need-u/p/10805545.html
作業系統系列:https://www.cnblogs.com/f-ck-need-u/p/10481466.html
精通awk系列:https://www.cnblogs.com/f-ck-need-u/p/12688355.html