一篇文章玩透awk

来源:https://www.cnblogs.com/f-ck-need-u/archive/2023/11/03/17806881.html
-Advertisement-
Play Games

安裝新版本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

讀取文件的幾種方式

讀取文件有如下幾種常見的方式:

  1. 按字元數量讀取:每一次可以讀取一個字元,或者多個字元,直到把整個文件讀取完
  2. 按照分隔符進行讀取:一直讀取直到遇到了分隔符才停止,下次繼續從分隔的位置處向後讀取,直到讀完整個文件
  3. 按行讀取:每次讀取一行,直到把整個文件讀完
    • 它是按照分隔符讀取的一種特殊情況:將分隔符指定為了換行符\n
  4. 一次性讀取整個文件
    • 是按字元數量讀取的特殊情況
    • 也是按分隔符讀取的特殊情況
  5. 按位元組數量讀取:一次讀取指定數量的位元組數據,直到把文件讀完

下麵使用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示例:

# 輸出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

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作者:張富春(ahfuzhang),轉載時請註明作者和引用鏈接,謝謝! cnblogs博客 zhihu Github 公眾號:一本正經的瞎扯 近期在學習 golang plan9 彙編,總算基本做到了手寫彙編,並整理了很多筆記。 plan9 彙編的資料少,難學,難用。可能也有想學習彙編的人會遇到與我 ...
  • 鐵子們,分享一個開源組件安全檢索 免費工具,需要的自取~ 輸入組件名,一鍵查詢可以組件版本、來源、安全狀態、漏洞詳情和推薦版本、修複建議這些。 點這個鏈接註冊後直接就能用:組件安全檢索工具 一鍵查詢第三方組件版本、漏洞、所屬國家、所屬語言、源碼鏈接等: 查看漏洞詳情: 查看修複建議: 查看版本推薦和 ...
  • 歡迎訪問我的GitHub 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos 本篇概覽 本文是《Go語言基準測試(benchmark)三部曲》的第二篇,目標是掌握如何用基準測試來觀察被測方法的記憶體分配情況 今天除了常規的操作,即指定 ...
  • 一、定義: ValidatesOnDataErrors 是一種在 WPF 中實現數據校驗的方式,可以通過在 XAML 中設置屬性 ValidatesOnDataErrors 為 True 來啟用。 二、使用: ① 在 ViewModel 中實現 IDataErrorInfo 介面,該介面定義了兩個屬 ...
  • 前言 特點 成熟,穩定 消息持久化 靈活的消息路由 高性能,高可用性,可擴展性高 支持插件系統:RabbitMQ 具有豐富的插件系統,可以通過安裝插件來擴展其功能,例如管理界面、消息追蹤、消息轉換等。 ...
  • TerraMoursGPT V1.0 開發總結 TerraMoursGPT V1.0 是之前gpt項目基於TerraMours後端框架的重構,實現用戶登陸和基於SK的多語言模型聊天、基於chatgpt和SD的多模型圖片生成等功能。管理端實現數據看板、聊天記錄管理,圖片記錄管理、用戶管理、系統配置等。 ...
  • VPN介紹 VPN(Virtual Private Network)虛擬專用網路,是一種加密技術,用於再公共網路上建立一條安全的通道實現數據的傳輸。VPN的原理就是將數據進行加密,從而在不安全的網路上安全傳輸。 實現VPN的技術有很多,例如PPTP、IPsec、SSL\TLS。使用不同的技術,加密的 ...
  • Awk實戰案例精講 插入幾個新欄位 在"a b c d"的b後面插入3個欄位e f g。 echo a b c d|awk '{$3="e f g "$3}1' 格式化空白 移除每行的首碼、尾碼空白,並將各部分左對齊。 aaaa bbb ccc bbb aaa ccc ddd fff eee gg ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...