Linux awk命令高階用法完全攻略

2020-07-16 10:04:52
前面一節已經介紹了 awk 的基本用法,其實在 awk 指令碼程式中,還支援使用一些程式語言,比如變數、陣列、分支結構(if-then-else)、迴圈結構(while)、函數等,下面一一給大家介紹。

awk 使用變數

在 awk 的指令碼程式中,支援使用變數來存取值。awk 支援兩種不同型別的變數:
  • 內建變數:awk 本身就建立好,使用者可以直接拿來用的變數,這些變數用來存放處理資料檔案中的某些欄位和記錄的資訊。
  • 自定義變數:awk 支援使用者自己建立變數。

內建變數

awk 程式使用內建變數來參照程式資料裡的一些特殊功能。常見的一些內建變數,包括上一節介紹的資料欄位變數($0、$1、$2...$n)以及表 1 、表 2 中所示的這些變數。

表 1 欄位和記錄分隔符變數
變數 功能
FIELDWIDTHS 由空格分隔的一列數位,定義了每個資料欄位的確切寬度。
FNR 當前輸入文件的記錄編號,常在有多個輸入文件時使用。
NR 輸入流的當前記錄編號。
FS 輸入欄位分隔符
RS 輸入記錄分隔符,預設為換行符 n。
OFS 輸出欄位分隔符,預設為空格。
ORS 輸出記錄分隔符,預設為換行符 n。

在表 1 中,變數 FS 和 OFS 定義了 awk 如何處理資料流中的資料欄位。我們已經知道了如何使用變數 FS 來定義記錄中的欄位分隔符,變數 OFS 具備相同的功能,只不過是用在 print 命令的輸出上,例如:

[[email protected] ~]# cat data1
data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
[[email protected] ~]# awk 'BEGIN{FS=","; OFS="-"} {print $1,$2,$3}' data1
data11-data12-data13
data21-data22-data23
data31-data32-data33
[[email protected] ~]# awk 'BEGIN{FS=","; OFS="--"} {print $1,$2,$3}' data1
data11--data12--data13
data21--data22--data23
data31--data32--data33

可以看到,print 命令會自動將 OFS 變數的值放置在輸出中的每個欄位間。通過設定 OFS 變數,可以在輸出中使用任意字串來分隔欄位。

FIELDWIDTHS 變數允許使用者不依靠欄位分隔符來讀取記錄。在一些應用程式中,資料並沒有使用欄位分隔符,而是被放置在了記錄中的特定列,這種情況下,必須設定 FIELDWIDTHS 變數來匹配資料在記錄中的位置。

一旦設定了 FIELDWIDTH 變數,awk 就會忽略 FS 變數,並根據提供的欄位寬度來計算欄位,下面是個採用欄位寬度而非欄位分隔符的例子:

[[email protected] ~]# cat data1b
1005.3247596.37
115-2.349194.00
05810.1298100.1
[[email protected] ~]# awk 'BEGIN{FIELDWIDTHS="3 5 2 5"}{print $1,$2,$3,$4}' data1b
100 5.324 75 96.37
115 -2.34 91 94.00
058 10.12 98 100.1

注意,一旦設定了 FIELDWIDTHS 變數的值,就不能再改變了,因此,這種方法並不適用於變長的欄位。

變數 RS 和 ORS 定義了 awk 程式如何處理資料流中的欄位,預設情況下,awk 將 RS 和 ORS 設為換行符。預設的 RS 值表明,輸入資料流中的每行新文字就是一條新紀錄。 有時,你會在資料流中碰到佔據多行的欄位。典型的例子是包含地址和電話號碼的資料,其中地址和電話號碼各佔一行,例如:

Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234

如果你用預設的 FS 和 RS 變數值來讀取這組資料,awk 就會把每行作為一條單獨的記錄來讀取,並將記錄中的空格當作欄位分隔符,這並不是使用者想要的。

要解決這個問題,只需把 FS 變數設定成換行符,這就表明資料流中的每行都是一個單獨的欄位,每行上的所有資料都屬於同一個欄位;與此同時,把 RS 變數設定成空字串,然後在資料記錄間留一個空白行,awk 會把每個空白行當作一個記錄分隔符。例如:

[[email protected] ~]# cat data2
Riley Mullen
123 Main Street
Chicago, IL  60601
(312)555-1234

Frank Williams
456 Oak Street
Indianapolis, IN  46201
(317)555-9876

Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
[[email protected] ~]# awk 'BEGIN{FS="n"; RS=""} {print $1,$4}' data2
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938


表 2 環境資訊變數
變數名 功能
ARGC 命令列引數個數。
ARGIND 當前檔案在 ARGC 中的位置。
ARGV 包含命令列引數的陣列。
CONVFMT 數位的轉換格式,預設值為 %.6g。
ENVIRON 當前 shell 環境變數及其值組成的關聯陣列。
ERRNO 當讀取或關閉輸入檔案發生錯誤時的系統錯誤號。
FILENAME 當前輸入文件的名稱。
FNR 當前資料檔案中的資料行數。
IGNORECASE 設成非 0 值時,忽略 awk 命令中出現的字串的字元大小寫。
NF 資料檔案中的欄位總數。
NR 已處理的輸入記錄數。
OFMT 數位的輸出格式,預設值為 %.6g。
RLENGTH 由 match 函數所匹配的子字串的長度。
TSTART 由 match 函數所匹配的子字串的起始位置。

其中,FNR 和 NR 變數雖然類似,但又略有不同。FNR 變數含有當前資料檔案中已處理過的記錄數,NR 變數則含有已處理過的記錄總數。舉個例子:

[[email protected] ~]# cat data1
data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
[[email protected] ~]# awk '
> BEGIN {FS=","}
> {print $1,"FNR="FNR,"NR="NR}
> END{print "There were",NR,"records processed"}' data1 data1
data11 FNR=1 NR=1
data21 FNR=2 NR=2
data31 FNR=3 NR=3
data11 FNR=1 NR=4
data21 FNR=2 NR=5
data31 FNR=3 NR=6
There were 6 records processed

由此可以看出,當只使用一個資料檔案作為輸入時,FNR 和 NR 的值是相同的;如果使用多個資料檔案作為輸入,FNR 的值會在處理每個資料檔案時被重置,而 NR 的值則會繼續計數直到處理完所有的資料檔案。

自定義變數

和其他典型的程式語言一樣,awk 允許使用者定義自己的變數在指令碼程式中使用。awk 自定義變數名可以是任意數目的字母、數位和下劃線,但不能以數位開頭。更重要的是,awk 變數名區分大小寫。

舉個簡單的例子:

[[email protected] ~]# awk '
> BEGIN{
> testing="This is a test"
> print testing
> testing=45
> print testing
> }'
This is a test
45

可以看到,print 語句的輸出是 testing 變數的當前值。

也可以用 awk 命令列來給程式中的變數賦值,這允許我們在正常的程式碼之外賦值,即時改變變數的值,比如:

[[email protected] ~]# cat script1
BEGIN{FS=","} {print $n}
[[email protected] ~]# awk -f script1 n=2 data1
data12
data22
data32
[[email protected] ~]# awk -f script1 n=3 data1
data13
data23
data33

需要注意的是,使用命令列引數來定義變數值會有一個問題,即設定了變數後,這個值在程式碼的 BEGIN 部分不可用,如下所示:

[[email protected] ~]# cat script2
BEGIN{print "The starting value is",n; FS=","}
{print $n}
[[email protected] ~]# awk -f script2 n=3 data1
The starting value is
data13
data23
data33

解決這個問題,可以用 -v 命令列引數,它可以實現在 BEGIN 程式碼之前設定變數。在命令列上,-v 命令列引數必須放在指令碼程式碼之前,如下所示:

[[email protected] ~]# awk -v n=3 -f script2 data1
The starting value is 3
data13
data23
data33

注意,awk 指令碼程式中輸出函數還可以使用 C 語言中的 printf 函數,具體用法可閱讀《C語言 printf 函數》 和《C語言輸出大彙總》兩篇文章,系統學習此函數的用法。

awk 使用陣列

為了在單個變數中儲存多個值,許多程式語言都提供陣列,awk 使用關聯陣列提供陣列功能。

關聯陣列跟數位陣列不同之處在於,它的索引值可以是任意文字字串。使用者不需要用連續的數位來標識陣列中的資料元素;相反,關聯陣列用各種字串來參照值。每個索引字串都必須能夠唯一地標識出賦給它的資料元素。

如果你熟悉其他程式語言的話,其實關聯陣列和雜湊表、字典的用法類似。

關聯陣列的定義和使用

在 awk 指令碼程式中,定義一個陣列變數可以使用標準複製語句,其基本格式為:

var[index]=element

其中,var 是陣列名,index 是關聯陣列的索引值,element 是資料元素值。例如:

capital["Illinois"] = "Springfield"
capital["Indiana"] = "Indianapolis"
capital["Ohio"] = "Columbus"

在參照陣列變數時,必須用索引值(index)來提取相應的資料元素值,例如:

[[email protected] ~]# awk 'BEGIN{
> capital["Illinois"] = "Springfield"
> print capital["Illinois"]
> }'
Springfield

陣列變數也是變數,也可以使用其進行基本的算術運算,例如:

[[email protected] ~]# awk 'BEGIN{
> var[1] = 34
> var[2] = 3
> total = var[1] + var[2]
> print total
> }'
37

關聯陣列的遍歷

在 awk 中遍歷關聯陣列,可以用 for 語句的一種特殊形式:

for (var in array)
{
    statements
}

這個 for 語句會在每次回圈時將關聯陣列 array 的下一個索引值賦給變數 var,然後執行一遍 statements。

再次強調,整個遍歷過程中,傳給 var 的都是每個陣列元素的索引值(也就是 index),不是陣列元素的值。

舉個例子:

[[email protected] ~]# awk 'BEGIN{
> var["a"] = 1
> var["g"] = 2
> var["m"] = 3
> var["u"] = 4
> for (test in var)
> {
>    print "Index:",test," - Value:",var[test]
> }
> }'
Index: u  - Value: 4
Index: m  - Value: 3
Index: a  - Value: 1
Index: g  - Value: 2

注意,索引值不會按任何特定順序返回,但它們都能夠指向對應的資料元素值。

刪除陣列變數

awk指令碼程式還支援從關聯陣列中刪除某個陣列索引,使用 delete 命令就可以,此命令會從陣列中刪除指定的索引值及相關的資料元素的值。

delete 命令的基本格式如下:

delete array[index]

舉個例子:

[[email protected] ~]# awk 'BEGIN{
> var["a"] = 1
> var["g"] = 2
> for (test in var)
> {
>    print "Index:",test," - Value:",var[test]
> }
> delete var["g"]
> print "---"
> for (test in var)
> {
>    print "Index:",test," - Value:",var[test]
> }
> }'
Index: a  - Value: 1
Index: g  - Value: 2
---
Index: a  - Value: 1

需要注意的是,一旦從關聯陣列中刪除了索引值,就沒法再用它來提取元素值。

awk使用分支結構

awk 支援標準的 if-then-else 格式的 if 語句,其基本格式為:

if (condition)
    statement1
else
    statements

也可以將它放在一行上,像這樣:

if (condition) statement1;else statement2

舉個簡單的例子:

[[email protected] ~]# cat data4
10
5
13
50
34
[[email protected] ~]# awk '{if ($1 > 20) print $1 * 2; else print $1 / 2}' data4
5
2.5
6.5
100
68

awk使用迴圈結構

awk 指令碼程式中,可以使用 while、do-while、for 這 3 種迴圈結構,它們各自的基本格式分別如表 3 所示。

表 3 迴圈結構基本格式及範例
基本格式 範例
while (條件) {
   執行程式碼;
}

[[email protected] ~]# cat data5
130 120 135
160 113 140
145 170 215
[[email protected] ~]# awk '{
> total = 0
> i = 1
> while (i < 4)
> {
>    total += $i
>    i++
> }
> avg = total / 3
> print "Average:",avg
> }' data5
Average: 128.333
Average: 137.667
Average: 176.667

do
{
執行程式碼;
}while(條件)

[[email protected] ~]# awk '{
> total = 0
> i = 1
> do
> {
>    total += $i
>    i++
> } while (total < 150)
> print total }' data5
250
160
315

for(變數;條件;計數器)
{
    執行程式碼;
}

[[email protected] ~]# awk '{
> total = 0
> for (i = 1; i < 4; i++)
> {
>    total += $i
> }
> avg = total / 3
> print "Average:",avg
> }' data5
Average: 128.333
Average: 137.667
Average: 176.667


從表 3 中可以看出,awk 支援使用的迴圈結構的用法和 C 語言完全一樣,除此之外,awk 還支援使用 break(跳出迴圈)、continue(終止當前迴圈)關鍵字,其用法和 C 語言中也完全相同,這裡不再過多贅述,讀者可以閱讀《C語言回圈結構和選擇結構》一章系統學習。

awk使用函數

內建函數

和內建變數類似,awk 也提供了不少內建函數,可進行一些常見的數學、字串以及時間函數運算,如表 4 所示。

表 4 awk 內建函數
函數分類 函數原型 函數功能
數學函數 atan2(x, y) x/y 的反正切,x 和 y 以弧度為單位。
cos(x) x 的餘弦,x 以弧度為單位。
exp(x) x 的指數函數。
int(x) x 的整數部分,取靠近零一側的值。
log(x) x 的自然對數。
srand(x) 為計算亂數指定一個種子值。
rand() 比 0 大比 1 小的隨機浮點值。
sin(x) x 的正弦,x 以弧度為單位。
sqrt(x) x 的平方根。
位運算函數 and(v1, v2) 執行值 v1 和 v2 的按位元與運算。
compl(val) 執行 val 的補運算。
lshift(val, count) 將值 val 左移 count 位。
or(v1, v2) 執行值 v1 和 v2 的按位元或運算。
rshift(val, count) 將值 val 右移 count 位。
xor(v1, v2) 執行值 v1 和 v2 的按位元互斥或運算。
字串函數 asort(s [,d]) 將陣列 s 按資料元素值排序。索引值會被替換成表示新的排序順序的連續數位。另外,如果指定了 d,則排序後的陣列會儲存在陣列 d 中。
asorti(s [,d]) 將陣列 s 按索引值排序。生成的陣列會將索引值作為資料元素值,用連續數位索引來表明排序順序。另外如果指定了 d,排序後的陣列會儲存在陣列 d 中。
gensub(r, s, h [, t])  查詢變數 $0 或目標字串 t(如果提供了的話)來匹配正規表示式 r。如果 h 是一個以 g 或 G 開頭的字串,就用 s 替換掉匹配的文字。如果 h 是一個數位,它表示要替換掉第 h 處 r 匹配的地方。
gsub(r, s [,t]) 查詢變數 $0 或目標字串 t(如果提供了的話)來匹配正規表示式 r。如果找到了,就全部替換成字串 s。
index(s, t) 返回字串 t 在字串 s 中的索引值,如果沒找到的話返回 0。
length([s]) 返回字串 s 的長度;如果沒有指定的話,返回 $0 的長度。
match(s, r [,a]) 返回字串 s 中正規表示式 r 出現位置的索引。如果指定了陣列 a,它會儲存 s 中匹配正規表示式的那部分。
split(s, a [,r]) 將 s 用 FS 字元或正規表示式 r(如果指定了的話)分開放到陣列 a 中,並返回欄位的總數。
sprintf(format, variables) 用提供的 format 和 variables 返回一個類似於 printf 輸出的字串。
sub(r, s [,t])  在變數 $0 或目標字串 t 中查詢正規表示式 r 的匹配。如果找到了,就用字串 s 替換掉第一處匹配。
substr(s, i [,n]) 返回 s 中從索引值 i 開始的 n 個字元組成的子字串。如果未提供 n,則返回 s 剩下的部分。
tolower(s) 將 s 中的所有字元轉換成小寫。
toupper(s) 將 s 中的所有字元轉換成大寫。
時間函數 mktime(datespec) 將一個按 YYYY MM DD HH MM SS [DST] 格式指定的日期轉換成時間戳值。
strftime(format [,timestamp]) 將當前時間的時間戳或 timestamp(如果提供了的話)轉化格式化日期(採用 shell 函數 date() 的格式)。
systime() 返回當前時間的時間戳。

時間戳指的是格林威治時間,即從 1970年1月1日8時1起到現在的總秒數。

自定義函數

除了awk 中的內建函數,還可以在 awk 指令碼程式中自定義函數,建立自定義函數的基本格式為:

function 函數名(引數1,引數2,...)
{
    執行程式碼;
}

注意,自定義函數的函數名必須能夠唯一標識此函數,換句話說,在同一個 awk 指令碼程式中,多個函數的函數名不能相同。同時,函數的引數可以有多個(0 個、1 個或多個)。

例如:

function printthird()
{
    print $3
}

此函數會列印記錄中的第三個資料欄位。

函數還能用 return 語句返回值,例如:

function myrand(limit) {
    return int(limit * rand())
}

需要注意的是,在定義函數時,它必須出現在所有程式碼塊之前(包括 BEGIN 和 END程式碼塊)。

建立函數庫

awk 提供了一種途徑來將多個函數放到一個庫檔案中,這樣使用者就能在所有的 awk 指令碼程式中使用了。為了方便大家理解,下面給大家舉個範例。

首先,我們需要建立一個儲存所有 awk 函數的檔案:

[[email protected] ~]# cat funclib
function myprint() {
   printf "%-16s - %sn", $1, $4
}
function myrand(limit)
{
   return int(limit * rand())
}
function printthird()
{
   print $3
}

要想讓 awk 成功讀取 funclib 函數庫檔案,就需要使用 -f 選項,但此選項無法和 awk 指令碼程式同時放到命令列中一起使用。因此,要使用庫函數檔案,只能再建立一個指令碼程式檔案,例如:

[[email protected] ~]# cat script4
BEGIN{ FS="n"; RS=""}
{
     myprint()
}
[[email protected] ~]# awk -f funclib -f script4 data2
Riley Mullen     - (312)555-1234
Frank Williams   - (317)555-9876
Haley Snell      - (313)555-4938