Linux文本處理三劍客之awk學習筆記10:函數

前言

關於函數的基本概念,在學習bash的函數的時候已經大致講解過了,加上本人大學時期也學習過C語言(雖然都忘記了),因此這裏就不再對函數做過多冗餘的介紹了。

awk大致將函數分成了自定義函數和內置函數。不過其本質上沒有區別,自己寫的函數就叫做自定義函數,而官方寫好的嵌入在awk本身的我們直接拿來用的函數就叫做內置函數。關於內置函數的介紹請見這裏

本博文學習的內容是瞭解函數,學會如何創建與使用它們。也就是學會自定義函數。雖然內置函數可以拿來直接使用,不需要了解其內部實現。但是學習自定義函數就是在學習函數的基礎。

函數的定義

function funcName([arg, ...]){
    ... function body ...
}

func funcName([arg, ...]){
    ... function body ...
}

awk的函數可以定義在代碼的任意位置,沒有先後順序之分。例如定義在下面的下劃線位置:

awk '_BEGIN{}_main{}_main{}_END{}_' ...

這是因爲awk在執行BEGIN代碼塊執行,awk就會將代碼編碼成內部格式,而在這一步中就會去識別代碼中的函數定義了。這在awk的工作流程中就有講述到了。

注意:別把函數定義在main代碼塊中即可。畢竟不是每次內部循環一次就要定義一次函數。

因此可以在任意位置調用任意位置定義好的函數。

# awk 'BEGIN{f()}function f(){print "hello world"}'
hello world

函數的返回值

函數使用return語句來返回返回值。一旦遇到return語句,在return語句後面的函數內部語句就不會執行。

# awk 'func re(){return 100;print "hello world"} BEGIN{a=re();print a;print re()}'
100
100

注意:返回值也可以是字符串。

# awk 'func re(){return "abc";print "hello world"} BEGIN{a=re();print a;print re()}'
abc
abc

如果函數沒有return語句或者return語句沒有具體的返回值,則返回空字符串。

# awk 'func f(){} BEGIN{print "---"f()"---"}'
------
# awk 'func f(){return} BEGIN{print "---"f()"---"}'
------
# awk 'func f(){return 100} BEGIN{print "---"f()"---"}'
---100---

函數的參數

函數可以不帶參數,不過大多數時候是帶參數的,這樣使得函數在調用時更加靈活。

# cat funcArg.awk
func f(a,b){
    print a
    print b
    return a+b
}

BEGIN{
    x=10;y=20
    res=f(x,y)    # 調用函數時打印了x和y的值,並將返回值賦值給res
    print res
    print f(x,y)    # 在打印函數返回值的同時由於調用了函數,而函數本身包含了打印x和y的值,所以先打印x和y的值,再打印返回值。
}
# awk -f funcArg.awk
10
20
30
10
20
30

使用函數來重複連接字符串。函數接受2個參數,其一是想要串聯的字符串,其二是想要串聯的次數。

# cat funcCatStr.awk
func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5)}
# awk -f funcCatStr.awk
-----

在編程語言中,函數的參數有兩種,形式參數和實際參數。

在函數定義時用來定義函數可接受的參數的參數稱爲形式參數,簡稱形參。在函數調用時實際向函數傳遞的參數成爲實際參數,簡稱實參。

f(x,y){}    # 定義形式參數x和y。
a=10;b=5
f(a,b)    # 傳遞實際參數a和b。

在計算機英語中,我們使用parameter來表示形參,使用argument來表示實參。如果某種情況下沒有實參和形參之分的話,那麼parameter和argument都可以用來表示參數之意。

在函數調用時,實參和形參的個數可以不一致。但是,如果實參數量多於形參數量,那麼awk會返回警告信息。

# awk 'func f(a,b){} BEGIN{f(1,2,3)}'
awk: cmd. line:1: warning: function `f' called with more arguments than declared

參數類型衝突

實參和形參的變量類型需要一致,否則會報錯。

# 實參是數值變量,而形參是數組。
# awk 'func f(a){a["name"]="alongdidi"} BEGIN{x=10;f(x)}'
awk: cmd. line:1: fatal: attempt to use scalar parameter `a' as an array
# 首先進行函數的調用,實參x被識別爲形參中的數組,因此在BEGIN的後續想要將實參x當作數值變量來使用會報錯。
# awk 'func f(a){a["name"]="alongdidi"} BEGIN{f(x);x=10}'
awk: cmd. line:1: fatal: attempt to use array `x' in a scalar context

參數的傳遞方式

首先我們回顧一下bash中的變量的概念,變量的名稱,起始是指向某個內存空間的地址,我們引用變量,就是引用對應地址的內存空間中的數據。

在函數參數傳遞時,有兩種傳參方式:

  1. 先找到地址對應的內存空間中的數據,將該數據複製一份放入新的內存空間中。將該值作爲參數傳遞就是將形參指向該新內存空間。這種方式叫做按值傳遞,會產生新的內存空間。
  2. 直接將實參所對應的內存空間的地址傳遞給形參,使得實參和形參同時指向了同一個內存空間。這種指向應該是基於指針的概念。這種方式叫做按引用傳遞。

由此可見,按值傳遞使用不同的內存空間,因此即便實參和形參的變量名稱相同,其指向的內存地址也回是不同的。

因此按值傳遞的函數內部的變量修改不會影響到函數外部,反之亦然。

在awk中,如果傳遞的參數的變量類型是數值或者字符串,則是按值傳遞。

# awk 'func f(a){a=10} BEGIN{a=5;print a;f(a);print a}'
5
5
# awk 'func f(a){a="alonggege"} BEGIN{a="alongdidi";print a;f(a);print a}'
alongdidi
alongdidi

按引用傳遞由於使用了相同的內存空間,並且傳參時傳遞的是內存的地址。因此按引用傳遞的函數內部的變量修改會影響到函數外部,反之亦然。如果傳遞的參數的是數組,則是按引用傳遞。

# awk 'func f(a){a["name"]="alonggege"} BEGIN{a["name"]="alongdidi";print a["name"];f(a);print a["name"]}'
alongdidi
alonggege

函數中變量的作用域

我們先來回顧funcCatStr.awk代碼。

# cat funcCatStr.awk
func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5)}
# awk -f funcCatStr.awk
-----

我們新增一部分代碼。

func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print cat("+",5)}    # 紅色字體爲新增部分。

此時我們可能會理所應當地認爲輸出的結果應該是:

-----
+++++

但是:

# awk -f funcCatStr.awk
-----
-----+++++

造成這種結果的原因和變量的作用域有關。在awk中,函數內部定義的變量屬於全局變量,因此在函數內部的變量newStr是一個全局變量,經過第一次函數調用以後,它的值是“-----”,函數返回以後,由於它是全局變量,因此該變量不會被釋放,在第二次函數調用時會在“----”的基礎之上進行操作。

我們可以在第一次函數調用後print看看。

# cat funcCatStr.awk
... ...
BEGIN{print cat("-",5);print newStr;print cat("+",5)}
# awk -f funcCatStr.awk
-----
-----
-----+++++

在awk中,沒有顯式定義局部變量的關鍵詞。如果希望將某個變量具有局部變量的特性的話,可以將變量置於函數定義時的參數的位置,即形參的位置。

# cat funcCatStr.awk
func cat(str,count    ,newStr){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print newStr;print cat("+",5)}
# awk -f funcCatStr.awk
-----

+++++

由於newStr並不是真實的參數,放在形參的位置僅僅是因爲我們希望使其具備局部變量的特性罷了,因此真實的形參現在前面,作爲局部變量的假形參寫在後面,並使用多個空格分隔。

如果我們看到一個函數的定義是這樣的,我們就應該明白該函數僅支持2個參數,不要向c和d傳遞參數,因爲它們僅僅作爲局部變量存在。

func f(a,b    ,c,d){...}

到這裏我們應該也會明白所有的形參,無論是作爲真實的形參還是局部變量,它們都具備有局部變量的特性。

cat funcCatStr.awk
func cat(str,count    ,newStr){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print cat("+",5);print str;print count}
[root@c7-server awk]# awk -f funcCatStr.awk
-----
+++++
    # 打印形參str,結果爲空字符串。
    # 打印形參count,結果爲空字符串。

 

實戰

寫一個一次性讀取文件的所有數據的函數

# cat funcReadFile.awk 
func readFile(file    ,RSBak,data){
    RSBak=RS
    RS="^$"
    if((getline data<file)<=0){
        print "Reading file error!"
        exit 1
    }
    close("c.txt")
    RS=RSBak
    return data
}

/^1/{
    print $0
    content=readFile("c.txt")
    print content
}
# awk -f funcReadFile.awk a.txt 
1   Bob     male    28   [email protected]     18023394012
abc
def
ABC
DEF

10  Bruce   female  27   bcbd@139.com   13942943905
abc
def
ABC
DEF

寫一個可以重讀文件的函數

在處理某個文件的時候,如果遇到某些條件(比如讀取到第3行),我們就要求重新讀取一遍該文件。

PS:個人覺得這個示例怪怪的,需求都怪怪的。

# cat funcRewind.awk 
func rewind(){
    for(i=ARGC;i>ARGIND;i--){
        ARGV[i]=ARGV[i-1]
    }
    ARGC++
    nextfile
}

NR==3{    # 這裏如果改成FNR的話,會陷入死循環。
    print
    rewind()
}

{
    print
}

# awk -f funcRewind.awk a.txt 
ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
2   Alice   female  24   [email protected]  18084925203
ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
2   Alice   female  24   [email protected]  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   [email protected]    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   [email protected]     17729348758
9   Steven  female  23   [email protected]    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905

格式化數組的輸出

當我們有一個數組的時候,我們是無法直接使用print語句將其全部輸出的。

# awk 'BEGIN{arr["name"]="alongdidi";arr["age"]=29;arr["gender"]="male";print arr}'
awk: cmd. line:1: fatal: attempt to use array `arr' in a scalar context

現在我們編寫一個自定義的函數,來輸出這個數組的數據,輸出的格式如下。

{
    arr["name"]="alongdidi"
    arr["age"]=29
    arr["gender"]="male"
}
# cat funcA2S.awk
func a2s(arr    ,str){
    for(i in arr){
        str=str""(sprintf("\tarr[\"%s\"]=%s\n",i,arr[i]))
    }
    return "{\n"str"}"
}

BEGIN{
    arr["name"]="alongdidi"
    arr["age"]=29
    arr["gender"]="male"
    print a2s(arr)
}
# awk -f funcA2S.awk
{
    arr["age"]=29
    arr["name"]=alongdidi
    arr["gender"]=male
}

識別文件名帶等於號的文件

一般來說,如果出現了這種CLI,那麼awk會將a=b識別變量賦值,倘若“a=b”真的是一個文件的話,我們只需要在爲其帶上相對路徑即可。

awk -f xxx.awk a=b a.txt c.txt
awk -f xxx.awk ./a=b a.txt c.txt
# awk '{print}' a=b
^C
# awk '{print}' ./a=b
aaa
bbb
ccc

思路:

  • 所有的CLI的參數保存在ARGV中,因此從中尋找文件名形似變量賦值的文件。
  • 變量賦值的規律:
    • 等於號。
    • 變量名包含數字、字母和下劃線並且只能以字母或者下劃線打頭。
  • 找到後替換ARGV中對應的參數。
  • 要提供一個開關選項,畢竟不是每次都會遇到文件名形如變量賦值的形式。
# cat funcRecogAssignFile.awk
func recog(argv,argc    ,i){
    for(i=1;i<argc;i++){
        if(argv[i]~/^[[:alpha:]_][[:alnum:]_]*=.*/){
            argv[i]="./"argv[i]
        }
    }
}

BEGIN{
    if(open){
        recog(ARGV,ARGC)
    }
}

{print}
# awk -f funcRecogAssignFile.awk a=b
^C
# awk -v open=1 -f funcRecogAssignFile.awk a=b
aaa
bbb
ccc

識別時間

在學習了時間類內置函數以後,我們可以基於已有的時間類內置函數來識別一些在日誌文件中常見的時間格式,將其轉換爲epoch值(即時間戳)。這樣有助於我們的後續深入的運維工作。

在運維工作中,一般遇到的日誌文件中的時間格式一般都形如以下兩種:

2019-11-11T03:42:42+08:00
Sat 26. Jan 15:36:24 CET 2013

這種日期時間格式無法直接拿來比較,必須先轉換成epoch值。但是這種格式也無法直接被mktime()轉換成epoch值,需要先做處理。

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

因此我們可以自定義兩個函數來將上面的兩種格式轉換爲epoch值。

str1ToTime()

2019-11-11T03:42:42+08:00

思路:

  1. 將字符串轉換成mktime()可識別的格式“Y M D H m S”。注意:需要使用sprintf()來構建這種格式。
  2. 然後使用mktime()輸出時間戳。
# cat str1ToTime.awk
func str1ToTime(str    ,newStr,Y,M,D,H,m,S,arr){
    newStr=gensub("[-:T+]+"," ","g",str) # 2019 11 11 03 42 42 08 00
    split(newStr,arr)
    Y=arr[1]
    M=arr[2]
    D=arr[3]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    # print mktime(Y M D H m S)
    # Do not write like this, otherwise mktime() return -1!!!
    # Use sprintf() instead!!!
    return mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
}

BEGIN{
    print str1ToTime("2019-11-11T03:42:42+08:00")
    print str1ToTime("2021-11-11T03:42:42+08:00")
    print (str1ToTime("2019-11-11T03:42:42+08:00") < str1ToTime("2021-11-11T03:42:42+08:00"))
}

# awk -f str1ToTime.awk
1573414962
1636573362
1

需要注意,日期和時間信息存入數組以後不能直接拿來用,必須要使用sprintf()轉換纔可以。

mktime(Y M D H m S)    # 這樣會使得mktime()接收6個參數,實際上它只能接收2個,其中一個還是可選的。
mktime("Y M D H m S")    # 這樣寫的話,就無法變量替換了,而是識別了字符串字面量。

str2ToTime()

Sat 26. Jan 15:36:24 CET 2013

思路:

  • 與str1ToTime()類似,區別在於需要識別月份“Jan”,因此需要事先寫一個映射函數。
# cat str2ToTime.awk
func str2ToTime(str    ,Y,M,D,H,m,S,arr){
    patsplit(str,arr,"[[:alnum:]]+")
    Y=arr[8]
    M=monthMap(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))
}

func monthMap(mon    ,mrr){
    mrr["Jan"]=1
    mrr["Feb"]=2
    mrr["Mar"]=3
    mrr["Apr"]=4
    mrr["May"]=5
    mrr["Jun"]=6
    mrr["Jul"]=7
    mrr["Aug"]=8
    mrr["Sep"]=9
    mrr["Oct"]=10
    mrr["Nov"]=11
    mrr["Dec"]=12
    return mrr[mon]
}

BEGIN{
    print str2ToTime("Sat 26. Jan 15:36:24 CET 2013")
    print mktime("2013 01 26 15 36 24")
}
# awk -f str2ToTime.awk
1359185784
1359185784

 

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