安裝新版本gawk awk有很多種版本,例如nawk、gawk。gawk是GNU awk,它的功能很豐富。 本教程採用的是gawk 4.2.0版本,4.2.0版本的gawk是一個比較大的改版,新支持的一些特性非常好用,而在低於4.2.0版本時這些語法可能會報錯。所以,請先安裝4.2.0版本或更高版本 ...
安裝新版本gawk
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 'awk_program' a.txt
- a.txt是awk要讀取的文件,可以是0個文件或一個文件,也可以多個文件
- 如果不給定任何文件,但又需要讀取文件,則表示從標準輸入中讀取
- 單引號包圍的是awk代碼,也稱為awk程式
- 儘量使用單引號,因為在awk中經常使用
$
符號,而$
符號在Shell是變數符號,如果使用雙引號包圍awk代碼,則$
符號會被Shell解析成Shell變數,然後進行Shell變數替換。使用單引號包圍awk代碼,則$
會脫離Shell的魔掌,使得$符號留給了awk去解析
- 儘量使用單引號,因為在awk中經常使用
- 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命令的執行過程:
BEGIN和END語句塊
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命令行結構和語法結構
awk命令行結構
awk [ -- ] program-text file ... (1)
awk -f program-file [ -- ] file ... (2)
awk -e program-text [ -- ] file ... (3)
其中:
awk語法結構
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}
可以直接連接連用 - action中多個語句如果寫在同一行,則需使用分號分隔
- pattern部分用於篩選行,action表示在篩選通過後執行的操作
- pattern和action都可以省略,其中:
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如何讀取文件
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為多個字元:將其當做正則表達式,只要匹配正則表達式的符號,都用來分割記錄
- 設置預定義變數IGNORECASE為非零值,正則匹配時表示忽略大小寫
- 相容模式下,只有首字元才生效,不會使用正則模式去分割記錄
特殊的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
兩種行號:NR和FNR
在讀取每條記錄之後,將其賦值給$0,同時還會設置NR、FNR、RT。
- NR:所有文件的行號計數器
- FNR:是各個文件的行號計數器
awk '{print NR}' a.txt a.txt
awk '{print FNR}' a.txt a.txt
詳細分析awk欄位分割
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
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
指定預定義變數FIELDWIDTHS按字元寬度分割欄位,這是gawk提供的高級功能。在處理某欄位缺失時非常好用。
用法:
FIELDWIDTHS="3 5 6 9"
表示第一個欄位3字元,第二欄位5字元...FIELDWIDTHS = "8 1:5 6 2:33"
表示:- 第一個欄位讀8個字元
- 然後跳過1個字元再讀5個字元作為第二個欄位
- 然後讀6個字元作為第三個欄位
- 然後跳過2個字元在讀33個字元作為第四個欄位(如果不足33個字元,則讀到結尾)
FIELDWIDTHS="2 3 *"
:- 第一個欄位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
劃分欄位方式(三):FPAT
FS是指定欄位分隔符,來取得除分隔符外的部分作為欄位。
FPAT是取得匹配的字元部分作為欄位。它是gawk提供的一個高級功能。
FPAT根據指定的正則來全局匹配record,然後將所有匹配成功的部分組成$1、$2...
,不會修改$0
。
awk 'BEGIN{FPAT="[0-9]+"}{print $3"-"}' a.txt
- 之後再設置FS或FPAT,該變數將失效
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...
}
修改欄位或NF值的聯動效應
註意下麵的分割和計算兩詞:分割表示使用FS(field Separator),計算表示使用預定義變數OFS(Output Field Separator)。
關於$0
當讀取一條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
awk數據篩選示例
篩選行
# 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
awk運維面試試題
從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}'
awk工作流程
參考自:man awk
的"AWK PROGRAM EXECUTION"段。
man --pager='less -p ^"AWK PROGRAM EXECUTION"' awk
執行步驟:
getline用法詳解
除了可以從標準輸入或非選項型參數所指定的文件中讀取數據,還可以使用getline從其它各種渠道獲取需要處理的數據,它的用法有很多種。
getline的返回值:
- 如果可以讀取到數據,返回1
- 如果遇到了EOF,返回0
- 如果遇到了錯誤,返回負數。如-1表示文件無法打開,-2表示IO操作需要重試(retry)。在遇到錯誤的同時,還會設置
ERRNO
變數來描述錯誤
為了健壯性,getline時強烈建議進行判斷。例如:
if( (getline) <= 0 ){...}
if((getline) < 0){...}
if((getline) > 0){...}
上面的getline的括弧儘量加上,因為getline < 0
表示的是輸入重定向,而不是和數值0進行小於號的比較。
無參數的getline
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
沒有參數的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
awk從指定文件中讀取數據
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。
awk從Shell命令輸出結果中讀取數據
cmd | getline
:從Shell命令cmd的輸出結果中讀取一條記錄保存到$0
中- 會進行欄位劃分,設置變數
$0 NF $N RT
,不會修改變數NR FNR
- 會進行欄位劃分,設置變數
cmd | getline var
:從Shell命令cmd的輸出結果中讀取數據保存到var中- 除了var和RT,其它變數都不會設置
如果要再次執行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中的coprocess
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()
close(filename)
close(cmd,[from | to]) # to參數只用於coprocess的第一個階段
如果close()關閉的對象不存在,awk不會報錯,僅僅只是讓其返回一個負數返回值。
close()有兩個基本作用:
- 關閉文件,丟棄已有的文件偏移指針
- 下次再讀取文件,將只能重新打開文件,重新打開文件會從文件的最開頭處開始讀取
- 發送EOF標記
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才會開始處理。
執行Shell命令system()
多數時候,使用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的行為,參考下文。
fflush()
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輸出數值
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
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()
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後臺非同步運行的管道。
stdin、stdout、stderr
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的變數是動態變數,在使用時聲明。
所以awk變數有3種狀態:
- 未聲明狀態:稱為untyped類型
- 引用過但未賦值狀態:unassigned類型
- 已賦值狀態
引用未賦值的變數,其預設初始值為空字元串或數值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中聲明變數的位置
awk中使用Shell變數
要在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版本中,還支持第三種基本的數據類型:正則表達式類型。
數據是什麼類型在使用它的上下文中決定:在字元串操作環境下將轉換為字元串,在數值操作環境下將轉換為數值。這和自然語言中的一個詞語、一個單詞在不同句子內的不同語義是一樣的。
隱式轉換:
- 算術加0操作可轉換為數值類型
"123" + 0
返回數值123" 123abc" + 0
轉換為數值時為123- 無效字元串將轉換成0,例如
"abc"+3
返回3
- 連接空字元串可轉換為字元串類型
123""
轉換為字元串"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
顯式轉換:
- 數值->字元串:
- CONVFMT或sprintf():功能等價。都是指定數值轉換為字元串時的格式
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)}'
- 字元串->數值:strtonum()
gawk 'BEGIN{a="123.4567";print strtonum(a)}' # 123.457
awk字面量
awk中有3種字面量:字元串字面量、數值字面量和正則表達式字面量。
數值字面量
- 整數、浮點數、科學計數
- 105、105.0、1.05e+2、1050e-1
- awk內部總是使用浮點數方式保存所有數值,但用戶在使用可以轉換成整數的數值時總會去掉小數點
- 數值12.0面向用戶的值為12,12面向awk內部的值是12.0000000...0
# 結果是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/
- 匹配結果返回值為0(匹配失敗)或1(匹配成功)
- 任何單獨出現的
/pattern/
都等價於$0 ~ /pattern/
if(/pattern/)
等價於if($0 ~ /pattern/)
- 坑1:
a=/pattern/
等價於將$0 ~ /pattern/
的匹配返回值(0或1)賦值給a - 坑2:
/pattern/ ~ $1
等價於$0 ~ /pattern/ ~ $1
,表示用$1
去匹配0或1 - 坑3:
/pattern/
作為參數傳給函數時,傳遞的是$0~/pat/
的結果0或1 - 坑4.坑5.坑6...
強類型的正則字面量(gawk 4.2.0才支持):
gawk支持的正則
. # 匹配任意字元,包括換行符
^
$
[...]
[^...]
|
+
*
?
()
{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布爾值
在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中比較操作
strnum類型
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...else
# 單獨的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...case
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和do...while
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迴圈
for (expr1; expr2; expr3) {
statement
}
for (idx in array) {
statement
}
break和continue
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和nextfile
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 [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數組特性:
- awk的數組是關聯數組(即key/value方式的hash數據結構),索引下標可為數值(甚至是負數、小數等),也可為字元串
- 在內部,awk數組的索引全都是字元串,即使是數值索引在使用時內部也會轉換成字元串
- awk的數組元素的順序和元素插入時的順序很可能是不相同的
- awk數組支持數組的數組
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數組長度
awk提供了len