getline用法詳解
在預設情況下,awk支援從檔案或者STDIN中讀取資料。我們也可以使用getline來靈活讀取資料,例如在main代碼塊執行過程中讀取某個非待處理檔案的資料,或者從某個讀取某個shell指令結果資料。
getline有傳回值:1:正确讀取到了資料。
0:讀取資料遇到EOF。
負數:讀取遇到了錯誤。-1表示檔案無法打開,-2表示IO操作需要重試。遇到錯誤時還會使用變量ERRNO來描述錯誤。
為了awk代碼的健壯性,在使用getline的時候,一般會加上條件判斷。if((getline)<0){...}
if((getline)<=0){...}
if((getline)>0){...}
記得将getline使用小括号包裹,否則getline<0會被識别為輸入重定向而不是大小判斷。
無參數的getline
getline無參數時表示立即從目前資料流(檔案或者STDIN)中讀取下一條記錄儲存至$0。做字段分割。然後從getline的位置繼續向後執行awk代碼。
此時getline會設定$0、位置參數($1...$NF)、NR、FNR和RT。# awk '/^1/{print;getline;print}' a.txt
1 Bob male 28 [email protected] 18023394012
2 Alice female 24 [email protected] 18084925203
10 Bruce female 27 [email protected] 13942943905
10 Bruce female 27 [email protected] 13942943905
記住,print省略參數表示print $0。從輸出結果來看,第4行比較詭異。因為Bruce那行已經是檔案的末尾,此時再getline會遇到EOF,傳回值為0。$0不做修改,依然是Bruce那行。是以Bruce那行輸出了兩次。
是以我們最好是對getline做條件判斷,增強代碼健壯性。# awk '/^1/{print;if((getline)<=0){exit};print}' a.txt
1 Bob male 28 [email protected] 18023394012
2 Alice female 24 [email protected] 18084925203
10 Bruce female 27 [email protected] 13942943905
awk中有另外一個指令類似于getline,叫next,我們先來看執行結果。# awk '/^1/{print;next;print}' a.txt
1 Bob male 28 [email protected] 18023394012
10 Bruce female 27 [email protected] 13942943905
遇到next以後,立即讀取下一條記錄,但是它不會像getline那樣從目前位置繼續往下執行代碼,而是會跳出目前的awk内部循環(類似于循環語句中的continue),重新執行一遍main代碼塊(即要重新比對pattern了)。由于需要重新比對pattern,是以第一次next取得Alice行就不符合pattern,第二次next已經遇到EOF,是以就結束了。
帶參數的getline
無參數的getline在擷取下一條記錄後将記錄指派給$0并劃分字段,而帶參數的getline帶的是一個參數,這個參數是一個變量。帶參數的getline在擷取下一條記錄後将記錄指派給參數變量并且不劃分字段。
是以,帶參數的getline隻會設定NR、FNR、RT和參數變量var,不會修改$0、位置參數和NF。# awk '/^1/{print;if((getline var)<=0){exit};print var;print $0;print $2}' a.txt
1 Bob male 28 [email protected] 18023394012
2 Alice female 24 [email protected] 180849252031 Bob male 28 [email protected] 18023394012
Bob10 Bruce female 27 [email protected] 13942943905
上面的輸出結果中,即使通過getline已經處理到了Alice那行,但是$0和$2依然是上一行Bob的資料。
再來一個例子對比帶參和無參getline的差別。[[email protected] awk]# awk '/Tony/{print;getline;print $0,$2}' a.txt
3 Tony male 21 [email protected] 17048792503
4 Kevin male 21 [email protected] 17023929033 Kevin
[[email protected] awk]# awk '/Tony/{print;getline var;print $0,$2}' a.txt
3 Tony male 21 [email protected] 17048792503
3 Tony male 21 [email protected] 17048792503 Tony
注意這裡我們為了簡便沒有對getline的傳回值做條件判斷。
從指定的檔案中getline
上面兩種getline的用法均是從目前處理的檔案中(假設沒有使用STDIN,因為情況較少)讀取下一條記錄,不過我們使用getline的情況一般是為了在處理目前檔案的過程中擷取其他檔案的資料進行處理。例如假設a.txt是配置檔案,在處理該檔案的過程中遇到了某些關鍵字需要追加另一個配置檔案c.txt的内容,這種情況是可能存在的。
無參getline從檔案中擷取資料:記錄儲存至$0,劃分字段(設定$N(即位置參數)),設定NF。由于是讀取其他檔案的資料,是以不設定NR和FNR。getline
從Coprocess中getline
中文協程,在英文中有兩種解釋,一種叫做Coroutine,另一種叫做Coprocess,它倆是不同的概念。
我們這裡說的awk的協程指的是Coprocess,有協助的程式之意。要解釋協程我們先來看bash中的1條指令。cmd1 | cmd2 | cmd3 ...
這個是bash的管道,管道之間的指令是同步執行的。而協程是異步執行的,形如管道。cmd1 |& cmd2cmd2 |& cmd3
這邊展示的是僞代碼,因為bash中實作協程使用的是bash内置指令coproc。“|&”是awk實作協程的符号。其中cmd2被稱作協程程式(coprocess)。
注意這種管道也叫做雙路管道(two-way pipe)。
協程的使用場景:雖然awk功能強大,但是某些功能不好用awk實作或者使用者更熟悉bash下其他的指令,那麼我們可以使用協程将資料由awk傳遞給協程處理,再由協程傳遞回awk。僞代碼如下。awkPrint "data" |& shellCmd
shellCmd |& getline [var]
例如,假設我們不懂awk中的substr()這個取子字元串的函數,那麼我們可以借助shell指令sed來取得郵箱字段的域名。
首先我們先确定sed指令。# echo "[email protected]" | sed -nr "s/.*@(.*)/\1/p"qq.com
代碼量比較多,是以寫成檔案使用-f選項調用。awk中的sed中的雙引号和反斜線需要使用轉義。# cat getlineCoprocSed.awk BEGIN {
CMD="sed -nr \"s/.*@(.*)/\\1/p\""}
NR>1{
print $5 |& CMD close(CMD,"to")
CMD |& getline email_domain close(CMD)
print email_domain
}# awk -f getlineCoprocSed.awk a.txt
qq.com
... ...
139.com
代碼中有兩處close函數需要引起我們的注意。我們先來看看第一個close()函數。print $5 |& CMD
close(CMD,"to")
close()函數的第二個參數的值如果是to,則表示關閉向協程寫入資料的管道,也可以了解為向協程寫入EOF。用來辨別我們已經向協程寫完了資料,協程中的指令可以開始執行了(對于該案例就是sed指令)。這麼做的原因是某些協程中的指令需要等待檔案内容全部準備好了才可以開始執行,例如sort排序指令,無論排序的規則是什麼,它想實作排序的前提條件就是要讀取完全部的資料才可以,而确定自己是否讀取完了檔案的全部資料就是看是否遇到了EOF。如果指令需要EOF而協程中又不存在的話,指令就會阻塞在那裡等待EOF。同學們可以自己嘗試注釋掉該close試看看。
再來看看第二個close()函數。CMD |& getline email_domain
close(CMD)
這裡的close()函數雖然沒有帶第二個參數,其實它是省略了from,因為它是預設參數,下面兩個是等價的。close(CMD)
close(CMD,"from")
它表示關閉從協程(coprocess)讀取資料的管道。如果資料寫入端的協程管道關閉了,資料讀取端的協程管道沒關閉,那麼這個管道就會存在,下次即便是相同的代碼也會繼續使用同一個管道。我們嘗試注釋掉getlineCoprocess.awk中的第二個close()函數就會遇到報錯。# awk -f getlineCoprocSed.awk a.txt
qq.com
awk: getlineCoprocSed.awk:6: (FILENAME=a.txt FNR=3) fatal: print: attempt to write to closed write end of two-way pipe
在NR==2時我們輸出了qq.com,但是遇到NR==3的時候,由于上一條記錄處理過程中我們沒有關閉掉讀取協程資料的管道導緻這個雙路管道依然存在,而這個管道的資料寫入端此前已經被我們關閉了,是以遇到了這樣的報錯。
是以正确使用協程雙路管道的方式是:向協程寫入資料完畢以後要關閉寫入端的管道(close(cmd,"to"))。
從協程讀取資料完畢以後要關閉讀取端的管道(close(cmd[,"from"]))。
我們再來看一個使用協程的例子。我們期望對a.txt檔案内容按照年齡字段進行排序,輸出的内容要是sort指令的輸出結果,但是我們必須使用awk指令。sort -k4n a.txt
思路:awk是我們的主程式。将sort指令作為協助程式。awk内部循環将第二行開始的每一行資料發送給協程。要在資料全部發送完畢後(END代碼塊)再對資料進行排序,然後再循環輸出排序後的資料。# cat getlineCoprocSort.awk BEGIN {
cmd="sort -k4n"}
NR==1 {
}
NR>1 {
print |& cmd
}
END {
close(cmd,"to") # 這裡需要close,否則協程sort會阻塞。
while(cmd |& getline){
}
close(cmd) # 這裡的close實測是可以不要的,因為剛好到了代碼的尾部了,不過強烈不建議養成這種壞習慣!
}
這裡還有一個知識點,我不太了解,但是還是列出。
如果協程中的cmd是按塊緩沖的,則需要将其改變成按行緩沖,否則getline會阻塞。cmd="cmdline"cmd="stdbuf -oL cmdline"
close()函數
在awk當中,使用getline從檔案或者指令結果中擷取資料,檔案/指令隻會在第一次getline時打開/執行。當檔案内容/指令結果有多條記錄時,getline每次僅擷取下一條記錄,想讓getline擷取多條記錄就需要使用循環。
由于getline的運作機制,當讀取完資料集(檔案的内容與指令的執行結果我稱之為資料集比較友善)的所有記錄後,getline的标記會一直停留在EOF處導緻同樣的檔案或者指令的資料集無法被getline重新擷取,要想重新擷取的話就必須關閉它。關閉資料集以後,下次使用資料集才會重新打開。close("file")
close("cmd")
在從coprocess中getline的情況下,會産生一個雙路管道(two-way pipe),一端向協程寫入資料,另一端從協程讀取資料。兩端都需要關閉。awkPrint "data" |& shellCmd # 使用close(shellCmd,"to")關閉。
shellCmd |& getline [var] # 使用close(shellCmd,"from")關閉,可簡寫close(shellCmd)。
通過system()函數執行shell指令
我們可以通過管道,将需要執行的shell指令print給shell解釋器來執行。# awk 'BEGIN{print "pwd" | "bash"}'
/root
# awk 'BEGIN{print "date" | "bash"}'Sat Jan 9 15:36:49 CST 2021
shell解釋器可以是sh、bash等,可以先絕對路徑也可以隻寫解釋器名稱。
我們也可以通過system()來執行shell指令。system()函數的傳回值是shell指令的退出狀态碼。通過system調用的shell指令也可以包含重定向、管道之類的複雜操作。# awk 'BEGIN{system("date +\"%F %T\"")}'
2021-01-09 15:40:14# awk 'BEGIN{system("date +\"%F %T\">/dev/null")}'# awk 'BEGIN{system("date +\"%F %T\"|cat")}'
2021-01-09 15:40:52
system()在開始運作前會flush出awk的緩沖區資料。如果shell指令是空的話,那麼system("")不會執行任何shell指令而隻會flush緩沖。這部分的概念請參考awk内置函數fflush()。