Linux文本處理三劍客之awk學習筆記11:選項、內置變量和內置函數

這部分的內容許多在以往的筆記中有涉獵,因此大多數不會詳述。

內置(built-in)和預定義(predefined)雖然名字不同,不過含義是等價的,官方文檔中也同時使用到這兩個英文詞彙。

選項

-e:指定awk代碼。一般代碼可以直接寫在CLI或者使用-f來指定代碼文件,不過這兩種只能二選一。如果已經使用-f指定了代碼文件還想在CLI中寫代碼的話,就得寫在-e中。

-e program-text
--source program-text
awk -f code.awk -e '...cliCode...' FILENAME

-f:指定awk的代碼文件,一般如果代碼內容較多不適合寫在CLI的情況下會寫在某個單獨的文件中。

-f source-file
--file source-file

-F:指定輸入字段分隔符。

-F fs
--field-separator fs

-n:默認情況下,awk會將來自輸入數據的數值均識別爲十進制,即使該數值以0或者0x開頭。而使用-n選項的話,可以根據數值的前綴自動識別爲八進制或者十六進制。

-n
--non-decimal-data
# echo "030" | awk '{print $1+0}'
30
# echo "030" | awk -n '{print $1+0}'
24

-o:將CLI中的awk代碼格式化後輸出到外部文件中。

-o[file]
--pretty-print[=file]
# awk -o 'NR==1{print}' a.txt
# cat awkprof.out
NR == 1 {
print $0
}

-v:變量賦值,在BEGIN代碼塊中可用。

-v var=val
--assign var=val

 

內置變量

內置變量大體可以分爲兩類:控制awk工作類和攜帶信息類。

控制awk工作類

RS:(輸入)記錄分隔符,默認爲換行符,詳見讀取文件

IGNORECASE:在正則匹配時是否忽略大小寫。要注意設置的位置,在設置位置之後的工作流才生效,需要對awk工作流程有一定的瞭解。

FS:按照字段分隔符取字段,詳見讀取文件

FIELDWIDTHS:按照字段寬度取字段,詳見讀取文件

FPAT:按照字段的模式取字段,詳見讀取文件

OFS:輸出字段分隔符,print命令會使用到。

ORS:輸出記錄分隔符,print命令會使用到。

CONVFMT:數值隱式轉換成字符串時所遵循的格式,默認值爲“%.6g”。

OFMT:使用print命令輸出小數時,數值轉換成字符串時所遵循的格式,默認值爲“%.6g”。

攜帶信息類

ARGC:參數的個數,詳見ARGC和ARGV等

ARGV:保存各個參數,詳見ARGC和ARGV等

ARGIND:ARGV中各參數的索引,詳見ARGC和ARGV等

FILENAME:當前正在處理的文件名,詳見ARGC和ARGV等

ENVIRON:引用shell環境變量,詳見ARGC和ARGV等

NR:當前已讀取的記錄數(可簡單理解爲行號),當處理多個文件時,NR不會重置而是會一直往上疊加。如果NR修改了,那麼下一條記錄會基於新的NR值。

FNR:在當前文件中已讀取的記錄數(可簡單理解爲行號),當處理多個文件時,NR會重置。如果NR修改了,那麼下一條記錄會基於新的NR值。

NF:當前記錄的字段數,詳見讀取文件。。

RT:每次記錄分隔時所採用的具體的記錄分隔符,詳見讀取文件

RLENGTH:詳見下文內置函數match()。

RSTART:詳見下文內置函數match()。

SUBSEP:多維數組中索引分隔字符,詳見數組

 

內置函數

數值類

int(x):取整函數,向0位置方向取整,也可以理解爲直接截斷小數部分。

# awk 'BEGIN{print int(3)}'
3
# awk 'BEGIN{print int(3.9)}'
3
# awk 'BEGIN{print int(-3)}'
-3
# awk 'BEGIN{print int(-3.9)}'
-3

對於包含字母字符串的,能截斷取整就截斷取整,不行就返回0。

# awk 'BEGIN{print int("3.14abc")}'
3
# awk 'BEGIN{print int("abc3.14")}'
0

sqrt(x):返回正整數的平方根,遇到負數就報錯。

# awk 'BEGIN{print sqrt(9)}'
3
# awk 'BEGIN{print sqrt(4)}'
2
# awk 'BEGIN{print sqrt(-4)}'
awk: cmd. line:1: warning: sqrt: called with negative argument -4
-nan

rand():返回隨機數,隨機數位於[0,1)。

# awk 'BEGIN{print rand()}'
0.924046

一般來說我們期望獲得一個隨機整數,那麼就會使用一個整數與之相乘,然後再結合int()。例如取得一個位於[0,10)之間的隨機數,則乘以10。

# awk 'BEGIN{print 10*rand()}'
9.24046
# awk 'BEGIN{print int(10*rand())}'
9

如果你反覆運行rand(),就會發現其每次生成的隨機數都是固定的。哪怕在SSH會話中重新連接或者新建會話窗口。

# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046

因爲在大多數awk實現中(包含gawk,不包含mawk),每次運行awk開始生成隨機數都會基於一個相同的數值或者說是種子(seed),只要這個種子的值不變,那麼隨機數就不會變。如果我們期望每次使用rand()生成隨機數時得到的數字是真隨機的話,就需要使用srand()來修改種子值。

srand([x]):不帶參數的srand()可以設置一個隨機的種子值,使得rand()返回真隨機數。

默認情況下,srand()將當前的日期和時間(精確到秒)作爲種子,也就是說如果兩次awk執行位於同一秒中,那麼使用的種子相同,生成的隨機數自然也就相同。

# awk 'BEGIN{srand();print rand()}'
0.929717
# awk 'BEGIN{srand();print rand()}'
0.145049
# awk 'BEGIN{srand();print rand()}'
0.145049

srand()會返回前一次的種子,每次都是1,也就是當我們不使用srand()指定種子的時候,每次都是使用1作爲種子,所以結果也就相同了。

# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print srand()}'
1
# awk 'BEGIN{print srand()}'
1
# awk 'BEGIN{srand(1);print rand()}'
0.924046
# awk 'BEGIN{srand(1);print rand()}'
0.924046

讓我們結合以上所學,生成一個位於[10,100]的隨機數。

awk 'BEGIN{srand();print int(10+91*rand())}'

字符串類

sprintf(format, expression1, ...):詳見輸出操作

length([string]):這個我們見過很多了,返回字符數量。如果參數是數組則返回數組元素的數量,詳見數組。如果參數爲空的話則返回$0的字符數量。

在返回字符數量的時候有一些需要注意的點,比如我們返回一個小數的時候,小數點也屬於字符數量的統計範圍中。有的時候返回的字符數量不對,是因爲可能遇到了需要使用OFMT或者CONVFMT來根據默認值“%.6g”來轉換的情況。

# awk 'BEGIN{print length(100)}'
3
# awk 'BEGIN{print length(100.123)}'
7
# awk 'BEGIN{print length(100.123456)}'
7
# awk 'BEGIN{print 100.123456}'
100.123
# awk 'BEGIN{print length(1000000000000000.123456)}'
5
# awk 'BEGIN{print 1000000000000000.123456}'
1e+15

strtonum(str):詳見語法。如果str以0、0x或者0X開頭則會識別成對應的八或者十六進制以後再轉換。

# awk 'BEGIN{print strtonum("010")}'
8
# awk 'BEGIN{print strtonum("0x10")}'
16
# awk 'BEGIN{print strtonum("0X10")}'
16

tolower(str)和toupper(str):大小寫的轉換。

# awk 'BEGIN{print tolower("aBcDeFg")}'
abcdefg
# awk 'BEGIN{print toupper("aBcDeFg")}'
ABCDEFG

index(str,substr):在字符串str中尋找子字符串(簡稱子串)substr。若找到則返回子串substr在字符串str中的起始位置,若找不到則返回0。

注意:在awk中涉及到字符索引位置的函數,其索引位置都是從1開始計算,其他大多編程語言則是從0開始計算。

# awk 'BEGIN{print index("alongdidi","di")}'
6
# awk 'BEGIN{print index("alongdidi","zzz")}'
0

基於這個特性我們可以使用該函數來判斷A字符串是否包含B字符串的功能,類似正則匹配功能。

# awk '$5~/qq.com/{print}' a.txt 
1   Bob     male    28   [email protected]     18023394012
8   Peter   male    20   [email protected]     17729348758
# awk 'index($5,"qq.com"){print}' a.txt 
1   Bob     male    28   [email protected]     18023394012
8   Peter   male    20   [email protected]     17729348758

substr()

substr(string,start[,length])

substr()函數的作用是從給定的字符串string中,根據給定的索引起始位置和長度來提取子串。

# awk 'BEGIN{print substr("alongdidi",3,3)}'
ong

長度可以省略,表示提取到字符串結束爲止。

# awk 'BEGIN{print substr("alongdidi",3)}'
ongdidi

起始位置如果是非正整數,那麼表示從1開始。如果起始位置大於字符串長度,那麼返回空字符串。

# awk 'BEGIN{print substr("alongdidi",-1)}'
alongdidi
# awk 'BEGIN{print substr("alongdidi",0)}'
alongdidi
# awk 'BEGIN{print substr("alongdidi",20)}'

如果子串的長度是非正整數,那麼返回空字符串。

# awk 'BEGIN{print substr("alongdidi",1,0)}'

# awk 'BEGIN{print substr("alongdidi",1,-1)}'

split()和patsplit()

split(string,array[,fieldsep[,seps]])

split()函數根據字段分隔符fieldsep將字符串string分割成各個字段並存入數組array中。由於fieldsep支持正則表達式,因此每次切分字段的字段分隔符可能不同,將每次實際的字段分隔符存入數組seps中。這個關係有點類似於RS和RT的關係。

# awk 'BEGIN{split("a b  c   d",arr);for(i in arr){print i"-->"arr[i]}}'
1-->a
2-->b
3-->c
4-->d

如果不指定字段分隔符,那麼默認使用FS,即默認值爲空格。因此一個或多個空格均可作爲默認的字段分隔符。

關聯數組array的索引是從1開始的。

函數的返回值是切割得到的字段個數(即數組元素的長度)。

# awk 'BEGIN{print split("a b  c   d",arr)}'
4

使用正則字段分隔符分割字段。實際字段分隔符的數組索引也是從1開始。

# awk 'BEGIN{split("a1b22c333d",arr,"[[:digit:]]+",seps);for(i in arr){print i"-->"arr[i]};for(i in seps){print i"-->"seps[i]}}'
1-->a
2-->b
3-->c
4-->d
1-->1
2-->22
3-->333

split()函數在將分割後的字段寫入數組之前,會清空該數組。因此可以將待分割的字符串設置爲空,從而用來清空某個數組。不過清空數組直接”delete arr“即可,此可是瞭解split()的工作原理。

# awk 'BEGIN{for(i=1;i<=3;i++){arr[i]=i};print length(arr);split("",arr);print length(arr)}'
3
0

如果split()函數根據已有條件無法分割字符串的話,則會將整個字符串當作一個字段存入數組。

# awk 'BEGIN{split("alongdidi",arr);for(i in arr){print i"-->"arr[i]}}'
1-->alongdidi

patsplit()函數。

patsplit(string,array[,fieldpat[,seps]])

split()和patsplit()的區別在於前者使用字段分隔符分隔字段,後者使用字段模式來匹配字段。它們的關係類似於FS和FPAT的關係(因此這裏就不再贅述patsplit()的用法,不懂的就去看看鏈接中的文章。)。如果省略fieldpat,則按照FPAT的值來。

# awk 'BEGIN{patsplit("aaa1bbb22ccc333",arr,"[[:alpha:]]+",seps);for(i in arr){print i"-->"arr[i]};for(j in seps){print j"-->"seps[j]}}'
1-->aaa
2-->bbb
3-->ccc
0-->    # 請留意這裏的實際字段分隔符以0開始。
1-->1
2-->22
3-->333

match()

match(string,reg[,arr])

使用正則表達式在字符串中進行匹配,匹配成功則返回匹配成功部分最開始的索引位置,匹配失敗則返回0。

# awk 'BEGIN{print match("alongdidi","(di)+")}'    # 從字符串alongdidi的第6個位置開始匹配成功。
6
# awk 'BEGIN{print match("alongdidi","z+")}'
0

可以指定數組參數arr,將整個匹配到的結果存入arr[0]。如果正則中使用了分組捕獲,那麼依照順序將每個分組捕獲到的數據存入arr[1], arr[2], ...。

# awk 'BEGIN{match("foo+++bar+++baz+++","(foo)\\++(bar)\\++(baz)\\++",arr);for(i=0;i<=3;i++){print i"-->"arr[i]}}'
0-->foo+++bar+++baz+++ 1-->foo 2-->bar 3-->baz

注意:CLI中想要正確將第一個+字符識別爲字面量,必須使用雙反斜線,如果使用單反斜線會有警告,並且結果會異常。

awk: cmd. line:1: warning: escape sequence `\+' treated as plain `+'

如果我們使用遍歷數組的方式會發現還存儲有其他數組元素。顧名思義是每個元素的在原字符串中的起始位置以及長度。

0start-->1
0length-->18
3start-->13
1start-->1
2start-->7
3length-->3
2length-->3
1length-->3

如果匹配成功,則會將arr[0]的起始位置和長度存入預定義變量RSTART和RLENGTH,等同於arr[0start]和arr[0length]。

# awk 'BEGIN{match("alongdidi","(di)+");print RSTART,RLENGTH}'
6 4

如果匹配失敗,則RSTART等於0,RLENGTH等於-1。

# awk 'BEGIN{match("alongdidi","z+");print RSTART,RLENGTH}'
0 -1

這種匹配成功返回正整數匹配失敗返回0的特性,可以用來表示布爾值的真假。因此可以用作if條件判斷或者pattern。

# awk 'match($2,"A.+"){print}' a.txt 
2   Alice   female  24   [email protected]  18084925203
5   Alex    male    18   [email protected]    18185904230
6   Andy    female  22   ddd@139.com    18923902352

sub()、gsub()和gensub()

這三個函數都是字符串替換(substitute)函數。它們的工作方式和sed或者vim中的基於正則匹配替換相似。

sub(regexp,replacement[,target])
gsub(regexp,replacement[,target])

在target中匹配正則regexp,如果匹配到則將其替換成replacement,並把替換後的結果重新賦值給target。因此target必須是可賦值的(變量名、數組元素名或者$N等),不能是字面量。

sub()和gsub()唯一的區別在於後者是全局替換(global)。sub()返回1或者0來表示替換成功或者失敗。gsub()返回正整數或者0來表示替換成功的次數或者替換失敗。

如果省略target的話,則使用$0。如果target是$0或者$N,由於函數會將target重新賦值,因此就涉及到$0或者$N的重建/重新計算,詳見讀取文件中的【字段與記錄的重建】部分。

# awk 'BEGIN{str="aooboocoo";count=sub("oo","xx",str);print count;print str}'
1
axxboocoo
# awk 'BEGIN{str="aooboocoo";count=gsub("oo","xx",str);print count;print str}'
3
axxbxxcxx

在替換時(replacement中)可以使用&來引用匹配成功的部分。

# awk 'BEGIN{str="aooboocoo";count=sub("oo","x&x",str);print str}'
axooxboocoo
# awk 'BEGIN{str="aooboocoo";count=gsub("oo","x&x",str);print str}'
axooxbxooxcxoox

但是,sub()和gsub()均不支持“\N”(N爲正整數)的形式來反向引用。

gensub()相對於前兩者的最大區別在於其支持反向引用,並且gensub()可以完全代替前兩者。

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

how:用來指定對第幾個匹配到的字符串進行替換。1等同於sub(),使用g/G開頭的字符串等同於gsub(),也可以指定其他正整數。

gensub()返回替換成功後的結果而不是替換成功的次數;如果匹配失敗則返回target本身。

# awk 'BEGIN{str="aooboocoo";count=gensub("oo","x",1,str);print count;print str}'
axboocoo
aooboocoo
# awk 'BEGIN{str="aooboocoo";count=gensub("oo","x","g",str);print count;print str}'
axbxcx
aooboocoo
# awk 'BEGIN{str="aooboocoo";count=gensub("zz","x","g",str);print count;print str}'
aooboocoo
aooboocoo

反向引用需要使用兩根反斜線。awk對於轉義(反斜線)的解釋比較混亂,有時候需要1根,有需要需要2根,這個需要自行測試。\0等同於&。

# awk 'BEGIN{str="abc def";new=gensub("(.+) (.+)","\\2|\\1|\\0|&","global",str);print new}'
def|abc|abc def|abc def

asort()和asorti()

asort(src[, dest[, how]])
asorti(src[, dest[, how]])

這兩個函數用來對數組進行排序。

asort()對數組的元素值進行排序,排序的原則基於how(也就是說可以按照預定排序規則或者自定義函數),排序完成以後將數組的索引值修改爲正整數序列(1, 2, 3, ...)。

asorti()與asort()的區別在於其是對數組的索引值(index/indices)進行排序。

這部分,作者在視頻中沒有講解,可能是較少使用或者較複雜,暫時留白。

IO類

1、close()。在介紹getline時已講解。

close(filename[,how])

2、fflush()。

fflush([filename])

flush任何與filename相關的輸出,filename可以是一個已打開用於寫入的文件或者是一個用於重定向輸出到管道或者協程的shell命令。

許多程序都會緩衝自己的輸出,相對於一有一點點數據就立刻將其輸出來說,緩衝機制會是效率更高。不過有時候也有必要在緩衝區還沒有滿的時候就輸出數據,因此awk提供了fflush()函數來實現這個功能。

從4.0.2開始,如果fflush()函數沒有參數或者參數是空字符串,awk會flush所有的緩衝。

fflush()    # 無參數
fflush("")    # 參數爲空字符串

當將數據輸出至管道或者協程時可能會被緩衝。而輸出到終端是行緩衝,遇到換行即輸出,此前我們說的緩衝一般叫塊緩衝,可以緩衝多行的數據。

# awk '{print $1+$2}'
1 1    # 鍵入1空格1回車
2    # 立即返回2
2 3    # 鍵入2空格3回車
5    # 立即返回 5
    # 鍵入Ctrl+d表示終止輸入
# awk '{print $1+$2}' | cat
1 1    # 鍵入1空格1回車
2 3    # 沒有返回輸出結果,再次鍵入2空格3回車
2    # 依然沒有返回,輸入Ctrl+d終止輸入後才返回結果,此前的數據都被緩衝起來了。
5

使用flush就可以使得原本輸出到管道需要緩衝的數據立刻被flush。

awk '{print $1+$2;fflush()}' | cat

3、system()。在介紹getline時已講解。

system(command)

時間類

由於awk常用於處理日誌文件,而時間對於日誌文件來說是一個非常重要的概念。因此與時間相關的內置函數就顯得很重要了。

systime()

返回當前系統時間距離epoch的秒數,我們可以勉強把它稱之爲【epoch值】。在計算機領域中,和時間相關的epoch指的是“1970-01-01 00:00:00”。

# awk 'BEGIN{print systime()}'
1612173172

mktime()

mktime("YYYY MM DD HH MM SS [DST]"[,utc-flag])

根據用於給出的日期時間格式信息,返回對應的epoch值。

# awk 'BEGIN{print mktime("2021 02 01 18 05 00")}'
1612173900

mktime()比較智能,假如用戶輸入的時間超出範圍,會自動換算成對應的時間,甚至支持負數。

# awk 'BEGIN{print mktime("2021 02 01 18 05 65");print mktime("2021 02 01 18 06 05")}'
1612173965
1612173965
# awk 'BEGIN{print mktime("2021 02 01 18 05 -5");print mktime("2021 02 01 18 04 55")}'
1612173895
1612173895

如果日期時間格式不對或者返回的時間戳超出範圍的話,mktime()返回-1。

strftime()

strftime([format[,timestamp[,utc-flag]]])

將時間戳(timestamp)轉換成用戶給定的格式(format)輸出。

這裏的格式和date命令中的格式類似,所有的格式詳見官方文檔。時間戳則是epoch值,因此該函數一般可以結合mktime()函數一起使用。

# awk 'BEGIN{print strftime("%F %T",mktime("2021 02 01 00 00 00"))}'
2021-02-01 00:00:00

如果省略時間戳,那麼使用當前的日期和時間對應的時間戳。

# awk 'BEGIN{print strftime("%F %T")}'
2021-02-01 18:25:43

如果省略格式,則使用PROCINFO["strftime"]對應的值。

# awk 'BEGIN{print PROCINFO["strftime"];print strftime()}'
%a %b %e %H:%M:%S %Z %Y
Mon Feb  1 18:26:56 CST 2021

數據類型類

isarray()

isarray(x)

判斷變量x是否是數組,如果是則返回1,否則返回0。

typeof()

typeof(x)

返回變量x的數據類型。有以下值:

  • array:數組。
  • regexp:正則字面量。
  • number:數值。
  • string:字符串。
  • strnum:形似數值的字符串,詳見語法
  • unassigned:未賦值狀態,有引用過但是沒有賦值過,詳見語法
  • untyped:未聲明的狀態,也可以理解爲未鍵入的(type本身也有從鍵盤鍵入的意思),就是既沒引用更沒賦值的,詳見語法

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章