一篇文章玩透awk

駿馬金龍發表於2023-11-03

安裝新版本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   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   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   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18                  18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   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提供了length()函式來獲取陣列的元素個數,它也可以用於獲取字串的字元數量。還可以獲取數值轉換成字串後的字元數量。

awk 'BEGIN{arr[1]=1;arr[2]=2;print length(arr);print length("hello")}'

awk刪除陣列元素

  • 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

awk檢測是否是陣列

isarray(arr)可用於檢測arr是否是陣列,如果是陣列則返回1,否則返回0。

typeof(arr)可返回資料型別,如果arr是陣列,則其返回"array"。

awk 'BEGIN{
    arr[1]=1;
    print isarray(arr);
    print (typeof(arr) == "array")
}'

awk測試元素是否在陣列中

不要使用下面的方式來測試元素是否在陣列中:

if(arr["x"] != ""){...}

這有兩個問題:

  • 如果不存在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遍歷陣列

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複雜索引的陣列

在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]
      }
    }
  }
}

awk子陣列

子陣列是指陣列中的元素也是一個陣列,即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指定陣列遍歷順序

由於awk陣列是關聯陣列,預設情況下,for(idx in arr)遍歷陣列時順序是不可預測的。

但是gawk提供了PROCINFO["sorted_in"]來指定遍歷的元素順序。它可以設定為兩種型別的值:

  • 設定為使用者自定義函式
  • 設定為下面這些awk預定義好的值:
    • @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()來對陣列進行排序。

awk ARGC和ARGV

預定義變數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 ARGC和ARGV進行操刀

awk判斷命令列中給定檔案是否可讀

awk命令列中可能會給出一些不存在或無許可權或其它原因而無法被awk讀取的檔名,這時可以判斷並從中剔除掉不可讀取的檔案。

  1. 排除命令列尾部(非選項型引數)的var=val、-、和/dev/stdin這3種特殊情況
  2. 如果不可讀,則從ARGV中刪除該引數
  3. 剩下的都是可在main程式碼段正常讀取的檔案
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])
    }
  }
}

awk 自定義函式

可以定義一個函式將多個操作整合在一起。函式定義之後,可以到處多次呼叫,從而方便複用。

使用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 "星期日"
    }
'

awk 函式的return語句

如果想要讓函式有返回值,那麼需要在函式中使用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函式引數

為了讓函式和呼叫者能夠進行資料的互動,可以使用引數。

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函式引數資料型別衝突問題

如果函式內部使用引數的型別和函式外部變數的型別不一致,會出現資料型別不同而導致報錯。

awk '
    function f(a){
        a[1]=30
    }
    BEGIN{
        a="hello world"
        f(a)   # 報錯

        f(x)
        x=10   # 報錯
    }
'

函式內部引數對應的是陣列,那麼外面對應的也必須是陣列型別。

awk引數按值傳遞還是按引用傳遞

在呼叫函式時,將資料作為函式引數傳遞給函式時,有兩種傳遞方式:

# 傳遞普通變數:按值複製
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作用域問題

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對函式引數列表做了兩類區分:

  • arguments:呼叫函式時傳遞的引數
  • local variables:呼叫函式時省略的引數

local variables是awk實現真正區域性變數的技巧,只是因為函式內部新增的變數都是全域性變數,所以退而求其次將其放在引數列表上來實現區域性變數。

自定義函式示例

1.一次性讀取一個檔案所有資料

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設定為^$是永遠不可能出現的分隔符,除非這個檔案為空檔案。

2.重讀檔案

實現一個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

3.格式化陣列的輸出

實現一個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"}"
}

4.禁用命令列尾部的賦值語句

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

awk選項、內建變數

選項

-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,參考RS
  • IGNORECASE:預設值為0,表示所有的正則匹配不忽略大小寫。設定為非0值(例如1),之後的匹配將忽略大小寫。例如在BEGIN塊中將其設定為1,將使FS、RS都以忽略大小寫的方式分隔欄位或分隔record
  • FS:讀取記錄後,劃分為欄位的欄位分隔符。參考FS
  • FIELDWIDTHS:以指定寬度切割欄位而非按照FS。參考FIELDWIDTHS
  • FPAT:以正則匹配匹配到的結果作為欄位,而非按照FS劃分。參考FPAT
  • OFS:print命令輸出各欄位列表時的輸出欄位分隔符,預設為空格" "
  • ORS:print命令輸出資料時在尾部自動新增的記錄分隔符,預設為換行符\n
  • CONVFMT:在awk中數值隱式轉換為字串時,將根據CONVFMT的格式按照sprintf()的方式自動轉換為字串。預設值為"%.6g
  • OFMT:在print中,數值會根據OFMT的格式按照sprintf()的方式自動轉換為字串。預設值為"%.6g

第二類:攜帶資訊的預定義變數

  • ARGCARGV:awk命令列引數的數量、命令引數的陣列。參考ARGC和ARGV
  • ARGIND:awk當前正在處理的檔案在ARGV中的索引位置。所以,如果awk正在處理命令列引數中的某檔案,則ARGV[ARGIND] == FILENAME為真
  • FILENAME:awk當前正在處理的檔案(命令列中指定的檔案),所以在BEGIN中該變數值為空
  • ENVIRON:儲存了Shell的環境變數的陣列。例如ENVIRON["HOME"]將返回當前使用者的家目錄
  • NR:當前已讀總記錄數,多個檔案從不會重置為0,所以它是一直疊加的
    • 可以直接修改NR,下次讀取記錄時將在此修改值上自增
  • FNR:當前正在讀取檔案的第幾條記錄,每次開啟新檔案會重置為0
    • 可以直接修改FNR,下次讀取記錄時將在此修改值上自增
  • NF:當前記錄的欄位數,參考NF
  • RT:在讀取記錄時真正的記錄分隔符,參考RT
  • RLENGTH:match()函式正則匹配成功時,所匹配到的字串長度,如果匹配失敗,該變數值為-1
  • RSTART:match()函式匹配成功時,其首字元的索引位置,如果匹配失敗,該變數值為0
  • SUBSEParr[x,y]中下標分隔符構建成索引時對應的字元,預設值為\034,是一個不太可能出現在字串中的不可列印字元。參考複雜陣列

awk預定義內建函式

預定義函式分為幾類:

  • 數值類內建函式
  • 字串類內建函式
  • 時間類內建函式
  • 位操作內建函式
  • 資料型別相關內建函式:isarray()、typeof()
  • IO類內建函式:close()、system()、fflush()

awk數值類內建函式

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字串類內建函式

注意,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):將字串轉換為十進位制數值

    • 如果str以0開頭,則將其識別為8進位制
    • 如果str以0x或0X開頭,則將其識別為16進位制
  • tolower(str):轉換為小寫

  • toupper(str):轉換為大寫

  • index(str,substr):從str中搜尋substr(子串),返回搜尋到的索引位置(索引從1開始),搜尋不到則返回0

awk substr()

  • 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,則返回空字串。

awk split()和patsplit()

  • 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
    }
'

awk match()

  • match(string,reg[,arr]):使用reg匹配string,返回匹配成功的索引位(從1開始計數),匹配失敗則返回0。如果指定了arr引數,則arr[0]儲存的是匹配成功的字串,arr[1]、arr[2]、...儲存的是各個分組捕獲的內容

match匹配時,同時會設定兩個預定義變數:RSTART和RLENGTH

  • 匹配成功時:
    • RSTART賦值為匹配成功的索引位,從1開始計數
    • RLENGTH賦值為匹配成功的字元長度
  • 匹配失敗時:
    • RSTART賦值為0
    • RLENGTH賦值為-1

例如:

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

awk sub()和gsub()

  • 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()又要解析,這時\&才表示的是對&做轉義。
掃描程式碼階段稱為詞法解析階段,執行解析階段稱為執行時解析階段。

awk gensub()

gawk支援的gensub(),完全可以取代sub()和gsub()。

  • gensub(regexp, replacement, how [, target])

可以替代sub()和gsub()。

how指定替換第幾個匹配,例如指定為1表示只替換第一個匹配。此外,還可以指定為gG開頭的字串,表示全域性替換。

awk 'BEGIN{
    a = "abc def"
    b = gensub(/(.+) (.*)/, "\\2 \\1, \\0 , &", "g", a)
    print b  # def abc, abc def , abc def
}'

awk asort()和asorti()

  • 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"]一致:可以是預定義的排序函式,也可以是使用者自定義的排序函式。參考指定陣列遍歷順序

IO類內建函式

  • close(filename [, how]):關閉檔案或命令,參考close
  • system(command):執行Shell命令,參考system
  • fflush([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的資料型別,有以下可能的值:
    • "array":是一個陣列
    • "regexp":是一個真正表示式型別,強正則字面量才算是正則型別,如@/a.*ef/
    • "number":是一個number
    • "string":是一個string
    • "strnum":是一個strnum,參考strnum型別
    • "unassigned":曾引用過,但未賦值,例如"print f;print typeof(f)"
    • "untyped":從未引用過,也從未賦值過

例如,輸出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,構建失敗則返回-1
  • systime():返回當前系統時間點,返回的是秒級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

awk mktime()

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

awk strftime()

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

awk將字串轉換為時間:strptime1()

例如:

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)
}

awk將字串轉換為時間:strptime2()

下面是更難一點的,月份使用的是英文或英文縮寫,日期時間分隔符也比較特殊。

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)
}

幾個常見的gawk擴充套件

使用擴充套件的方式:

awk -l ext_name 'BEGIN{}{}END{}'
awk '@load "ext_name";BEGIN{}{}END{}'

1.檔案相關的擴充套件

awk和檔案相關的擴充套件是"filefuncs"。

它支援chdir()、stat()函式。

2.awk檔名匹配擴充套件

"fnmatch"擴充套件提供檔名通配。

@load "fnmatch"
result = fnmatch(pattern, string, flags)

3.awk原處修改檔案

awk透過載入inplace.awk,也可以實現sed -i類似的功能,即內容直接修改原始檔(其本質是先寫入臨時檔案,寫完後將臨時檔案重新命名為原始檔進行覆蓋)。

例如:

4.awk多程式擴充套件

"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"
        }
    }
'

5.awk日期時間擴充套件

"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"}'

相關文章