[TOC]
# Awk動作規則詳解
## 表達式
> 表達式就像是積木中形形色色的最小塊兒一樣,需要組裝一個小車時我們會將這些小塊兒積木按照一定規則邏輯組裝在一起,我們先來看看表達式都包含哪些吧。
一個表達式是通過運算符將主表達式和其它表達式結合在一起的。
`主表達式`包括 常量、變量、數組、函數調用和內建函數或者內建變量。
1. 我們先從常量和變量開始了解。
2. 然后了解連接表達式的運算符, 運算符分為五類:算數運算、比較、邏輯、條件和賦值。
3. 接著是Awk的數組使用介紹,最后是awk內建的數值運算函數和字符串函數。
### 常量
> 常量有兩種:數值 和 字符串 兩種類型.
- 字符串常量使用雙引號擴起來來定義,它可以包含轉義符號,例如 "Hello world.\n".
- 數值常量可以是整數、小數或者科學計數表達式,他們都是統一使用浮點格式存儲的,所以不同表達形式的同一數值的比較是相等的。
### 變量
> 變量就是通過一個變量名存儲一個字符串或者數字,在執行過程中變量可以被修改未其它值,所以成為變量。
變量包含以下幾種:
- 用戶自定義變量: 自定義變量名由字母、數字或下劃線序列組成,但是不能以數字開頭,變量名區分字母大小寫,對于未初始化變量值可以是空字符串""或數值0。例如:0var 就是不合法變量名,而 name01 是合法的變量名。
- 內建變量: awk預定義變量,如FILENAME代表當前正在讀的文件名,詳細列表后面我們會列出。
- 字段變量: 即 $0,$1,$2...$NF, $0 表示正行數據,$1 代表通過分隔符FS將整行$0分割出的第一個字段,以此類推,$NF表示最后一個字段(NF表示分割的字段數量).
#### 內建變量表
- ARGC : 命令行參數數量,默認"-"
- ARGV : 命令行參數值數組,與ARGC對應,默認"-".
- FILENAME: 當前讀取的文件名,默認"-"
- FNR : 正在讀取的當前文件記錄數,當記錄分隔符為"\n"換行符時,FNR就是行號。
- FS : 控制輸入字段分隔符,默認為空白符(制表符或空格)
- NF : 當前記錄的字段數量,默認"-"
- NR : Awk命令到目前讀取的記錄總數,當讀取多文件時 FNR 與 NR 就存在差別了。
- OFMT : 數字的輸出格式,默認"%.6g"
- OFS : 輸出字段分隔符,默認為制表符"\t"
- ORS : 輸出記錄分隔符,默認為回車符"\n"
- RLENGTH : match函數匹配的字符串長度
- RS : 控制輸入記錄的分隔符,默認為回車符"\n"
- RSTART : match函數匹配的字符串開始位置
- SUBSEP : 多維數組下表分隔符,默認為"\034"
### 運算符
```
賦值運算符: = , += , -=, *=,/=, %=, ^=
條件運算符: ?:
邏輯運算符: || , && , !
匹配運算符: ~ , !~
關系運算符: < <= == != > >=
連接運算符: (沒有顯示運算符,如 var1="123""xyz" , "123"與"xyz"的連接形成"123xyz"常量賦值給var1)
算數運算符: +,-,*,/,%,^
增減運算符: ++,-- (前綴和后綴的返回值區別要記住)
一元字段符: $
分組運算符: ()
數組關系符: in
```
### 內建數值運算函數
- atan2(y,x) : 返回(x,y)的反正切函數值,x,y不為0。
- cos(x) : 返回x的余弦函數
- exp(x) : 返回x的指數e^x
- int(x) : 返回x的數值部分截斷,如 x="123abc",int(x)返回數值123。
- log(x) : 返回x的自然對數
- rand() : 返回隨機數r, 0<= r < 1
- sin(x) : 返回x的正弦函數
- sqrt(x) : 返回x的平方跟
- srand(x) : 設置x 為rand()函數新種子,返回值為前一個種子,默認種子為當前時間戳
隨機數生成示例 :
```
## $RANDOM 為Shell生成[0,32767]范圍的偽隨機數變量
awk -v seed=$RANDOM 'BEGIN{
srand(seed);
for(i=1;i<=10;i++)
printf("%d%s", rand()*100, i == 10 ? "\n":",")
}'
```
### 字符串作為正則表達式
> 到目前為止,我們看到的正則表達式都是通過雙斜線包含起來的(例如:/\^[0-9]+$/),但實際上任何表達式都可以作為正則表達式匹配。
Awk先計算表達式,有必要時會將值轉換成字符串,然后翻譯字符串為正則表達式,例如:
```
## 打印第二列為數字的行數據
BEGIN{ digits = "^[0-9]+$" }
$2 ~ digits
```
**既然表達式可以組合,那么正則表達式同樣也可以由組件組成**
```
## 匹配校驗有效的浮點數
BEGIN{
sign = "[+-]?"
decimal = "[0-9]+[.]?[0-9]*"
fraction = "[.][0-9]+"
exponent = "([eE]" sign "[0-9]+)?"
number = "^" sign "(" decimal "|" fraction ")" exponent "$"
}
$0 ~ number
```
組合用法看起來很方便,但是有時候小細節還是要注意的,比如 特殊字符轉義問題。
```
sign1 = "[+-]?"
sign2 = "(\\+|-)?"
```
sign1 和 sign2 現在表達相同含義,但是 sign2 要注意使用兩個反斜線對加號+進行轉移,保證這個加號的語義不被理解為正則匹配1次或多次的含義。
解析: awk 轉換字符串 sign2 為正則表達式 `"(\\+|-)?" ==> /(\+|-)?/`
***小技巧***: 如果我們想要檢測我們寫的正則表達式是否可以匹配某字符串,可以這樣寫: `awk '$1~$2'`
### 內建字符串處理函數
- gsub(r,s) : 對$0進行全局替換r為s,返回替換次數
- gsub(r,s,t) : 對t進行全局替換r為s,返回替換次數
- index(s,t) : 返回t子串在s字符串的位置,0表示沒找到
- length(s) : 返回s字符串長度
- match(s,r) : 測試s字符串是否包含r子串,返回索引值并設置RSTART和RLENGTH,0表示不包含。
- split(s,a) : 將s字符串以FS分隔符拆分到數組a中,返回數組元素數量
- split(s,a,fs) : 將s字符串以指定fs分隔符拆分到數組a中,返回數組元素數量
- sprintf(fmt,expr-list) : 返回按照fmt格式化后的字符串,用法與printf相同(差異是不輸出結果而是返回結果)
- sub(r,s) : 對$0進行一次替換r為s,返回替換次數,0表示沒有匹配
- sub(r,s,t) : 對t進行一次替換r為s,返回替換次數,0表示沒有匹配
- substr(s,p) : 字符串截取,從p位置開始截取s字符串到結束位置。
- substr(s,p,n) : 字符串截取,從p位置開始截取s字符串長度為n部分字串。
sprintf 格式化示例:
```
x = sprintf("%10s %6d",$1,$2)
```
### **如何判斷一個變量類型是數值還是字符串?**
答案是,看你如何處理這個變量,如果進行數值運算那么變量就會強制轉換成數字,如果進行字符串處理就會轉換為字符串,例如:
```
BEGIN{
pop = "12abc"
pop += 3
res = 1 2
print pop
print res
}
輸出結果:
15
12
```
但是,有些運算符適用于數值和字符串,那就需要特殊規則了:
1. 賦值運算 = , v = e , 變量v的類型將依賴于e變量類型。
2. 比較運算 == , x == y, 如果 x 和 y 都是數值類型,那么就按照數值比較,否則就要按照字符串比較。
如果需要強制按照特定類型進行比較也比較容易,例如:
- 按照數值進行比較: $1 + 0 == $2 + 0
- 按照字符串進行比較: $1 "" == $2
***動手嘗試一下,下面這個示例會輸出什么呢?***
```
$1 < 0 { print "abs($1) = " -$1 }
輸出結果:
```
## 控制語句
控制語句包含:
- { 聲明語句塊 }
- if( 表達式 ) 聲明語句
- if( 表達式 ) 聲明語句 else 聲明語句
- while(表達式) 聲明語句
- for( 表達式; 表達式 ; 表達式 ) 聲明語句
- for(變量 in 數組) 聲明語句
- do 聲明語句 while(表達式)
- break
- continue
- next
- exit
- exit 表達式
### if else 條件判斷
語法格式:
```
if(expr1)
statement1
else
statement2
```
如果 expr1 為真(非0或者非空) 則執行`statement1`語句,否則執行`statement2`。
為了消除歧義,Awk規定 `else`永遠都匹配最近未關聯過`else`的 `if` 語句,例如下面:
```
if($1==1) if($2==1) s=1; else s=2; print s;
```
### while 循環
語法格式:
```
while(expr)
statement
```
換行符是可選的,循環之行時先判斷`expr`結果是否為真true,如果為真則執行`statement`語句塊,否則結束循環,繼續向下執行其它語句。
下面是一段將每列都逐行輸出的示例:
```
{i=1;while( i <= NF) print $(i++)}
```
### for 循環
語法格式:
```
for( exp1;expr2;expr3)
statement
expr1;
while(expr2){
statement
expr3
}
```
換行符是可選的,for語法格式等同于其下面的while語句,for的三個表達式都是可選的, 如果`expr2`表達式沒有的話表示永遠為真true(稱之為死循環)。
另外一種 `for`循環格式是遍歷數組的方法:
```
for( var in array)
statement
```
### do while 循環
語法格式:
```
do
statement
while(expr)
```
do和statement后的換行符是可選的,當statement與while之間沒回車時,statement最后必須以分號(;)結束。與while循環區別是`do while`循環的 while 在最后進行判斷,也就是會先之行一次 statement后再進行判斷`expr`是否為真。
### 改變循環邏輯
- break : 終止循環
- continue : 中止本次循環,繼續下一次的循環
###next和exit
- next : 中止本次awk繼續向下數據處理,讀取下一條記錄重新開始進行模式匹配和處理。
- exit expr : 終止awk命令的運行,可以攜帶一個返回值`expr`,默認返回0。
### 空語句- 分號(;)
一個分號(;)就可以作為空語句執行,例如下面語句:
```
### 判斷數據里是否包含空值
BEGIN{ FS = "\t" }
{
for(i=1; i<=NF && $i != ""; i++) ;
if( i <= NF) print
}
```
### 數組的使用
> Awk 提供了一維數組存儲字符串或者數字,數組不需要提前聲明也不需要定義數組大小。數組在沒有設置值時的默認值為0或者""。
Awk數組的元素索引與C語言最大不同是可以是數值也可以是字符串,我們可以理解 數組為哈希數組,數組索引為key,存儲內容為value。
倒序輸出文件內容:
```
{ x[NR] = $0 }
END{for(i=NR; i>0;i--) print x[i] }
```
#### 遍歷數組
對于數組的遍歷也有專門的for循環,語法格式:
```
for( var in array)
statement
```
for循環執行過程: 每次循環都從array數組中獲取一個不同的數組下標,然后執行statement,直到所有數組元素都遍歷完結束循環。如果循環過程中增加了數組元素,那么執行結果是不確定的,數組下標遍歷順序依賴于具體實現規則,通常是無序的。
#### 檢測數組是否包含一個數組下標方法
如果想要檢測某個數組下標是否存在于數組中,可以使用下面語句:
```
var in array
```
如果 array[var]已經存在了,則返回1,否則返回0。
```
if( "Asia" in pop ) print
```
#### 刪除數組元素
```
delete array[index]
```
#### 數組下標區分數字和字符串么?
答案是不區分,字符串做為數組的統一下標類型,也就是 數字1 和 字符串"1"作為下標是一樣的,而 01 與 1 下標是不一樣的。
#### 如何實現多維數組?
> 雖然awk沒有支持多維數組,但是我們可以通過一維數組來模擬支持多維數組。
我們以一個10x10的數組的初始化為例
```
for(i = 1; i <= 10; i++)
for(j = 1; j <= 10; j++)
arr[i, j] = 0
## 判斷某下標是否存在于arr數組中方法
if( (i,j) in arr) ...
## 遍歷數組方法
for( k in arr) ...
```
實際上我們構建的是一個大小為100的一維數組,下標為 "1SUBSEP1","1SUBSEP2",以此類推。其中SUBSEP默認為"\034"。
### 用戶自定義函數
> 除了Awk內建函數外,用戶也可以自己實現需要的函數
自定義函數的語法格式:
```
function name(parameter-list) {
statements
}
```
函數的返回值是通過函數最后的`return expr`語句返回 expr 值,如果沒有`return`那么返回值為空。
函數內部變量的生命周期是值得我們關注的,內部定義的變量默認是`全局變量`(直到Awk程序執行結束釋放), 參數列表變量的生命周期為`局部變量`(調用完成后釋放)。
下面是一個體現了局部變量和全局變量差別的例子:
```
$ awk '{ print a,b,c,f($0) }function f(a,b,c){ b = a+1; c = b+1; return c }'
輸出結果:
1
3
2
4
$ awk '{ print a,b,c,f($0) }function f(a){ b = a+1; c = b+1; return c }'
輸出結果:
1
3
2
2 3 4
```
可以看到,輸入分別為1和2。結果第二個awk命令的變量b,c就有了值,而第一個awk命令因為b,c是局部變量而輸出為空。
注意:自定義函數內部如果需要使用`局部變量`時,最好的辦法就是在參數列表的末尾定義。
自定義函數的位置可以是任意`pattern-action`語句出現的位置。
所以,其實一個Awk程序就是一系列的`pattern-action`語句塊和函數定義的組合。
### 輸出
> 輸出信息的兩個函數為 `print` 和 `printf` ,其中 print 用于簡單輸出, 而 `printf` 用于對輸出內容的格式有特定要求的輸出。
> 輸出信息默認輸出到標準輸出,當然也可以被重定向到文件中,也可以對接管道進行特殊處理。
```
print : 默認輸出 $0 到標準輸出
print 表達式1,表達式2,... : 輸出所有表達式結果, 每字段以 OFS 分隔, 以 ORS 結束行
print 表達式1,表達式2,... > filename : 覆蓋方式輸出內容到 filename 文件中(注意不調用close),而不是默認的標準輸出
print 表達式1,表達式2,... >> filename : 追加輸出內容到 filename 文件中,不是覆蓋輸出
print 表達式1,表達式2,... | command : 輸出內容
注意: print命令覆蓋輸出文件是,默認情況 filename 只會在運行awk命令時打開一次,如果調用close關閉過文件,那么再次打開時會覆蓋掉之前的內容
```
輸出分隔符的作用:
`OFS` : `output field separator` 輸出字段域分隔符,例如`print $1,$2,$3`,每個逗號分隔之間的字段都會用`OFS`替換。
`ORS` : `output record separator` 輸出記錄分隔符,例如`print $1,$2,$3`,最后一個`$3`后用`ORS`分隔符追加結束。
1. print 如果想要為每個字段域修改分隔符該如何寫呢?
```
awk 'BEGIN{OFS=":"; ORS="\n" } { print }'
awk 'BEGIN{OFS=":"; ORS="\n" } { $1 = $1 ; print }'
```
2. `printf` 的格式化用法
printf的用法與C語言非常相似(除了*用法不支持),格式串參數是必須的,它用來表示輸出內容格式,包括文本和格式表達式,每個格式表達式以%開始,以一個用來表示轉換格式的字符結束,同時中間可以包含以下三個修飾符:
- "-" : 表示字段內容進行左對齊, 例如 %-10s
- width : 填充字段寬度,0開頭則表示以0進行左填充,例如 %5d
- ".prec" : 表示字符串最大寬度,或者表示數字小數部分, 例如 %10.5f。
格式控制符號列表:
- c : ASCII字符
- d : 十進制數字
- e : 指數表達形式 [-]d.dddddddE[+-]dd
- f : 浮點數 [-]ddd.dddddd
- g : 等同 e or f ,以更短形式表達,減少無效的零。
- o : 無符號八進制格式
- s : 字符串
- x : 無符號十六進制格式
- % : 輸出一個%
下面是一個示例:
```
$ echo "張三 165 65\n李四 190 100\n王五 178 80" | awk '{ printf("姓名:%-6s 身高:%6.02f 厘米 體重:%6.02f 公斤\n", $1,$2,$3)}'
姓名:張三 身高:165.00 厘米 體重: 65.00 公斤
姓名:李四 身高:190.00 厘米 體重:100.00 公斤
姓名:王五 身高:178.00 厘米 體重: 80.00 公斤
```
3. 輸出內容重定向到標準錯誤方法?
```
print "hello" | "cat 1>&2"
system("echo hello 1>&2")
print "hello" > "/dev/stderr"
```
3. 關閉文件和管道
```
awk '{ print $0 >> "test.txt" }END{ close( "test.txt") }'
awk '{ print $0 | "sort -nrk1" }END{ close( "sort -nrk1") }'
```
為什么要關閉文件呢?
1. Linux系統對于單個程序打開文件數量有限制 `ulimit -n` .
2. 關閉文件可以將緩沖區數據寫入文件中,防止數據異常丟失.
### 輸入
`awk`程序的輸入方式有如下方式:
1. 通過文件輸入 : `awk 'program' datafile`
2. 通過管道方式輸入: `cat datafile | awk 'program'`
#### 輸入分隔符`FS`
> 默認內建輸入分隔符變量FS取值為“ ”(空白符,可以是空格 和/或 Tab符,默認分隔符可以理解為`[ \t]+`),輸入的每行數據都會按照`FS`符分隔出每個字段域(field1 ... fieldNF) , 當然我們也可以根據自己的需要來自定義`FS`分隔符的取值。
下面是設定分隔符自定義方法:
1. 通過`FS`變量修改: `BEGIN{ FS = ",[ \t]*|[ \t]+"}`
2. 通過`-F`變量修改: `awk -F',[ \t]*|[ \t]+' 'program'`
#### 多行記錄數據的使用
> 默認情況下,記錄是以換行符分隔的,當然我們可以通過`RS`變量自定義記錄分隔符,這樣我們有了更多的可能。
將每個記錄都轉化為單行數據:
`awk 'BEGIN{RS="[ \t]+"; FS="\n" }{print $1, NF}' data.txt`
#### getline輸入讀取函數
`getline` 用于讀取輸入數據記錄,這些數據可以來自當前輸入,或者來自一個文件或者管道。
getline 的使用方法:
表達式 : 影響變量
`getline` : $0,NF,NR,FNR
`getline var`: var,NR,FNR
`getline <file `: $0,NF
`getline var < file ` : var
`cmd | getline ` : $0,NF
`cmd | getline var` : var
`getline` 返回值:
1. `0` : 文件讀取完畢。
2. 正數 : 返回實際讀取到數據的字節數。
3. `-1` : 讀取文件失敗,可能是文件不存在或者不可讀取狀態等。
注意: `getline` 函數讀取文件時要注意文件是否存在,需要考慮到不存在的場景,否則你的程序會陷入無限的循環當中了。
舉例:
```
while ( getline < "file") ... ## 不安全
while ( getline < "file" > 0 )... ## 安全
```
### Awk命令行變量賦值
> Awk命令行形式有如下幾種:
```
awk 'program' f1 f2
awk -f progfile f1 f2
awk -Fsep 'program' f1 f2
awk -Fsep -f progfile f1 f2
```
這些命令行中的`f1`和`f2`都是命令行參數,并且默認代表著文件名。如果一個參數的格式為"var=text",這個參數將代表一個變量賦值操作,而不是代表文件名了。這種類型的賦值可以在一個文件讀取之前也可以在之后,例如:
```
$ awk 'FNR<=2{ print v1 ":" v2 ":" FILENAME ":" $0 }' v1="Var1" ls.awk v2="Var2" print.awk
輸出結果:
Var1::ls.awk:#!/bin/awk -f
Var1::ls.awk:
Var1:Var2:print.awk:#!/bin/awk -f
Var1:Var2:print.awk:### 模擬輸出進度條更新 ##
```
### 命令行參數
> 命令行參數在Awk命令中是通過內建變量ARGC和ARGV進行存儲的,其中ARGC存儲的是參數數量,第一個參數是"awk"命令名,存放在`ARGV[0]`變量中,之后一次遞增直到ARGC個參數。
以下面命令為例說明下參數列表:
```
awk -F'\t' '$3 > 100' countries
```
ARGC為2,ARGV[1] 的值為 "countries"。
下面是一個輸出參數列表的示例:
```
BEGIN{
for( i = 1; i < ARGC ; i++)
printf("%s%s",ARGV[i], i == ARGC -1 ? "\n" : " ")
}
```
再來看一個輸出遞增數值序列的示例:
```
$ cat seq.awk
#!/usr/bin/awk -f
## 功能: 輸出數值數列
## 參數: q , p q , p q r ; 其中 q >= p ; r > 0
## 輸出: 輸出數值 1 - q , p - q , 或者 按照 r 從 p 遞增 到 q
function usage(f){
printf("Usage: %s q\n", f);
printf(" %s p q\n", f);
printf(" %s p q r\n", f);
printf("其中 q >= p , r > 0\n", f);
}
BEGIN{
if( ARGC == 2) {s = 1 ; e = ARGV[1] ; step = 1}
else if( ARGC == 3 ){s = ARGV[1] ; e = ARGV[2] ; step = 1}
else if( ARGC == 4 ){s = ARGV[1] ; e = ARGV[2] ; step = ARGV[3] }
else {
usage(ARGV[0])
exit 1
}
for( i = s; i<= e ; i += step )
printf("%d\n", i)
}
執行方法:
$ awk -f seq.awk 1 10 2
輸出結果:
1
3
5
7
9
```
`ARGV`變量值是可以被修改和增加的,如果我們將某個變量賦值為空"",表示這個參數將不會被當作文件進行解析處理,當Awk命令行參數值為 "-" 時,代表從標準輸入進行數據讀取。
## 與其他程序交互
> 本章節我們會了解到Awk程序與其他命令交互的幾種方式。
### 通過函數 system
> `system(cmd)`函數是Awk命令內建命令,可以用來在Awk命令中執行操作系統中的命令,例如 cat/grep/sed/zcat等等,然后返回執行結果狀態。
以下是一個執行`cat`命令的示例:
```
$1 == "#include" {gsub(/"/," ",$2); system("cat " $2) ; next}
{print}
```
執行結果:
```
$ cat a.h
//filename:a.h
int abc = 1024;
int f( int v1, int v2)
{
return v1 + v2;
}
$ cat a.cpp
//filename:a.cpp
#include<iostream>
#include "a.h"
using namespace std;
int main(int argc,char argv[][])
{
cout << "Hello World!"<<endl;
return 0;
}
$ awk '$1 == "#include" {gsub(/"/," ",$2); system("cat " $2) ; next} { print }' a.cpp
//filename:a.cpp
#include<iostream>
//filename:a.h
int abc = 1024;
int f( int v1, int v2)
{
return v1 + v2;
}
using namespace std;
int main(int argc,char argv[][])
{
cout << "Hello World!"<<endl;
return 0;
}
```
### 與Shell結合實現命令腳本
> 我們可以將Awk命令放在Shell腳本中執行完成指定功能。
接下來,我們來看一下如何編寫一個命令腳本:
```
$ cat field1.sh
awk '{ print $1}' $@
```
這是一個實現功能為輸出第一列數據的命令腳本,只會輸出文件中第一列數據。
執行方法:
```
## 增加可執行權限
$ chmod +x field1.sh
## 執行命令
$ ./field.sh file1 file2
```
## 總結
> 本章內容比較詳細的介紹了Awk的各種使用細節。所以可以在學習過程中詳細閱讀并實踐練習,只有多實踐才能更加深刻的理解Awk的使用方法。
我更加希望大家學習完成之后,可以多多的獨立編寫一些自己的腳本程序,腳本不用很大,通過編寫來實現自己需要的功能,這樣學習完成后才會記得更加深刻。
--- END
- 目錄
- 概述
- 第一章 編寫第一個Awk命令
- 1.1 什么是Awk命令
- 1.2 第一個Awk命令
- 第二章 Awk的模式匹配
- 2.1 Awk模式語法規則
- 2.2 Awk模式規則詳解
- 第三章 Awk的動作規則
- 3.1 Awk動作匹配語法規則
- 3.2 Awk動作規則詳解
- 第四章 Awk數據處理方法
- 4.1 數據轉換和提煉
- 4.2 數據驗證
- 4.3 數據打包與拆包處理
- 4.4 多行數據處理
- 4.5 隨機數生成
- 第五章 Awk的輸出報告和腳本封裝
- 5.1 輸出報告
- 5.2 封裝查詢結果和報告
- 第六章 Awk實現排序算法
- 6.1 插入排序算法實現
- 6.2 快速排序算法實現
- 6.3 堆排序算法實現
- 6.4 拓撲排序算法實現
- 總結