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 abc@qq.com 18023394012
2 Alice female 24 def@gmail.com 18084925203
10 Bruce female 27 bcbd@139.com 13942943905
10 Bruce female 27 bcbd@139.com 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 abc@qq.com 18023394012
2 Alice female 24 def@gmail.com 18084925203
10 Bruce female 27 bcbd@139.com 13942943905
awk中有另外一個指令類似於getline,叫next,我們先來看執行結果。
# awk '/^1/{print;next;print}' a.txt
1 Bob male 28 abc@qq.com 18023394012
10 Bruce female 27 bcbd@139.com 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 abc@qq.com 18023394012
2 Alice female 24 def@gmail.com 18084925203
1 Bob male 28 abc@qq.com 18023394012 Bob 10 Bruce female 27 bcbd@139.com 13942943905
上面的輸出結果中,即使通過getline已經處理到了Alice那行,但是$0和$2依然是上一行Bob的數據。
再來一個例子對比帶參和無參getline的區別。
[root@c7-server awk]# awk '/Tony/{print;getline;print $0,$2}' a.txt
3 Tony male 21 aaa@163.com 17048792503
4 Kevin male 21 bbb@189.com 17023929033 Kevin [root@c7-server awk]# awk '/Tony/{print;getline var;print $0,$2}' a.txt 3 Tony male 21 aaa@163.com 17048792503 3 Tony male 21 aaa@163.com 17048792503 Tony
注意這里我們為了簡便沒有對getline的返回值做條件判斷。
從指定的文件中getline
上面兩種getline的用法均是從當前處理的文件中(假設沒有使用STDIN,因為情況較少)讀取下一條記錄,不過我們使用getline的情況一般是為了在處理當前文件的過程中獲取其他文件的數據進行處理。例如假設a.txt是配置文件,在處理該文件的過程中遇到了某些關鍵字需要追加另一個配置文件c.txt的內容,這種情況是可能存在的。
無參getline從文件中獲取數據:記錄保存至$0,划分字段(設置$N(即位置參數)),設置NF。由於是讀取其他文件的數據,因此不設置NR和FNR。
getline < filename
# awk 'NR==5{print NR,FNR,$0,$2,NF;getline<"c.txt";print NR,FNR,$0,$2,NF}' a.txt
5 5 4 Kevin male 21 bbb@189.com 17023929033 Kevin 6
5 5 aaa bbb ccc ddd bbb 4
帶參getline從文件中獲取數據:記錄保存至變量var。$0、$N、NF、NR和FNR均不會設置。
getline var < filename
# awk 'NR==5{print NR,FNR,$0,$2,NF;getline var<"c.txt";print NR,FNR,$0,$2,NF}' a.txt
5 5 4 Kevin male 21 bbb@189.com 17023929033 Kevin 6
5 5 4 Kevin male 21 bbb@189.com 17023929033 Kevin 6
在使用getline獲取文件數據時,文件名稱需要使用雙引號包裹,使其不會被awk識別為變量。
awk 'BEGIN{getline<"c.txt";print $0}' a.txt
awk 'BEGIN{getline<c.txt;print $0}' a.txt
文件路徑可以拆解成目錄和文件名並保存於變量中,結合時要使用小括號調整優先級。
awk 'BEGIN{dir="/root/awk";file="c.txt";getline < dir"/"file;print $0}' a.txt
awk 'BEGIN{dir="/root/awk";file="c.txt";getline < (dir"/"file);print $0}' a.txt
上面的getline均只讀取了1條記錄,如果我們期望讀取整個文件的數據的話,應該使用循環。我們修改c.txt文件內容。
# cat c.txt
abc
def
ABC
DEF
讀取c.txt整個文件的內容。由於getline返回值的存在,當讀取到EOF的時候會返回0,此時循環就會自動停止。
# awk 'BEGIN{while(getline<"c.txt"){print $0}}' a.txt
abc
def
ABC
DEF
我們嘗試在打印第一條記錄后再次讀取並輸出c.txt。
# awk 'BEGIN{while(getline<"c.txt"){print $0}}NR==1{print $0;while(getline<"c.txt"){print $0}}' a.txt
abc
def
ABC
DEF
ID name gender age email phone
此時我們會發現第二次嘗試輸出c.txt失敗。原因在於每次我們getline c.txt就會讀取1條記錄返回,並在該記錄的尾部打上一個標記(類似於指針的指向)。
abc| # 第一次getline標記點
def| # 第二次getline標記點 ABC| # 第三次getline標記點 DEF| # 第四次getline標記點
BEGIN中的循環進行了4次,每次都在對應的位置做了標記,下一次getline從該位置讀取下一條記錄。因此BEGIN循環后,標記點就位於文件的EOF了,並不會因為讀取到EOF就將標記重新指向文件頭部,而是默認情況下一直處於該位置。main中的循環判斷中,由於第一次判斷就直接是EOF,因此循環體一次也不會執行。於是就出現了上面的輸出結果了。
這也可以理解為文件只在第一次getline時打開了,我們若想使標記重回文件頭部就需要重新打開該文件,即我們需要先關閉掉這個文件。我們需要使用到close()函數。
# awk 'BEGIN{while(getline<"c.txt"){print $0}{close("c.txt")}}NR==1{print $0;while(getline<"c.txt"){print $0}{close("c.txt")}}' a.txt
abc
def
ABC
DEF
ID name gender age email phone
abc
def
ABC
DEF
第二個close不加也不會影響輸出的結果,但是關閉getline曾經打開的文件是個好習慣,也避免了潛在的bug。
從shell命令結果中getline
"cmd" | getline
從shell命令cmd的結果中讀取1條記錄保存至$0,會進行字段的分割,因此會設置$0、$N、NF、RT。由於不是getline當前文件,因此不會設置NR和FNR。
"cmd" | getline var
從shell命令cmd的結果中讀取1條記錄保存至變量var。僅設置var和RT。
類似於從文件中getline,cmd必須使用雙引號包裹,shell命令的結果也可以理解為文件的數據,getline讀取完畢后要關閉。
# awk '/^1/{print;while("seq 1 5" | getline){print};{close("seq 1 5")}}' a.txt
1 Bob male 28 abc@qq.com 18023394012
1
2
3
4
5
10 Bruce female 27 bcbd@139.com 13942943905
1
2
3
4
5
shell命令一般比較長,而且至少要打開一次和關閉一次,可以將其保存至變量中,方便打開和關閉。shell命令中出現引號的話要適當使用轉義字符或者在條件允許的情況下交替使用引號。
# awk 'BEGIN{getDate="date +\"%F %T\""}/^1/{print;getDate|getline date;print date;close(getDate)}' a.txt
1 Bob male 28 abc@qq.com 18023394012
2021-01-08 10:22:05
10 Bruce female 27 bcbd@139.com 13942943905
2021-01-08 10:22:05
該示例中,date命令的雙引號使用反斜線轉義。此處不能使用單引號,否則會和包裹awk代碼的最外層單引號沖突。
shell命令本身也可以包含一些特殊字符,例如管道與重定向等。
awk 'BEGIN{cmd="seq 1 5|xargs -i echo x{}y 2>/dev/null"}/^1/{print;while(cmd|getline){print};close(cmd)}' a.txt
從Coprocess中getline
中文協程,在英文中有兩種解釋,一種叫做Coroutine,另一種叫做Coprocess,它倆是不同的概念。
我們這里說的awk的協程指的是Coprocess,有協助的程序之意。要解釋協程我們先來看bash中的1條命令。
cmd1 | cmd2 | cmd3 ...
這個是bash的管道,管道之間的命令是同步執行的。而協程是異步執行的,形如管道。
cmd1 |& cmd2
cmd2 |& 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 "abc@qq.com" | 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 { print } NR>1 { print |& cmd } END { close(cmd,"to") # 這里需要close,否則協程sort會阻塞。 while(cmd |& getline){ print } 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()。
