1. 程式人生 > >Linux awk總結(一)

Linux awk總結(一)

雖然sed編輯器是非常方便自動修改文字檔案的工具,但其也有自身的限制。通常你需要一個用來處理文字檔案中的資料的更高階工具,它能提供一個類程式設計環境來修改和重新組織檔案中的資料,而這正式awk能夠做到的。awk讓流編輯邁上了一個新的臺階,它提供了一種程式語言而不只是編輯器命令。在awk中,你可以做下面的事情(awk使用的是gawk版本,相對於原版awk功能更為強大,文中所有的awk均可替換為gawk):

  • 定義變數來儲存資料;
  • 使用算數和字串操作符來處理資料;
  • 使用結構化的程式設計概念(比如if-else語句和迴圈)來為資料處理增加處理邏輯;
  • 通過提取資料檔案中的資料元素,將其重新排列或格式化,生成格式化報告。

1. awk的命令格式

awk程式的基本格式如下:

awk options program file

可用的選項如下:

  1. -F fs   指定行中劃分資料欄位的欄位分隔符
  2. -f programFile   從指定的檔案中讀取程式碼資料
  3. -v var=value   定義awk程式中的一個變數及其預設值

2. 從命令列讀取程式指令碼

awk程式指令碼用一對花括號來定義,必須將指令碼命令放在兩個花括號({})中。此外,由於awk假定程式指令碼是單個文字字串,因此必須還要將指令碼放到單引號中。下面的例子在命令列上指定了一個簡單的Hello World例子:

$ awk '{print "Hello World!"}'

這個指令碼會將Hello World!列印到STDOUT。如果嘗試執行這個命令,你可能會有些失望,因為什麼都不會發生,而當你輸入任意一行文字並回車之後就會顯示。所以awk和sed類似,如果沒有指定資料檔案,就會預設從STDIN接收資料,在執行這個指令碼時會一直等待從SDTIN輸入的文字,並對輸入的每一行文字執行程式指令碼。在向STDIN中輸入檔案結束符(EOF,Ctrl+D組合鍵)後,指令碼會停止執行。

3. 使用數字欄位變數

awk會自動給每一行中的每個元素分配一個變數,在預設情況下,會按照下面的方式分配:

  • $0代表整個文字行
  • $1代表文字行中的第一個資料欄位,$2代表第二個欄位,以此類推

在文字行中,每個資料欄位都是通過欄位分隔符劃分的。awk中預設的欄位分隔符是任意的空白字元(例如空格或製表符tab)。例如:

$ cat test.txt
This is a test, this is the first line of the test.
This is a test, this is the second line of the test.
Hahahaha!
$ awk '{print $1}' test.txt
This
This
Hahahaha!

如果你要讀取採用了其他欄位分隔符的檔案,可以用-F選項指定:

$ awk -F: '{print $1}' /etc/passwd
nobody
root
daemon
_uucp
_taskgated
_networkd
_installassistant
_lp
下面省略...

由於/etc/passwd檔案是使用冒號來分離欄位的,因此要在awk選項中將冒號指定為欄位分隔符。

4. 在程式指令碼中使用多個命令

有兩種方法:可以在命令之間加個分號,也可以一行輸入一個程式指令碼命令:

$ echo "My name is oldmanw" | awk '{$4="handsomeBoy"; print $0}'
My name is handsomeBoy
$ echo "My name is oldmanw" | awk '{$4="handsomeBoy"
> print $0}'
My name is handsomeBoy

在使用了表示起始的單引號之後,bash shell會使用次提示符(>)提示你輸入更多的資料。你可以在每一行輸入一條命令,知道輸入了結尾的單引號。

5. 從檔案中讀取程式

和sed一樣,awk允許將程式儲存到檔案中,然後在命令列中引用,使用-f選項即可。

$ cat awkScript
{print "The meaning of user " $1 " is " $5}
$ awk -F: -f awkScript /etc/passwd
The meaning of user nobody is Unprivileged User
The meaning of user root is System Administrator
The meaning of user daemon is System Services
The meaning of user _uucp is Unix to Unix Copy Protocol
The meaning of user _taskgated is Task Gate Daemon
The meaning of user _networkd is Network Services
The meaning of user _installassistant is Install Assistant
The meaning of user _lp is Printing Services
The meaning of user _postfix is Postfix Mail Server
以下省略...

也可以在程式檔案中指定多條命令,只要一行放一條命令即可,不需要使用分號。

6. 在資料處理前後執行指令碼

有的時候可能需要在處理資料前執行指令碼,如為報告建立標題;或是在處理完資料後執行指令碼,在awk中分別使用BEGIN和END關鍵字實現。

$ awk 'BEGIN {print "Hello World!"}'
Hello World!

這裡print命令的結果會直接顯示出來,因為BEGIN標示的指令碼區域會在讀取資料前執行。如果想使用正常的程式指令碼處理資料,必須使用另一個指令碼區域來定義程式(如果指令碼區域過長,可以分成多行輸入):

$ awk 'BEGIN {print "Hello World!"} {print $1}' test.txt
Hello World!
This
This
Hahahaha!

END關鍵字類似,會在處理完資料後執行:

$ awk 'BEGIN {print "Hello World!"} {print $1} END {print "Bye!"}' test.txt
Hello World!
This
This
Hahahaha!
Bye!

7. 使用變數

awk支援兩種不同型別的變數:內建變數和自定義變數,下面分別介紹。

(1)內建變數

  1. FIELDWIDTHS   由空格分隔的一系列數字,定義了每個資料欄位的確切寬度
  2. FS   輸入欄位分隔符
  3. RS   輸入記錄分隔符
  4. OFS   輸出欄位分隔符
  5. ORS   輸出記錄分隔符

FS和OFS定義了awk如何處理資料流中的資料欄位。FS定義輸入流的分隔符,OFS定義了輸出流的分隔符,在預設情況下,FS為任意的空白字元,OFS為一個空格。

$ cat test2.txt
data11,data12,data13
data21,data22,data23
data31,data32,data33
$ awk 'BEGIN {FS=","} {print $1, $2, $3}' test2.txt
data11 data12 data13
data21 data22 data23
data31 data32 data33
$ awk 'BEGIN {FS=","; OFS="-"} {print $1, $2, $3}' test2.txt
data11-data12-data13
data21-data22-data23
data31-data32-data33

FIELDWIDTHS允許不依賴欄位分隔符來讀取記錄。在一些文字檔案中,資料並沒有使用欄位分隔符,而是被放置在了記錄的特定列,這種情況下必須設定FIELDWIDTHS變數來匹配資料在記錄中的位置。一旦設定了FIELDWIDTHS變數,awk就會忽略FS變數,並根據提供的欄位寬度來計算欄位:

$ cat test3.txt
1005.2347596.37
115-2.349194.00
05810.1298100.1
$ awk 'BEGIN {FIELDWIDTHS="3 5 2 5"} {print $1, $2, $3, $4}' test3.txt
100 5.234 75 96.37
115 -2.34 91 94.00
058 10.12 98 100.1

變數RS和ORS定義了awk程式如何讓處理資料流中的記錄。預設情況下awk將RS和ORS設定為換行符。預設的RS值表明,輸入資料流中的每行新文字就是一條新紀錄。而有的時候會遇到佔據多行的欄位,如下所示,每一條資料佔據4行,資料之間使用空白行進行隔離:

$ cat test4.txt
oldmanw
No.10 XiTuCheng Road
Beijing
(010)6228-1234

handsomeBoy
No.10 XiTuCheng Road
Beijing
12345678901

如果使用預設的FS和RS變數值來讀取這些資料,awk就會把每行作為一條單獨的記錄來讀取,並將記錄中的空格當作欄位分隔符,但這並不是我們所希望的。因此,對於這個例子,需要把FS設定為換行符(\n),RS設定為空字串,這樣就能夠正確的處理資料:

$ awk 'BEGIN {FS="\n"; RS=""} {print $1, $4}' test4.txt
oldmanw (010)6228-1234
handsomeBoy 12345678901

除了欄位和記錄分隔符外,awk還提供了一些其他的內建變數來幫助你瞭解資料發生了什麼變化,並提取shell環境的資訊,如下所示:

  1. ARGC   當前命令列引數個數
  2. ARGIND   當前檔案在ARGV中的位置
  3. ARGV   包含命令列引數的陣列
  4. CONVFMT   數字的轉換格式(參見pringf語句),預設值%.6g
  5. ENVIRON   當前shell環境變數及其值組成的關聯陣列
  6. ERRNO   當讀取或關閉輸入檔案時發生錯誤時的錯誤系統號
  7. FILENAME   用作awk輸入資料的資料檔案的檔名
  8. FNR   當前資料檔案中的資料行數
  9. IGNORECASE   設定成非零值時,忽略awk命令中出現的字串的字元大小寫
  10. NF   資料檔案中的欄位總數
  11. NR   已處理的輸入記錄數
  12. OFMT   數字的輸出格式,預設為%.6g
  13. RLENGTH   由match函式所匹配的子字串的長度
  14. RSTART   由match函式所匹配的子字串的起始位置

ARGC和ARGV變數允許從shell中獲得命令列引數的總數以及它們的值,但是awk並不會將程式腳本當成命令列引數的一部分:

$ awk 'BEGIN {print ARGC, ARGV[1]}' test.txt
2 test.txt
$ awk 'BEGIN {print ARGC, ARGV[0]}' test.txt
2 awk

ARGC變量表明命令列上有兩個引數,包括awk和test.txt引數(程式指令碼並不算引數)。ARGV陣列的索引從0開始,儲存引數的名稱。

ENVIRON變數使用關聯陣列來提取shell環境變數,關聯陣列用文字作為陣列的索引值,而不是數值(有點類似於hashmap?):

$ awk 'BEGIN {print ENVIRON["HOME"]; print ENVIRON["PATH"]}'
/Users/oldmanw
/Users/oldmanw/apache-maven-3.5.4/bin:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin:/Users/oldmanw/Downloads/phantomjs/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/oldmanw/Documents/gradle-4.10.1/bin:/usr/local/mysql/bin

ENVIRON["HOME"]變數從shell中提取了HOME環境變數的值,ENVIRON["PATH"]變數從shell中提取的PATH環境變數的值。可以用這種方法從shell中提取任何環境變數的值,以供awk使用。

當要在awk程式中跟蹤資料欄位和記錄時,變數FNR,NF和NR用起來就非常方便。有時你並不知道記錄中到底有多少個數據欄位,NF變數能夠讓你在不知道具體位置的情況下指定記錄中的最後一個資料欄位:

$ cat test.txt
This is a test, this is the first line of the test.
This is a test, this is the second line of the test.
Hahahaha!
$ awk '{print $1, $NF}' test.txt
This test.
This test.
Hahahaha! Hahahaha!

FNR和NR變數雖然類似,但又略有不同。FNR變數含有當前資料檔案中已處理過的記錄數,NR變數含有已處理過的記錄數,請看以下例子:

$ cat test2.txt
data11,data12,data13
data21,data22,data23
data31,data32,data33
$ awk 'BEGIN {FS=","} {print $1, "FNR="FNR}' test2.txt test2.txt
data11 FNR=1
data21 FNR=2
data31 FNR=3
data11 FNR=1
data21 FNR=2
data31 FNR=3
$ awk 'BEGIN {FS=","} {print $1, "NR="NR}' test2.txt test2.txt
data11 NR=1
data21 NR=2
data31 NR=3
data11 NR=4
data21 NR=5
data31 NR=6

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

(2)自定義變數

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

在指令碼中給變數賦值:

$ awk 'BEGIN {
> x="This is a test"
> print x
> x=123
> print x
> x=x*2+3
> print x
> }'
This is a test
123
249

和shell指令碼中類似,在awk中使用賦值語句給變數賦值,變數可以儲存數值或文字值,還可以包含數學算式。

此外,也可以使用awk命令列給程式中的變數賦值。

$ cat awkScript2
BEGIN {FS=","}
{print $n}
$ cat test2.txt
data11,data12,data13
data21,data22,data23
data31,data32,data33
$ awk -f awkScript2 n=2 test2.txt
data12
data22
data32
$ awk -f awkScript2 n=3 test2.txt
data13
data23
data33

這個特效能夠讓你在不改變指令碼程式碼的情況下就能夠改變指令碼的行為。但是使用命令列引數來定義變數會有一個問題,在設定了變數後,這個值在指令碼的BEGIN部分不可用:

$ cat awkScript3
BEGIN {print "The starting value is", n; FS=","}
{print $n}
$ awk -f awkScript3 n=3 test2.txt
The starting value is
data13
data23
data33

解決方法是使用-v選項,它允許你在BEGIN程式碼段之前設定變數。在命令列上,-v選項必須放在指令碼程式碼之前:

$ awk -v n=3 -f awkScript3 test2.txt
The starting value is 3
data13
data23
data33

由於awk的相關知識很多,因此分成兩個部分介紹,在下一部分會介紹awk對陣列的處理,匹配模式的使用,數學表示式,結構化命令(if,while,do-while,for),格式化列印,內建函式(數字函式和字串函式)以及自定義函式的使用。

 

參考文獻:Linux命令列與shell指令碼程式設計大全(第3版)